競プロをはじめた家事手伝いロボットのブログ

競技プログラミングをしている家事手伝いロボットのブログです

ARC156 D - Xor Sum 5 コンテスト中に思ったこと/解くまでの道筋

これはなに

hackmd.io

こんな感じの「こう考えて解きました」系の記事を書きたくなったので、書きます。 Twitter でツイートするくらいの気持ちで書いているのであんまり丁寧じゃないです(Twitter アカウントが凍結されたので)。 考察で当たり方針をさくさく引けた気がするので、そういう感じの記事になります。

問題概要

正整数列 $(A _ i) _ {1\leq i\leq N}\ (1\leq N\leq 1000,0\leq A _ i\leq 1000)$ と正整数 $K\ (1\leq K\leq10^{12})$ が与えられます。

すべての $(X _ i) _ {1\leq i\leq K}\ (1\leq X _ i\leq N)$ にわたる $\displaystyle\sum _ {i=1} ^ KA _ {X _ i}$ の総 xor を求めてください。

第一とっかかり

サンプルを読むと $40$ が 2 つあったので、「$\displaystyle\sum _ {i=1} ^ KA _ {X _ i}$ の値が同じ選び方がいっぱいあるかも、特に偶数通りの選び方で値が同じになるものがいっぱいあるかも」と思いました。

実際いっぱいあって、$A _ i$ を $C _ i$ 回選ぶような選び方は $\dfrac{K!}{\prod _ {i=1} ^ {N}C _ i!}$ 通りあります(これは多項係数というやつです)。

これが奇数になるような条件を考えます。

$K!$ が $2$ で割り切れる回数は、$K+\left\lfloor\dfrac K{2}\right\rfloor+\left\lfloor\dfrac K{2 ^ 2}\right\rfloor+\cdots$ 回で計算できます。 これは、$2K-\operatorname{popcount}(K)$ です。 この式変形は

atcoder.jp

この問題で行ったことがあるので、比較的すぐできました。

よって、$\dfrac{K!}{\prod _ {i=1} ^ {N}C _ i!}$ が $2$ で割り切れる回数は $\displaystyle\sum _ {i=1} ^ N\operatorname{popcount}(C _ i)-\operatorname{popcount}(K)$ 回です。 これが $0$ になるような $C _ i$ の条件が考えたいものでした。

2進法で考えて、 $C _ i$ を合計するときに繰り上がりが生じると $\operatorname{popcount}$ の合計が減ってしまうことから、$C _ i$ は $K$ の $0$ 個以上のビットを重複なく担当している感じだとわかります。

これをぐっと睨むと $K=\displaystyle\sum _ {i=1} ^ x2 ^ {e _ i}\ (e _ i$ は相異なる$)$ に対して、奇数回出現する値は $\displaystyle\sum _ {i=1} ^ x2 ^ {e _ i}A _ {f _ i}$ と書けることがわかります。

よって、この問題は $N ^ x$ 通りの $(f _ i) _ {1\leq i\leq x}\ (1\leq f _ i\leq N)$ に対して $\displaystyle\sum _ {i=1} ^ x2 ^ {e _ i}A _ {f _ i}$ の総 xor を求める問題であることがわかります。

$N ^ K$ 通りが $N ^ x\leq N ^ {\log _ 2 K}$ 通りに減りました。嬉しい。

第二とっかかり

総 xor が欲しいので、ビットごとに考えたいです。

こういう問題は

  • 上の桁から決まる
  • 下の桁から決まる

のどっちかだと解きやすそうです。

総和の xor を計算したいので、繰り上がりを考えると上の桁からは決まりそうにないです。 下の桁から考えます。

1 の位はわかりそうです。 $K$ が奇数のときは $A _ i$ の 1 の位の総 xor によって決まりそうですし、偶数のときは $0$ です。

2 の位は、下の結果が流れてくる場合があるかもしれません。 ですが、流れてくる値がたかだか $500$ なので、$500\times1000$ だけの時間をかけると、たかだか $1500$ 通りの状態になります(ここを思いつくのにしばし時間がかかりました)。

同様に、下から流れてくる結果がたかだか $1000$ 状態で、$1000\times1000$ だけの時間をかけてたかだか $2000$ 通りの状態を作ることができます。

つまり、$2 ^ k$ の位まで見た結果 $\lbrace s _ 1,s _ 2,\ldots,s _ k\rbrace$ の $k$ 通りの和があったとして、$2 ^ {k+1}$ の位まで見ると $\left\lbrace \dfrac{s _ 1}2+A _ 1,\dfrac{s _ 1}2+A _ 2,\ldots,\dfrac{s _ 1}2+A _ N,\dfrac{s _ 2}2+A _ 1,\dfrac{s _ 2}2+A _ 2,\ldots,\dfrac{s _ 2}2+A _ N,\ldots,\dfrac{s _ k}2+A _ 1,\dfrac{s _ k}2+A _ 2,\ldots,\dfrac{s _ k}2+A _ N\right\rbrace$ のたかだか $kN$ 通りの和があることになります。

このうち偶数回出てきているものを $0$ 個にして奇数回出てきているものを $1$ 個にするとたかだか $2000$ 通りになってうまくいきそうですね。 これを繰り返すとうまく計算ができるはずです。 → WA

第三とっかかり

愚直を書いてランダムチェックをかけると $N$ が偶数のときにこわれていそうでした。

1 の位はわかりそうです。$K$ が奇数のときは $A _ i$ の 1 の位の総 xor によって決まりそうですし、偶数のときは $0$ です。

ここで、$A _ i$ の 1 の位の総 xor が 1 なら答えの 1 の位も 1 としていたのが悪かったですね。 なぜなら、同じ $1$ の位が $N ^ {x-1}$ 通りあるからですね。

よって、$N$ が偶数のときは最後以外は $0$ にする必要がありました。 → AC

余談

時間計算量は $O(N\max _ i A _ i\log _ 2K)$ になります。 が、上の位に移動する操作で $O(N\max _ iA _ i)$ 時間かけるところを、bitset を使って $O(N\max _ iA _ i/w)$ 時間や畳み込みを使って $O(\max _ iA _ i\log\max _ iA _ i)$ 時間にすることができます。

bitset を使って最速になりました。

この節が書きたかったのでこの記事を書いたわけですね。

追記: 無事陥落しました。

