ロゴ ロゴ

AtCoder 169 (ABC 169 C) のかけ算問題に苦戦した話

まずはじめまして

あげどーふ(最初tofuだったけど、似たような名前の方がいたので変えたい)と言います

ここでブログを書くのは初めてです

自己紹介はまた別の機会にやると思う(?)ので、そこで詳しいこと書くと思います

ABC(AtCoder Biginner Contest) 169-C (かけ算問題)の問題概要

問題引用元 : AtCoder 169 C問題

A×Bの小数点以下を切り捨て、結果を整数として出力してください

この問題の制約

・ 0≤A≤10^15
・0≤B<10
・Aは整数
・Bは小数第 2 位まで与えられる

最初にひっかかったこと

まず与えられたA×Bを普通にかけ算して、Math.floorで処理すればいいのではないかと思うじゃないですか(桁溢れとか考慮しないといけないんだけど、最初からlong使いには関係なかった…?)

結論から言えば、これはエラーは発生しないものの、AC判定をもらうことができません

理由は小数の処理をする過程で、精度が足りない・誤差がでる(記事によれば代入した段階で)らしいです

詳しい説明は 浮動小数点数オタクが AtCoder Beginner Contest 169 のC問題をガチで解説してみる にお任せしますが、要はコンピューターで最終的に計算するときには2進数で、普段人間の使う10進数の小数を扱うと誤差が出てしまうので、正確に10進数で計算できるようにするのが正攻法らしいです

例えばJavaだとBigDecimalを使うことになると思うのですが、JavaでこれをやるとTLEしそうなので今回は避けました

どうにかACをもらうための苦しい方法

残念ながら私はABC開催中にこの問題でACを取れず、どうしても悔しいと思いリトライしたところ、なんとも苦しい方法で突破することができたので紹介します

それは…
小数点を含む入力を最初から小数点として扱わず、文字列として計算するタイミングまで扱う

計算するタイミングで10倍か100倍して無理やり整数にする

再度計算結果を文字列に戻してから、本来小数だった位の部分を文字列の切り取り機能でなかったことにする

というものです

紹介した記事でいうところの「小数点を無視して整数として読み取る」っていうのに近いと思います

実際に書いたソース

public class Main {

    static java.util.Scanner scan = new java.util.Scanner(System.in);

    //10を指定された回数かける
    public static int pow(int a){
        int b = 1;
        for(int i = 0; i < a;i++){
            b *= 10;
        }
        return b;
    }

    public static void main(String[] args) {

        //入力を空白で区切る
        String[] line = line().split(" ");
        //小数点入りの入力を整数と小数部分で区切る
        String[] sliced_period = line[1].split("\\.");

        //1つ目には aを
        //2つ目には bの整数部分を
        //3つ目には bの小数部分を代入
        Long[] vars = new Long[]{Long.parseLong(line[0]),Long.parseLong(sliced_period[0]),Long.parseLong(sliced_period[1])};

        Long first = vars[0]; //a
        //bの小数点以下が何桁なのか
        int period_digit = sliced_period[1].length();
        //bの小数点以下の値を整数として扱うために、bの元々の整数を10のbの小数桁乗する
        Long second = (vars[1] * (pow(period_digit)))+vars[2];
        //ここで初めて a×bが計算される
        Long calc = first * second;
        //Long型を一度文字列にし、小数点以下が0であることは自明であることから、整数部分を取り出す
        String parse = String.valueOf(calc).split("\\.")[0]; 

        //a×bは10のbの小数桁乗されている
        //よって何乗かした値を元に戻す際に、全部切り捨てられてしまう場合は0として事前に終了させる
        if(parse.length() - sliced_period[1].length() <= 0){
            puts("0");
            return;
        }

        //文字列にしたa×bは10のbの小数桁乗されているので、これを切り捨てる(先頭から何乗かされるまでの文字列分を切り取る)
        String floor = parse.substring(0,(parse.length())-sliced_period[1].length());
        puts(floor);

        scan.close();
    }

    public static String line(){
        return scan.nextLine();
    }
//以下略

工夫した点(苦肉の策)

上に貼ったコードのうち、工夫したのは次の2点です

小数を小数のまま代入しない

コードで言うとこの部分です

//1つ目には aを
//2つ目には bの整数部分を
//3つ目には bの小数部分を代入
Long[] vars = new Long[]{Long.parseLong(line[0]),Long.parseLong(sliced_period[0]),Long.parseLong(sliced_period[1])};

ここで文字列として取得した値を.(ピリオド)で区切って、整数と小数をコンピューターに文字列として扱い、精度を落とさないようにしました

小数を含む値を整数として処理する

コードで言うとこの部分です

//bの小数点以下が何桁なのか
int period_digit = sliced_period[1].length();
//bの小数点以下の値を整数として扱うために、bの元々の整数を10のbの小数桁乗する
Long second = (vars[1] * (pow(period_digit)))+vars[2];

この部分でa.bbに100 とか c.dに10をかけて無理やり整数として扱うようにしています

もちろんここでかけた100とか10は最後切り捨てる(文字列の特定の長さだけ切り出すことで切り捨てをする)ことでなんとかしています

個人的にはこの辺の処理が一番苦肉の策というか泥臭いように感じながらコードを書いてました(とくに文字列から桁数出してるところとか、切り捨てのところとか)

最後に

AtCoderの問題はアルゴリズムやデータ構造といった知識があればすんなり解けるらしい(?)のですが、強引にプログラミングするとこうなるよっていう記事でした

実際にこのアイディアが閃いたのはABC 169が終わってちょうど1日経つぐらいだったので、現実とは残酷なものです(当日はAだけ解けたもののB/CがWAを連発したため、不貞腐れてゲームしてました)

本番思いついていれば…とか、こんなゴリ押しに近いけどいいの…?とは思うものの、そのとき閃かないと意味がないし、逆にACがもらえればどんなやり方であろうとスコアになるのは変わりない事実だし…

一応ソースは ここ AtCoderのページ で見られるのと、まぐれでACとったんじゃね?とか指摘があれば教えてください

💬 みんなのコメント

  1. pine pine より: 2020/06/03(水) 09:48

    AC出なかったので助かる

コメント入力

関連サイト