AtCoder Beginner Contest 138 F - Coincidence
解法
考察も実装もそこそこ重い問題です。順に考えていきましょう。
記法について
剰余演算を記号 、XOR演算を記号 で表すことにします。
またこの記事では数え上げの対象である2数のペアを ではなく の順番で記載しています。
ビットだけの条件に置き換える
XORが絡む、制約が大きめ、与えられた下限/上限の間で条件を満たす値の数え上げ…ということで、帰着先として有力なのは2進数の桁DPです。しかしそのためには余りの条件をビットの条件に置き換えないといけません。
余りについての必要条件を導く時によく使うのは という性質です。この性質と を見比べると、 と の2進数での桁数は同じである必要があることが分かります。もし のほうが桁数が大きい場合、 に の最上位桁が残るので必ず となってしまうからです。
そしてこの「2進数での桁数が同じ」という条件から、 を で割った商は必ず1であることが分かります。もし が の2倍以上だとすると、必ず2進数で より大きい桁数になってしまうからです。このことから と置き換えることができます。 条件式は と変形できて、剰余演算を消すことができました。
さらにXORと足し算/引き算は近い性質を持っていて、 なる非負整数 に対して、2進数で繰り上がりがない足し算 や繰り下がりのない引き算 はXORと一致します。以前足し算とXORの比較をこの記事に書きましたが、同様の表を引き算についても書けば同じことが言えます。
そしてもし繰り下がりがあると引き算のほうがその分小さい値になってしまうので、逆に引き算とXORが一致するならば繰り下がりが一切ないということも言えます。
これでようやくビットの条件に言い換えられました。結局 となる必要条件は
- にビット単位での繰り下がりがない。つまり の各桁のビットの組み合わせは、 のいずれかである。
- 2進数での桁数が同じである。つまり前項目の3パターンの中で、上位桁から見て が登場する前に が登場することはない。
ということになります。また逆に上記の条件を2つとも満たすならば、実際に商が1になり引き算とXORが一致するので が成立します。これで十分条件であることも確かめられます。
※このように条件の言い換えテクニックを駆使すれば数式ベースで導くことができますが、思いつかない場合は小さい数で実験して、条件を満たす組み合わせの2進数表記を書き下してみるのが良いと思います。
桁DPを組む
ということで2進数での桁DPを組みます。 の範囲内で先ほどの条件を満たす組 を数えます。
2変数なので を別々に処理するのが難しく、両端を真面目に処理する必要があります。上限だけを考慮する場合は「上限 より小さいことが確定したかどうか」をフラグとして持つのが桁DPの常套手段ですが、今回は下限についても同様のフラグを持ちます。
さらに「上位桁から見て、ビットの組み合わせ が登場する前に が登場することはない」という条件も考慮すると、DPの状態は以下のような定義になります。
- = 桁目以上のビットの決め方であって、これまでに条件違反が確定するような遷移がなくて、各種フラグについて以下のような状態となっているような場合の数。
- が上限 よりも小さいことが確定している()/いない()
- が下限 よりも大きいことが確定している()/いない()
- これまでの桁でビットの組み合わせ が登場している()/いない()
桁DPの実装は人によって細かい差異がありますが、この記事では最下位ビットを 桁目として、上位ビットから順に決めていく方法を取ります。つまり初期状態を として、答えを の総和として求めます。
では遷移を組みましょう。気合いです。
まず についての条件は楽です。「 から遷移するときには のパターンを使わない」「 のパターンを使ったら にセットする」の2点を守ればよいです。
次に両端についての条件 です。ビットの組み合わせとして のどれを採用するかを決めれば、 の遷移は と のビットから、 の遷移は と のビットからそれぞれ求めることができます。
上限についての条件は桁DPでよく使いますね。図で描くとこうです。
下限についてはこう。似たような条件ですが色々と逆になっています。
×印で記したところは、これを採用してしまうと条件違反 や が確定してしまうところです。上限/下限のどちらか片方でも×印に遷移してしまう場合は遷移しないことにします。
これをDPの実装に落とし込むことができれば答えを求めることができます。気合いです。
ACコード
Submission #7000214 - AtCoder Beginner Contest 138
おまけ:実装テク
先ほどの画像の通りの遷移を条件分岐でゴリゴリ書けばいいのですが、if文だらけになるとつらいですね。
どちらも、遷移先の値は
- / の 桁目のビット
- 遷移前のフラグ / の値
- 桁目の / のビットとして何を採用するか
の3つから決まるので、私の実装ではこの遷移先を求めるだけの関数を作っています。この関数の中はちょっと散らかりますが、そのぶんループを回している本体が少しスッキリします。
このとき、先ほどの×印に相当する「遷移できない」ことの扱いが少し面倒なので、そのときは遷移先フラグの値として2を返すようにしています。DPテーブルのサイズとして / の場所は確保しつつ、次の遷移元にも答えの計算にも利用しないことで実質的に「遷移しない」ようにしています。
この辺りは好みだと思うので、バグらせにくそうだと思ったら真似してみてください。