実装例

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>

int main() {
    using namespace std;

    unsigned long N, K;
    cin >> N >> K;
    vector<unsigned long> A(N);
    for(auto&& a : A)cin >> a;

    vector<unsigned long> possible{0};
    vector<unsigned long> count(2000);
    const auto reconstruct{[&possible, &count]{
        possible.clear();
        for(unsigned long i{}; i < 2000; ++i)if(count[i])possible.emplace_back(i);
        fill(begin(count), end(count), 0UL);
    }};

    unsigned long ans{}, c{};
    while(K){
        ans += (reduce(begin(possible), end(possible)) & N & 1) << c;
        for(const auto i : possible)count[i / 2] ^= 1;
        reconstruct();
        if(K & 1){
            for(const auto i : possible)for(const auto a : A)count[i + a] ^= 1;
            reconstruct();
        }
        K /= 2;
        ++c;
    }

    cout << (ans + (reduce(begin(possible), end(possible), 0UL, bit_xor<>{}) << c)) / 2 << endl;

    return 0;
}

凍ってこまったこと

凍りました。こまりました。まとめます。

こまったこと

コンテスト後の感想戦

参加できません。 ARC後AGC後とか、解けなかった問題をどうやったら解けるのか知る経路がかなり断たれた気がするのでこまります。 しばらくするとレート下がりそう?

Twitterでのちょっとした質問

誰にも答えられないまま流れていく質問やお気持ちを見ていて、答えたいのになあと思うことがあります(ツイートは見られるので)。

たとえば、

これは B が A よりとても大きいと x = 1 を代入することになって、 B + A が B と等しくなるので(情報落ち) B + A - B が 0 になるからですね。

ほかには、

この話題については、構築と同様に「ACになる保証がある出力の一例」が書いてある、という説明がいちばんしっくりきています(受け売りですけど)。 たとえば ABC279-D の入出力例 3 では、真の値 2924017738100+\dfrac{10 ^ {18}}{\sqrt{29240177382}} を小数点以下 10 桁まで表示すると 8772053214538.5981965204 になって、double で最も近い値は 8772053214538.5986328125 なんですけど、どっちが表示されているとうれしいんでしょう 後者だと「サンプルと一致してうれしいな」となるのでしょうか(お気持ち表明) 誤差についての説明があると(「絶対誤差もしくは相対誤差が 10^{-6} 以下のとき正解となるので〜」みたいな)わかってもらいやすいんでしょうか。

感想戦の話にも通じるんですけど、こういうのに言及できないとかなしくなったりします。

こまらなかったこと

見たい人のツイートを見る

公開アカウントのツイートは見えます。 気になる人のツイートをその人のページまで行って眺めたり、単語で検索することについては前とそんなに変わりません。 filter:follows が動かないのは悲しいです。

おしまい

ぽか。🐦🐦🧊🧊

ぽかいこの AtCoder ユーザ解説まとめ

これはなに

rsk0315.hatenablog.com

これです。

まとめ

My Editorial - AtCoder

こうです。

あれ?

今年の6月に既出らしいです。

本編

  • Xor Distances
    • 初ユーザ解説です。この問題はユーザ解説も多く、この内容はほとんど公式解説のマイナーチェンジと言っていいかも?
    • 発想のでどころは違うので、これを読むことで腑に落ちの人もいるかもしれません。
  • kcal
    • 浮動小数点数演算のお気持ち表明解説第一弾です。
    • 浮動小数点数の演算にかかる誤差は(変なことをしなければ)十分納得のいく範疇に収まってくれます。
    • みなさんも浮動小数点数の誤差を飼い慣らしましょう(わたしは飼い慣らせていますか?いいえ)
    • 「想定解答との絶対誤差または相対誤差」、なくなるといいなあと思っています。
  • Colorful Candies 2
    • Polynomial Taylor Shift の紹介/解説 がメインのユーザ解説です。
    • 形式的べき級数のちょっと非自明な操作シリーズの中では比較的シンプルな畳み込みなので簡単な部類になるかもしれません。その分使える範囲は広くないかも?
  • Grand Garden
    • 昔の問題のユーザ解説シリーズ?公式解説より計算量がいいシリーズでもあります。
    • 今のABCで出たらこっちの制約になりそうじゃないですか?ならないかも
  • 1111gal password
    • 公式解説より計算量がいいシリーズのユーザ解説です。
    • ある種のDPは行列累乗で解けるというの、出てくる概念のわりに実感としてはとても有名な話(初心者向けにもフィボナッチ数の話ができるから?)
    • 現実のほうでサークルの人からせっつかれて書いた解説なので、おてほん実装(?)なしの簡易解説になっています。
  • Range Count Query
    • 列を乗せるセグメント木の紹介?解説?をしたユーザ解説です。今回は実際に乗せているのは多重集合でした。
    • コンテスト中には公式解説の方針は一切思い浮かんでおらず、「いまどきの AtCoder でもセグ木を自分で実装する回があるんですねえ」なんて思っていました。
    • 公式解説より計算量がわるいシリーズ(!)でもあります。
  • Polynomial division
    • 公式解説より実装が楽になるシリーズです。多倍長整数を使います。
    • 係数と比べて絶対値が十分大きい整数  x を代入することで多項式と対応させることができます。
    • 多項式環 K\lbrack x\rbrack から K への準同型と思う見方もあるかもしれません。だから、aab から b を復元する操作も簡単にできるんですね。
    • わたしが以前聞いたちょっとしたパズルを紹介しておきます。
  • Together Square
    • 公式解説より計算量がいいシリーズのユーザ解説です。
    • 速いのに導出は平易でよくないですか?気に入っています。
    • 書いた瞬間あたりは最速だったんですけど、そのあとすぐに atcoder.jp\tilde{O}(N^{5/11}) について言及されてしまいました ぽか。
  • 連結成分
    • noshi91.hatenablog.com の紹介/解説がメインのユーザ解説です。
    • 公式解説とどちらが速いかはケースによる感じだと思います。公式解説は出力が線形で、こちらはマージが高速です。
  • 方程式
    • PAST ユーザ解説シリーズです。
    • 微分可能なのでニュートン法が使えます。ニュートン砲!
    • 誤差評価についてはなあなあです。助力を求めているところもあります?
  • Erase and Rotate
    • 公式解説の行間を埋めるシリーズのユーザ解説です。
    • 公式解説で「区間の左端・右端の位置に単調性があるためスライド最小値を用いて O(N) に改善できます。」とあるところを埋めてみました。
    • おてほん実装(?)のコーディングスタイルや癖がぽかいこスタイルになっています。これ以前はなるべく癖を消すようにしていましたけど、どちらがお好みですか?
  • Intersection 2
    • 浮動小数点数演算のお気持ち表明解説第二弾です。長いです。巨大です。執念です。キモいです。
    • 全部読んだ方、いらっしゃいますか?感想をお待ちしています。
    • 誤差の理論的な評価、ひたすらひたすら格闘して出てくる結果が「じゅうぶん小さいです。」なので、疲れるかも 誰もしていない理由がわかった気がします?そもそもできる人がそこまで多くないですか?
    • 「想定解答との絶対誤差または相対誤差」は、なくなるといいなあと思っています。

公式解説より速かったり、実装が楽だったりすると書きがちです? 浮動小数点数などおきもちが出てくるとにゅっと書いてしまうこともあるかもしれません。

おしまい

みなさんはユーザ解説書いてますか? 🌿←これはユーカリ 私は書いてません みなさん書きましょう(?)

誤差ジャッジ問題のw/t作業をするときのためのTIPS

お気持ち表明です(?)

これはなに

浮動小数点数を扱うのは人間には難しいことが知られていますが、競技プログラミング界では浮動小数点数が必要になる(ほんとうに?)問題が頻繁に出題されています。
みなさんも writer/tester 作業をしたくなるときがありますね。たまには実数で出力をさせる問題が書きたくなるときがありますね(ありますか?)。具体的にはこんなときですね。

  • 幾何などで、答えが有理数になりそうにない
    • \pi有理数がかかった形式では \pi で割っておいたり、整数の平方根になる場合は二乗して出力させたり、atcoder.jp のような感じにして有理数にエイヤとしてしまえる場合もあります
  • 答えが有理数になりそうだけど、分母が非常に大きな素因数を持つ場合がある(し、「この素因数は持たない」といえるちょうどいい大きさの素数がない)
  • 答えが有理数になりそうだけど、想定解が二分探索などなのでexactな値を求めさせるのは難しい/できるけど心苦しい
  • なんか mod とって無理矢理整数にするのは嫌

浮動小数点数を扱うのは人間には難しいですが、こういう場合に備えてやっていけることはあります。まとめてみました。

問題文編

実数で出力をする問題では、基本的に正しい値を出力することはできません

非常に単純な問題設定と非常に単純なコードの組み合わせではじめて正しい答えが出ます。たとえば、

atcoder.jp

に対して、

#include <iostream>

int main(){
  int A, B;
  std::cin >> A >> B;
  std::cout << A * B / 100.0 << std::endl;
  return 0;
}

は正しい答えを出力します。しかし、

#include <iostream>
#include <iomanip>

int main(){
  int A, B;
  std::cin >> A >> B;
  std::cout << std::fixed << std::setprecision(17);
  std::cout << A * B / 100.0 << std::endl;
  return 0;
}

こうすると正しい答えを出力しなくなりますね。

誤差が出てしまう例

もうちょっと複雑な問題になると「正しい値」が10進では無限小数になってしまうこともあり、正しい答えを出力させるのが不可能になることもあります。

このため、実数で答えを出力する問題では問題文に出力に許容する誤差について明記することが慣習となっています1

「ただし、正しい値からの相対誤差もしくは絶対誤差が10^{-6}以下のとき、正解と判定されます。」

誤差ジャッジ問にはよくこういう文章が 出力 の項に書かれています。基本的に、問題文を書く場合はこのような文を書いておけばいいです(誤差の具体的な値は問題によって変えたほうがいいです)。

「正しい値からの〜のとき、正解と判定されます。」

正しい値を計算するのが難しいことは上で話したので、「じゃあこのジャッジをするのは無理じゃん」となるかもしれません。 実はそうではなくて、これは「〜のとき、正解と判定されます。」の解釈の問題で「そうでないとき100%落とします」とは言っていないことが大事です。 なので、(理想的には) writer/tester は「正しい値と想定解との誤差」を上から評価して、正しい値からの誤差が hoge のとき を完全に含むような想定解との許容誤差を考え、設定することになります。

これを「想定解からの誤差が...」としてしまうとコンテスタントは「writerの想定解が(入力とは関係なく)5000兆を出力するもので、これまで出たそれっぽいACは全て気まぐれで出たACである」のような可能性を排除できませんが、なぜかよく使われている表現です2

「相対誤差もしくは絶対誤差」

なぜ「相対誤差」と「絶対誤差」のどちらか片方にしないのか?は、浮動小数点数の表現の仕方に理由があります。 ここでは深入りはしませんが(「浮動小数点数」とか「IEEE 754」とかで調べたらすぐ出てきます)、だいたい「大きいところはざっくり、小さいところは細かく、すごく小さいところはほどほどに」という感じです。 なので、大きい値のところは目が粗いので絶対誤差が大きく、すごく小さいところはほどほどなので相対誤差が大きくなってしまいます。

例えば、double で表現できる正の値のうち、小さい方から2つと大きい方から2つ取ってきて相対誤差と絶対誤差を見てみましょう。

小さいほう2つは、こちらです。

0.000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004940656458412465441765687928682213723650598026143247644255856825006755072702087518652998363616359923797965646954457177309266567103559397963987747960107818781263007131903114045278458171678489821036887186360569987307230500063874091535649843873124733972731696151400317153853980741262385655911710266585566867681870395603106249319452715914924553293054565444011274801297099995419319894090804165633245247571478690147267801593552386115501348035264934720193790268107107491703332226844753335720832431936092382893458368060106011506169809753078342277318329247904982524730776375927247874656084778203734469699533647017972677717585125660551199131504891101451037862738167250955837389733598993664809941164205702637090279242767544565229087538682506419718265533447265625
0.00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000988131291682493088353137585736442744730119605228649528851171365001351014540417503730599672723271984759593129390891435461853313420711879592797549592021563756252601426380622809055691634335697964207377437272113997461446100012774818307129968774624946794546339230280063430770796148252477131182342053317113373536374079120621249863890543182984910658610913088802254960259419999083863978818160833126649049514295738029453560318710477223100269607052986944038758053621421498340666445368950667144166486387218476578691673612021202301233961950615668455463665849580996504946155275185449574931216955640746893939906729403594535543517025132110239826300978220290207572547633450191167477946719798732961988232841140527418055848553508913045817507736501283943653106689453125

ちょうど倍なので、相対誤差は 1 、絶対誤差は 4.94\times 10^{-324} です。

大きいほう2つは、こちらです。

179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368
179769313486231550856124328384506240234343437157459335924404872448581845754556114388470639943126220321960804027157371570809852884964511743044087662767600909594331927728237078876188760579532563768698654064825262115771015791463983014857704008123419459386245141723703148097529108423358883457665451722744025579520

相対誤差は 1.11\times 10^{-16} 、絶対誤差は 2.00\times 10^{292} です。

これははしっこのほうを取ってきているので誇張されたといえる例ですが、値が非常に狭い範囲を取るなどでなければ「相対誤差もしくは絶対誤差」のような形にするといいと思います。

atcoder.jp

これは出力がすべて \lbrack0,3\rbrack に収まるので絶対誤差だけでジャッジしている例です。

10^{-6}

上の例でも相対誤差/絶対誤差のどちらかは 10^{-15} より小さかったように、「真の値に最も近い、double で表現できる値」を取ってくることができれば、その相対誤差もしくは絶対誤差はだいたい 10^{-16} 程度に収まります。 なのに許容誤差を 10^{-6} などにするのはなぜかというと、浮動小数点数を用いた四則演算などでは(ほぼ)毎回誤差が生じるためです。

計算が増えれば増えるほど誤差の絶対値の期待値は増えていきがちです。 「この問題は誤差が大きくなりそう...」といった直感をはたらかせてほどよい許容誤差を設定しましょう。精進あるのみ? 理想的には、IEEE 754 の倍精度浮動小数点数で想定解の入力/計算/出力を行ったときに発生する誤差を上から評価し、それを含むような許容誤差について考えるべきです。が、難しいのでここはとりあえずなあなあでもいいです(浮動小数点数は人類が扱うには難しすぎるので)。

個人的に、テストケースにおける想定解と真の値との誤差は問題文で設定されている許容誤差より 2-3 桁小さいことが多い印象があります。 誤差をランダムウォークだと思うと、プログラム中で N\varepsilon の誤差が生まれる足し算を行うと誤差の絶対値の期待値は  \varepsilon\sqrt N なのに対し、理論的に抑えられるのは N\varepsilon までなのでそんな感じかもしれません。 引き算などで桁落ちしたりすると計算結果の誤差はさらに大きくなりえますし、実装の方針でも誤差の出方は変わるのでゆるめに取ることを心がけるほうがいいかもしれません。

想定解編

想定解を書くまではとくに気をつけることはありません。好きに問題を作って想定解を書きましょう。 普通に誤差ジャッジ問を解くときに気をつけるべきことは妥当に気をつけておきましょう。

あと、問題設定をあまり変えずに誤差にやさしくする方法があればそのようにするほうがうれしいことがあります。 「最適解より最適値を求めさせる」とかは実行しやすい気がします。

writer作業編

writer が残りやっておく作業は

  • テストケースを作る
  • ジャッジを書く
  • 証明を書く

とかだと思います。

テストケースを作る

想定解では提出できる長さ/速さのコードを書くことになりますが、テストケースを作るときは多少時間がかかってもいいので誤差が出にくそうな方針を取ったり、途中まで有理数a+b\sqrt c のような形の数を計算するライブラリを使って exact な値を保持したり、区間演算ライブラリなどを使って丁寧丁寧丁寧に真の値を評価するようにするとよいです。 想定解の誤差がちゃんと評価できている場合は想定解をそのまま利用してもいいです。 この段階で真の値の評価がかなり難しかったりする場合は、問題をそもそも成立させられない(現実的な計算資源で正しい値に近い値を出すことが難しい)可能性を考えておいたほうがいいかもしれません。

ジャッジを書く

testlib.h とかが有名で、誤差ジャッジも入っています。 誤差ジャッジに設定する許容誤差の値は問題文に書いたそれではなく、想定解から許容範囲を全部含むようにした許容誤差の値に設定しておきましょう。 ジャッジ側で気をつけることは、ジャッジが出力を受け取るところや、許容誤差の境界を計算するところにも誤差が入る可能性があるところです。testlib.h はこのあたりも吸収してくれたはず?です。 不安ならここでさらに許容誤差をドンと倍にしておくといいです。これで変なACが出るようになることはめったにないので基本困りません。

証明を書く

証明を書きましょう。誤差が一定以内に収まることを証明するのは難しいです。頑張りましょう。無理だったら強いtesterさんを呼びましょう。それでも無理だったら、とりあえず許容誤差をもうちょっとゆるくしておきましょう。場合によっては正当性がないかもしれないと思いながら問題を出すことになります。

tester作業編

ここまでの作業に嘘が混入していないか確かめましょう。

丁寧丁寧丁寧に真の値を評価して、許容誤差ギリギリを攻めてみるのも仕事のひとつです。 許容誤差の内側であることが証明できる値を入れて落ちたら、正しくないです。 外側であることが証明できる値を入れてACになってもジャッジに問題はありません。

許容誤差ギリギリを攻めて落ちる落ちないをしたところで防げる不幸せはあんまりないと思います(たとえば、「高橋君ボール1号」に sample 1 における f(t)100.00000099999999999999999999999 程度になる 100.000000241453007005241418874476を提出すると全ケースWAになりますが、ここの非厳密さで不幸せになったひと、ほとんどいなさそうです)が、問題文に嘘を書かないことがしたくないですか?したくない場合、にゃん。

道具編

知っておくと便利かもしれません。

  • testlib.h
    • writer/tester の強い味方。わりといろいろ対応してくれているので、ジャッジに使うと便利です。
  • boost/multiprecision/cpp_dec_float.hpp
    • boost の多倍長浮動小数点数型。AtCoder や yukicoder などのコンテストサイトでも使えてうれしい。有効桁数は好きに増やせますが、「真の値」について正しい評価を行うのは難しいので注意しましょう。see also : Rump の例題
  • boost/rational.hpp
    • boost の有理数型。boost::multiprecision::cpp_int と合わせて使うともっと強力に。AtCoder や yukicoder などのコンテストサイトでも使えてうれしい。exact な値を計算できるので、結果が有理数にならなくても途中まで有理数で計算したりするのが有効な場合もあります。
  • kv verifiedby.me
    • C++区間演算を行えるライブラリ。入出力も適切に処理してくれて、真の値を評価するのにとても便利。オンラインジャッジには入っていないがちなので、手元でテストケースを作ったり、無理矢理ヘッダファイルをコピペして提出することになりがち(?)

おしまい

浮動小数点数を扱うのは難しいので \mathbb{F}_p の問題を作るほうが楽なことがわかりました! いかがでしたか?

数え上げ 𝓛𝓞𝓥𝓔...


  1. ところで、整数で出力する問題で、30 と出力すべきところに 30.0 とか 3e1 とか出力しても正しい値ではあります。このあたりは...うごごご 実数出力の問題はここに目を向けざるを得ないということなのかも?
  2. なんならこのネタでエイプリルフールコンあたりに1問提供しようかなと思っているくらい腑に落ちてない文なんですけど、慣習ってこわくないですか?

凸関数に囲まれた領域の中の格子点の凸包の頂点を列挙する一般的なテク

テク名が長すぎませんか?

これはなに

https://min-25.hatenablog.com/entry/2018/05/03/145505min-25.hatenablog.com

を書いてみましたエッセイです(??) 上の記事を読んでいなくても話がわかるように書きます

モチベーション

例えば、次のような問題を考えてみましょう。

問題

自然数 $x$ について、$x$ の正の約数の個数を $\sigma(x)$ と書くことにします。
自然数 $N,M(1\leq N\leq M)$が与えられるので、次の値を求めてください。

$\displaystyle \sum_{i=N}^M\sigma(i)$

この問題は、$f(X)=\sum_{i=1}^X\sigma(i)$ に対して $f(M)-f(N-1)$ を求めることで計算でき、 $f(X)$ は(主客転倒と呼ばれがちなテクニックを用いて)次のように書けます。

$\displaystyle f(X)=\sum_{d=1}^X \left\lfloor\dfrac{X}{d}\right\rfloor$

これは、凸な領域 $S_X:=\{(x,y)\in\mathbb{R}^2|0\lt x\leq X,0\lt y\leq X,xy>X\}$ に含まれる格子点 $S_X\cap\mathbb{Z}^2$ の要素数を数えられれば解けます。

この格子点はちょうどよく凸っぽい性質を残しているので、もしこの格子点たちの凸包を生成する点だけがすべてわかっているなら、$O( (\text{凸包の頂点数}) )$ や $O((\text{凸包の頂点数})\log X)$ くらいの時間で点の個数を数え上げることができます(このパートは下の問題とほぼ同じです)。

atcoder.jp

$f(X)$ は普通に計算すると $\Theta(X)$ や $\Theta(\sqrt{X})$ 時間がちですが、凸包の頂点数はそれより小さくて $O(\sqrt[3]{X}\log X)$ 程度な1ので、真に高速です。

速いとうれしいので、やっていきましょう

凸集合の中の格子点の凸包の頂点の性質

まず、凸包について考えれば十分であることを示します。

$C\subseteq\mathbb{R}^2$ が凸集合であるとします。

$C$ 内の格子点全体 $C_\mathbb{Z}=C\cap\mathbb{Z}^2$ が「凸っぽい」性質をもっていること、つまり「$C_\mathbb{Z}$ は、その凸包に含まれる格子点全体である」ことを示します。

$C_\mathbb{Z}$ の要素が凸包に含まれないことはないので、凸包に含まれる格子点であって $C_\mathbb{Z}$ の要素でないものが存在しないことをいいます。

$C_\mathbb{Z}$ の凸包は $C_\mathbb{Z}$ を含む凸集合全体の共通部分です。 $C$ は $C_\mathbb{Z}$ を含む凸集合なので、$C$ は $C_\mathbb{Z}$ の凸包を完全に含み、凸包に含まれる格子点は $C$ に含まれます。 示されました。

なので、$C$ に含まれる格子点について知りたい場合は $C$ 内の格子点全体の凸包について知れば十分であることがわかりました。 逆に、$C_\mathbb{Z}$ の凸包について知りたいとき、$C$ についての情報があれば大丈夫そうということもわかりました。

本編

凸集合 $C$ が与えられたとき、$C_\mathbb{Z}=C\cap\mathbb{Z}^2$ の凸包の頂点を列挙することを考えます。

ここでは、「いちばん上」から「いちばん右」へ向かう部分のみについて考え、$(0,y)$ から $(x,0)$ まで移動していくことを考えます。 適宜平行移動したり反転したりしておいてください。

主に次の2つのパートからなります。

  1. 今の傾きのまま外に出ないギリギリまで進む
  2. 次の傾きを計算する

1は簡単(にぶたんで $O(\log X)$ くらい、閉じた式で書けるならそれを計算するのにかかる時間くらい)なので、2について詳しく書きます

今いる点を $(x,y)$ として、$(x+a,y-b)\in C,(x+c,y-d)\notin C,bc-ad=1$ なる $a,b,c,d$ がわかっているとします。 今回の状況では例えば $(a,b,c,d)=(0,1,1,0)$ が常にこれを(最後に点が $(x,0)$ に到達したとき以外)満たします。 これは Stern-Brocot Tree の部分木を探索することに対応しています。

求めるもの(次の傾き)は、$(x+\alpha,y-\beta)\in C$ を満たす互いに素な非負整数の組 $(\alpha,\beta)$ であって $\dfrac{\alpha}{\beta}$ が最大となるもの2です。

$(a,b)$ が求めるものである場合と、そうでない場合があります。 $(a,b)$ が求めるものであるとは、傾きが $(a,b)$ と $(c,d)$ の間にあるような格子点はすべて $C$ に含まれないということです。

これから先に見ることになる格子点 $(x+\alpha,y-\beta)$ は、すべて $\dfrac{a}{b}\lt\dfrac{\alpha}{\beta}\lt\dfrac{c}{d}$ を満たします。 このような格子点の存在する範囲を厳しめに評価することで、$C$ に含まれるものが存在しないことを確かめられます。

実際にそのような格子点が存在しない場合。このとき、$(a,b)$ が求める次の傾きになる。

Stern-Brocot Tree の性質から、$\dfrac{a}{b}$ と $\dfrac{c}{d}$ の間にある既約分数はすべてある互いに素な正整数 $(n_1,n_2)$ に対して $\dfrac{n_1a+n_2c}{n_1b+n_2d}$ と書けます。 なので、これから先に見る格子点はすべて $(x+a+c+\alpha,y-b-d+\beta)\ (\alpha=n_1^\prime a+n_2^\prime c,\beta=n_1^\prime b+n_2^\prime d,0\leq n_1^\prime,n_2^\prime)$ の形で書けることがわかります。

よって、$(x+(t+1)a+c,y-(t+1)b-d)\ (0\leq t)$ が $C$ と共通点を持たないとき、$(a,b)$ が求める次の傾きです。

そうでないときは、Stern-Brocot Tree の子に動けばいいです。 $(x+a+c,y-b-d)$ が $C$ に含まれるかどうかで場合分けを行います。

この繰り返しは、求める傾きの Stern-Brocot Tree における深さの回数まで行われます(例えば、次向かうべき向きが $(13,5)$ なら下の図のように辿り、$(a,b,c,d)=(0,1,1,0)\to(1,1,1,0)\to(2,1,1,0)\to(2,1,3,1)\to(5,2,3,1)\to(5,2,8,3)\to(13,5,8,3)$ もしくは $(5,2,13,5)$ のように7回の繰り返しを行います)。

出てくる傾きの Stern-Brocot Tree における深さの最大値は $x^2+y^2\leq n$ では $\Omega(\sqrt[4]{n})$ 、$xy\geq n$ では $\Omega(n)$ となります。

これらの場合でも Stern-Brocot Tree の同じ向きの遷移をまとめることで遷移の回数が $O(\log n)$ で抑えられるようになり、いい感じです。

これを繰り返して探索をしていきます。 一度探索をした後の次の探索で求める傾きはさっき求めた傾きと近いことが予想されるので、Stern-Brocot Tree に潜ってきた道を残しておくことで高速化になりそうです(ここの計算量の証明をしていません 毎回一から探索しても $O(\log n)$ 倍になるだけなのできっと大丈夫です)。

実装方針/作例

ざっくり次の3つの機能があれば動くことがわかります。

  1. 点 $(x,y)$ が領域の中か外かを判定する
  2. 点 $(x,y)$ から $(a,-b)$ 方向($(x+ta,y-tb)$ です)に伸びる半直線と境界とが交わるか判定する
  3. 点 $(x,y)$ から $(a,-b)$ 方向に伸びる半直線と境界とが交わることがわかっているとき、境界上にある $(x+ta,y-tb)$ のうちもっとも小さい $t\geq0$ を求める

これらの関数 is_insideis_crossnext_in/next_out (始点 $(x,y)$ が領域の中にあるかで関数を分けたほうが気持ちがよかったのでそうしています $t$ の丸め方の方向も違うので) を使って、一回分のロジックはこんな風に書くことができます。 now_point には今見ている点(凸包の頂点)、stern_brocot_tree には探索の履歴、result には今まで見つかった凸包の頂点が入っています。

auto& [x, y]{now_point};

// subtree between a/b and c/d
auto [a, b, c, d]{stern_brocot_tree.back()};
stern_brocot_tree.pop_back();

// next vertex of convex hull
const auto n_upper{next_out(x, y, a, b, N)};
x += n_upper * a;
y -= n_upper * b;
result.emplace_back(now_point);

// backtrack
while(!empty(stern_brocot_tree)){
    tie(a, b, c, d) = stern_brocot_tree.back();
    if(is_inside(x + a, y - b, N))break;
    stern_brocot_tree.pop_back();
}

if(empty(stern_brocot_tree))break;

// search forward
while(y >= d){
    if(y >= b + d && is_inside(x + a + c, y - b - d, N)){
        const auto n_lower{next_out(x + a + c, y - b - d, c, d, N)};
        tie(a, b, c, d) = make_tuple(a + c * (n_lower + 1), b + d * (n_lower + 1), a + c * (n_lower + 2), b + d * (n_lower + 2));
    }else if(is_cross(x + c, y - d, a, b, N)){
        const auto n_lower{next_in(x + c, y - d, a, b, N)};
        if(y < b * n_lower + d || !is_inside(x + a * n_lower + c, y - b * n_lower - d, N))break;
        tie(a, b, c, d) = make_tuple(a * n_lower + c, b * n_lower + d, a * (n_lower - 1) + c, b * (n_lower - 1) + d);
    }else break;
    stern_brocot_tree.emplace_back(a, b, c, d);
}

subtree を特定するのには $\dfrac{a+c}{b+d}$ があれば十分だと思うんですけど、$\dfrac{a+c}{b+d}$ から $\dfrac{a}{b},\dfrac{c}{d}$ を復元する高速な方法がわからなかったのでそのままにしています 情報求ム

あとは、これに合うように初期値と各関数を定めていけばいいです。

例題

問題

自然数 $N$が与えられるので、$x^2+y^2=N$ を満たす自然数の組 $(x,y)$ をすべて求めてください。

求める自然数の組 $(x,y)$ は必ず $x^2+y^2\leq N$ の中の格子点全体の凸包の頂点になっています(背理法で示せます)。 なので、凸包の頂点のみを見れば十分で、今回のテクが使えます。

考えるべき領域が四分円 $x^2+y^2\leq N,x\geq0,y\geq0$ なので、is_insideis_crossnext_in/next_out は次のような判定をすればいいです。

  1. $x^2+y^2\leq N\wedge x\geq0\wedge y\geq0$
  2. $\dfrac{|ax-by|^2}{a^2+b^2}\leq N$
  3. $\dfrac{by-ax\pm\sqrt{(a^2+b^2)N-(ay+bx)^2}}{a^2+b^2}$

大きな $n$ について計算するとき、オーバーフローに気をつけてください。

点の初期値は $(x,y)=(0,\left\lfloor\sqrt{N}\right\rfloor)$ 、stern_brocot_tree には Stern-Brocot Tree 全体にあたる $(0,1,1,0)$ などを入れておけばいいです。

初期値を $(x,y)=\left(\sqrt{\dfrac{N}{2}},\sqrt{\dfrac{N}{2}}\right)$ 付近にして stern_brocot_tree を $(0,1,1,1)$ から始めると2倍の高速化になります。

これを実装すると、次のようになります。

gist.github.com

だいたい $2^{64}$ くらいの数 $18446744072107595890$ を入れると 380 ms くらいかかって結果が返ってきます。

$N=18446744072107595890$ の場合 全部で64通りあります

上で述べた定数倍高速化をするとしっかり2倍で 190 ms くらいになります。

シンプルな $\Theta(\sqrt{N})$ を書くとコードテストではタイムアウト、手元では2分ほどかけて答えを出力しました。

はやくなってうれしいね 愚直を書くのが上手な人にコードテスト上で通されると困った気持ちにはなります

例題 2

問題

自然数 $x$ について、$x$ の正の約数の個数を $\sigma(x)$ と書くことにします。
自然数 $N,M(1\leq N\leq M)$が与えられるので、次の値を求めてください。

$\displaystyle \sum_{i=N}^M\sigma(i)$

一番最初に出した問題です

$f(X)=\sum_{i=1}^X\sigma(i)$ を求めたいとき、考える領域は $xy\gt X,x\gt0,y\gt0$ です。 今は格子点だけ注目しているので $xy\geq X+1,x\gt0,y\gt0$ と書き換えていいです。 $X+1$ を改めて $N$ とすることにしましょう。

これは下に凸な関数なのでちょっとだけ変えないとうまく回りませんが、ちょっと変えるだけでうまく回ります。 具体的には、部分木を表す $(a,b,c,d)$ を $(c,d,a,b)$ のように保管するようにするとほぼ同じコードでうまく動くようになります。

初期点を $(1,N)$、stern_brocot_tree を $(1,0,0,1)$ から始めることでうまくやっていくことができます。

is_insideis_crossnext_in/next_out は次のような判定をすればいいです。

  1. $xy\geq N\wedge x\geq0\wedge y\geq0$
  2. $ab=0$ のとき $bx-ay\gt0$、$ab\neq0$ のとき$(bx+ay)^2\leq 4abN$
  3. $a=0$ のとき $\dfrac{xy-N}{bx}$、$b=0$ のとき $\dfrac{N-xy}{ay}$、$ab\neq0$ のとき $\dfrac{ay-bx\pm\sqrt{(ay+bx)^2-4abN}}{2ab}$

こちらも大きい $N$ で計算する場合はオーバーフローに気をつけましょう。

こっちにも対称性があるので、$\left(\sqrt{N},\sqrt{N}\right)$ あたりから始めることで定数倍が高速になります。

実装は次のようになります。

gist.github.com

判定パートをオーバーフローさせない方法が思いつかなかったので unsigned long__uint128_t 固定になっています。

また、凸包を構築する途中に「注目している辺の上にいくつ格子点があるか」がわかっているので、構築しながら答えも求めると $\gcd$ 一回分の計算量が浮いてお得です。

答えを unsigned long で計算しているので適当な $N,M$ を放り込むとオーバーフローすると思います。←??

1つめの例題ほど速くはなくて、$2^{64}$ くらいの数を入れるとコードテストで間に合わなかったりします(数が大きいところでバグっているのかもしれません(??))。 だいたい $10^{18}$ 前後の数 $N=662891320790343374,M=1808274646968721915$ を入れると 3秒ほどで答えが出ます。

$N=662891320790343374,M=180827464696872915$ の場合

__uint128_t などを経由していて定数倍も見るからに重そうで、凸包の頂点数の評価もひとつめの $O(\sqrt\[3\${n})] から $O(\sqrt\[3\${n}\log n)] になっていて、実際重いです。

これも定数倍高速化はきっちり 2 倍速になってだいたい 1.5 秒で答えが出ます。

例題 3

問題

自然数 $N$が与えられるので、素因数分解してください。

例題 2 の領域で例題 1 みたいなことをすればいいです。

いかがでしたか?

🦑←これはイカ

オーバーフロー回避策、高速化テクなどあればどしどしお寄せください 宛先はありません


  1. https://www.ams.org/journals/tran/1963-106-02/S0002-9947-1963-0143105-7/S0002-9947-1963-0143105-7.pdf の主定理を $n=2$ 次元の場合について使うと面積が $V$ の狭義凸集合の境界に格子点が $N$ 点存在するとき $N=O(\sqrt[3]{V})$ が言えるので、全体をうまく$O(\log X)$ 個の部分に分割して $O(\sqrt[3]{X}\log X)$ が示せます(thanks to Min_25 さん)
  2. ただし、$\beta=0$ のとき、そうでないどのような組よりも大きいとします

PAST ばちゃをしました

典型力のNASA of NASA
ところできょうびNASA of NASAって聞かなくなりましたね

これはなに

2019/12/30の14:45から第一回PASTのバーチャル参加をしました 88点でした 評価はなにになりますか 使った方針を述べていきます

A

C++には std::any_of といういい関数があります any of 入力が英小文字ならerrorと出力して終わります そうでないなら整数にして2倍にしておわりです

B

C++にはfor文といういい言語機能があります 直前のAと今のAを持ちながらやっていきます

C

C++には std::nth_element といういい関数があります おわりです

D

ヒストグラムを作ります C++には std::all_of といういい関数があります all of ヒストグラムが1ならCorrectと出力して終わります そうでないなら2であるようなindexと0であるようなindexを出力して終わります ここはよく考えると find といういい関数が使えますね

E

急に難しくなってませんか C++には std::bitset といういいデータ構造があります bitsetには operator|= といういい操作が実装されています おわりです

F

std::vector<std::string> にまとめて std::sort をします 文字列の文字cについて .back().push_back(c | 32) をして、偶数回目のnon-zeroな c & 32 が来たら .emplace_back() をします sort しておわりです 日本語が細切れすぎますね、競プロ界のルー大柴

G

全通りは310=59049通りです 各状態についてコストの計算は45回くらいの足し算で抑えられるので全体で2.6*106回くらいです 全通りを高速に列挙するのは

for(unsigned long i{0}; i < 1UL << N; ++i)
  for(unsigned long j{i}; j < 1UL << N; ++j |= i)

によってO(3N)です おわりです

H

これなに むずかしいです

(奇数枚目/偶数枚目)の(最小在庫/累計売上)の4つを持ちます 売りながら更新します おわりです

I

dpをします おわりです

J

全点間最短距離が求められたとします 私は求められませんでしたけど 本当に黄色ですか?

このとき答えはdist[最初][v]+dist[中間地点][v]+dist[ゴール][v]の最小値になります vを全探索しても間に合いますね おわりです たぶん

K

私は詳しいんです std::bitset で上司の情報を持ちながら全部探索したら間に合... [MLE]

仕方がないのでLCAを書きます 今回は初めてオイラーツアー+RMQによるLCAを書きました

rootからのBFS行きがけ順で頂点にindexを振り直します DFSをしてオイラーツアーと各頂点が出てくるところを持ちます RMQをします 楽をしたかったのでSegment Treeを書きました おわりです

L

使う小さい塔の集合32通りを定めると(使うので)最小全域木を書くだけでいいと思います ところで解けていませんけど 本当に黄色ですか?

M

お ま か せ 二番煎じですか? ごめんなさい...

実家のような二分探索です 誰か有志コンで有理数でこれ求めさせる問題出しませんか 喜んで解く場合とそうでない場合があります

判定問題「強さをx以上にできますか?」を考えます 単体で強さx+eのもののe×m(質量)の値でソートすると大きい方から足すのが最適になりますね お助けも同様にやります おわりです

N

開けたい区間[x, x+C]をx=0からずずずと動かしていくことを考えます すると各区間がx=いくらのときに 取り除くべきになる/取り除かなくてもよくなる のかがわかります ソートして処理していってコスト最小がこたえです

O

座圧してDPしていきます i番目に大きいものの最良期待値の計算とi+1番目に大きいものの最良期待値の計算ではi番目に大きい数が書いてあるサイコロを振った時の期待値しか変わらないのでそれだけ再計算します おわりです

おわりに

むずかしいですね グラフ理論は書かない期間が続くとグッと書けなくなるのでこれを読んでいる皆さんはぜひ私のようにならないようにしてくださいね おわりです

クリスマスコンテスト2019 write up

ぱちぱちぱち...(maimaiのボタンを押す音)
ふぅ...
え、今日ってクリスマスコンなんですか(イブにあるとは思っていなかった顔)

これはなに

Xmas Contest 2019にチーム「TSG」で参加して、全体27位でした そのwrite upです ちょっと解説です

atcoder.jp

ごはん

クリスマスコンがあると思っていなかったので18:50くらいからいそいでごはんをたべました おいしかったです 戻ってきたのは19:15

A

256x256の画像を8x8に分割した画像がシャッフルされた状態で与えられるので、復元します。

f:id:MMNMM_259:20191224233940p:plain

します。

f:id:MMNMM_259:20191224234000p:plain

します。

f:id:MMNMM_259:20191224234045p:plain

します。

f:id:MMNMM_259:20191224234109p:plain

しま... えっ これ 何

f:id:MMNMM_259:20191224234127p:plain

しました。

おわりです。

K

ゲームなので私が担当するしかないですね!!!!

f:id:MMNMM_259:20191224234636p:plain
チームSlackに噴出する欲求

当然不偏ゲームなのでgrundy数を考えますね この設定ではgrundy数は「根つき木の中で何番目に小さいか(0-origin)」ですね

さてそうすると困るんですけど、{1}は{0,0,0,...,0}=n (0がn個)より真に大きいです つまりgrundyの終域を自然数にしたままだと足りないんですね。

f:id:MMNMM_259:20191225000050p:plain
困り

なので...

f:id:MMNMM_259:20191225001542p:plain
これなに

順序数まで拡張するんですね いかなる自然数より大きい元であるところのωです ペアノの公理系のうちいかなる元の後続でもない元が0のみであるみたいな制約を取り去るとこんな感じの自然数もどきが作れますよね

順序数をとることを許すとこんな感じに計算が進んで、ぐっと睨むと計算式が出ますね

f:id:MMNMM_259:20191225003806p:plain
ウッ

さてgrundy数が出たはいいんですけど、これのxorってどうやって取ればいいんでしょう 実はωの次数が同じものどうしで係数をxorすればいいんですね すごいでしょ そうでもないですか? これをfactとして認めて考察を進めていきます(証明には石の数が順序数まであるNimをプレイしてみるといいと思います たぶん)

grundy数が等しい <=> 根つき木として同型 なのはそれはそうですね これを使うと結局全体のgrundy数が0というのは次の条件と同値になることがわかります

後手必勝と同値な条件

根つき木 \tau に対して、i\ \ (0\leq i\leq k) 番目の根のすぐ子にある \tau の個数を a_{\tau,i} としたとき、すべての \tau について a_{\tau,0}\;\mathrm{xor}\;a_{\tau,1}\;\mathrm{xor}\;\cdots\;\mathrm{xor}\;a_{\tau,k}=0

同じ次数どうしでxorされるので、それはそうですね

つまり、それぞれの根の直下にある部分木を列挙して同じものをまとめることができればこれが解けたことになります。 コンテスト中に「木 同型判定」でぐーぐる検索をすると

snuke.hatenablog.com

これが来ます(ところでサムネの画像が折り畳まれてるところの画像なんですけど いいんでしょうか) えっ writerさんのブログですけど 書きます テストケースが219個あってびっくりします 落ちます

hashをこねくりまわすと結果が変わるのでhashの衝突で落ちていることがわかります ちーん まあそれはそうですけどね

さて、みなさんは決定的な根つき木の同型判定を知っていますか?私は知りませんでした... と言いたいところなんですけど、私は知っているべき立場にいたんですよね みなさんはこの問題を知っていますか 私はwriter/tester陣だったんですけど当時これが解けませんでした これの想定解法とほとんど同じ方法で根つき木の同型判定ができます

www.hackerrank.com

基本的に根つき木をhashにするアイデアは同じです {}を0にします 子のhashをソートした列を考えます これを今までに出てきたものと比較して、違うなら(今までで最大のhash値)+1にします 同じならそれと同じhash値にします

おわりです(trieとかで高速になるんでしょうか わかりません)

あとは各頂点に対する部分木のhashがわかっているので上の問題をunordered_mapなどのデータ構造を使って高速に解くことができます。 FAでした。 やったあ!

まとめ

とけるとたのしい みなさんも得意分野をぜひひとつ ゲームが得意なだけだったらたぶん解けてないんですけどね 運がよかったです