モノグサプログラミングコンテスト2022(AtCoder Beginner Contest 238)F,G問題メモ
モノグサプログラミングコンテスト2022(AtCoder Beginner Contest 238)
Monoxerは、主に暗記学習について生徒・先生両者の負担を減らし効率化できるという学習用アプリ。
教育現場に売り込みたいにしてはちょっとネーミングが変という感じがしないでもない。
(むしろこういう方が印象に残ったりするのかな)
F - Two Exams
問題
- N 人が2回の試験を受け、1回目は Pi、2回目は Qi の順位を得た
- 同率はいないので、P,Q はそれぞれ 1~N の順列
- ここから以下の条件を満たして K 人を選ぶ方法の個数を 998244353 で割ったあまりで求めよ
- 条件
- ある人 x が選ばれていないとき、x より2回の試験でともに順位が低い人が選ばれていてはいけない
- たとえば (Pi,Qi)=(3,2) の人が選ばれず、(5,6) の人が選ばれていることがあってはいけない
- 1≤N≤300
解法
2次元座標に落としたとき、自分より左下の人は全て選ばれている必要がある。
| ⑤ 数字は人の番号 Q| ③ |④ ③が選ばれるには、①と④がともに選ばれていないといけない | ① ⑤が選ばれるには、④が選ばれていないといけない | ② +-----------P
P 基準でソートして、上位(左)から選ぶか選ばないかDPすることを考える。
DPでたとえば③を選ぶか選ばないか決める段階になったとき、
③より左下にある点が全て選ばれている場合のみ、選ぶという遷移をすることができる。
要はこれまで処理された中で、Qi が Q3 より小さい点が全て選ばれている必要がある。
ぱっと見、これまでに何を選んだかの全ての情報がないと判定できないように思えるが、 よく考えると「今までに選んでいない、一番小さい Qi」がわかれば、③を選べるかどうかを判定するには十分である。
よって、以下のDPを組めばよい。
- DP[i][j][k]=i 番目まで処理して、j 個選んで、選んでいない中で最小の Qi=k である場合の数
なかなか、こういうまとめ方をするDPは見たことなかった。
G - Cubic?
問題
- 長さ N の正整数列 A1,...,AN が与えられる
- Q 個のクエリを処理せよ
- クエリ
- Li,Ri が与えられる
- ALi×ALi+1×...×ARi が立方数(何らかの整数の3乗)かどうか判定せよ
- 1≤N,Q≤2×105
- 1≤Ai≤106
- 実行制限時間: 3sec
解法
計算量を気にしなければ、
- Ai に出現する素因数毎に、どこに何個、素因数として出現したかの累積和を取っておく
- 累積和を使えば、素因数毎に、Li~Ri の総積に含まれる個数を O(1) で計算できる
- クエリが来たら、全素因数につき、Li~Ri の総積に含まれる個数が3の倍数かチェック
とすればよい。
i (0) 1 2 3 4 5 6 7 8 A 7 49 30 1 15 8 6 10 ------------------------------- p=2 0 0 0 1 1 1 4 5 6 p=3 0 0 0 1 1 2 2 3 3 p=5 0 0 0 1 1 2 2 2 3 p=7 0 1 3 3 3 3 3 3 3 L,R=4,6 p=2 のとき、4-1 = 3 OK p=3 のとき、2-1 = 1 NG → 立方数ではない L,R=3,8 p=2 のとき、6-0 = 3 p=3 のとき、3-0 = 3 p=5 のとき、3-0 = 3 p=7 のとき、3-3 = 0 → ALL OK!
だがこれだと 106 以下の素数は 78498 個で、この全てが同時に出現する可能性があるので、約 78498(N+Q) 回の計算&メモリが必要になり間に合わない。
まぁ、このほとんど全てが出現するような場合、そもそも全体の総積に1~2個しか含まれない素数が多くなるので、
それを除けば実質的な個数は減るかもしれないが、それでも厳しい。
100 個程度だとギリギリ間に合わなくもないかも知れないが、さすがにそこまで減ることはなさそう。
そこで、複数の素数を同時に上手く管理する方法を考える。
2bitでのXOR
ある区間に含まれる何らかの個数が「2」の倍数かどうかを求めたい場合、 上記の累積和の代わりに、出現位置に'1'を立てての累積XORを使える。XORが0なら偶数。
ある区間の個数が3の倍数かどうかを求めたい場合も同様に、2bitを利用すれば累積XORを使える。
- 1番目に出現した場所に「1(0b01)」を立てる
- 2番目に出現した場所に「2(0b10)」を立てる
- 3番目に出現した場所に「3(0b11)」を立てる
- 以降繰り返し
こうすると、どこから始めてもちょうど個数が3の倍数になったときだけ、XORが0になる。
1つの素数につき2bitずつ使うので、1つの64bit整数で31~32個の素数を管理できる。
高速な言語なら素因数31個ごとにまとめるだけで通るかもしれない。
しかし、Pythonとかでは不安なので、より高速化する。
modでまとめちゃう
1番目の素数を1,2bit目、2番目の素数を3,4bit目、、、31番目の素数を61,62bit目に当てはめる。
ここで32番目の素数をまた1,2bit目から始め、1番目の素数とまとめてしまう。
当然、これは誤った結果を返しうるが、ループを29個、30個、31個などずらしてその全てでチェックすれば、 さすがに誤った結果を返す可能性は十分低くなる。
そうすると、1つの値だけのチェック(xループずらし数回)で済むので、十分高速になる。
上手くいかないケース
たとえばループを29,30,31の3パターンで試す場合に誤るテストケースはどんなものがあるか?
本当にYes(立方数)である場合は総XORは必ず0となり、Noと誤判定することはないので、誤るのは本当はNoなのにYesとなってしまうケース。
同じbitを共有する素数の区間XORを個別に計算したら、 個々で見ると0でないのに、全体のXORを取ると0になってしまう、 そんなことが全てのbit位置で発生していることになる。
i 番目の素数を pi とし、ある区間の p1 についての個別XORが 01
だったとする。
総XORが全体で0となり誤判定が生じる場合、たとえば以下を全て満たしている場合が当てはまる。
- 29でループするチェックに対し、p30 も
01
- 30でループするチェックに対し、p31 も
01
- 31でループするチェックに対し、p32 も
01
すると、29でループする時に p31,p32 とbitを共有するのは別の素数(たとえば p2,p3)なので、
これも 01
でなくてはならず、、、と連鎖することになる。
こうなるとさすがに全てで整合性を取るのはきつそう。
ただ、たとえば LCM(29,30,31)=26970 なので、
p26971 はどのチェックでも p1 と同じbitを共有してしまい、これが 01
だと全チェックをすり抜けてしまう。
また、p1~p53940 の全てが 01
だったりしてもすり抜ける。
そもそも、そうなること自体が相当珍しいが、、、
チェックにたとえば23を加え、4つでチェックすれば
LCM(23,29,30,31)=620310 となり、106 以下の素数の個数を超えるので撃墜は確実に防げる。
これでももしかすると上手く撃墜ケースが作れるのかも知れないが、具体的にわからん。
多倍長で殴る
多倍長が可能な言語では、まとめなくても全ての素数を個別に扱うこともできるっちゃできる。
ただし、それだとさすがに素数の個数が多くなったときにMLEとなった。
そういうケースは素数1つ当たりの出現回数は少ないことが想定できるので、
全体で2個以下しか出現しなかった素数は絶対に不可能として、bitを割り当てない、とする。
不可能な素数は出現位置を記録し、それを含むようなクエリが与えられた場合は問題無用でNoにすればよい。
まぁでもこの回避方法が読まれていた場合、最低限の3回ずつ多数の素因数が出てくるテストケースは作れるので、単に見逃されただけだったり。
Zobrist Hash
複数の要素の状態(パリティ)を同時に管理したいとき、要素ごとに乱数を対応させ、そのXORで判定する方法をZobrish Hashというらしい。
先ほどは、2bitを「ある1つの素数の区間内の個数 mod 3」を表現するのに使ったが、ここでは2つの適当な乱数を使う。
素数ごとに64bit以下の異なる正整数を2つ選ぶ。素数 p について、rp,sp とすると、
- 1回目の出現時は rp
- 2回目の出現時は sp
- 3回目の出現時は rp XOR sp
- 4回目の出現時は rp
- …
として累積XORを取ると、2bitを使ったときと同様に、個数がちょうど3の倍数になったときだけ XOR が0になる。
複数の素因数による rp,sp が混ざっても、全てが3の倍数になれば総XORは0になるので、容易に判定できる。
本当の答えはNoなのにYesと判定されるケースは、 「個別でXOR計算したら0にならない複数個の素数が、偶然打ち消しあって全体では0になってしまう」ような場合だが、 乱数なので64個ある総XORの各bitが0,1のどちらになるかは 12 で、それがたまたま全て0になるのは 1264 と概算でき、十分低い。
テストケース 2×106 個が全て通るのは、(264−1264)2000000≃0.9999999999998915797828000059 となり、ほぼ大丈夫なことがわかる。
それでも不安なら、複数回繰り返せばよい。
2bitずつ割り当てる方法は規則的なのでもしかしたら上手い撃墜ケースがあるかもしれないが、Zobrish Hashのように乱数を使うことで、より頑健になる。
Mo's Algorithm
クエリ平方分割手法の1つ、Mo's Algorithmを使っても解けるらしい。
素因数分解
ところで、XORとかを考える前に、まず N 個の整数の素因数分解をしなければならない。
√Ai までの素数で1個1個試し割っていくという 通常の?素因数分解では、1000以下の素数の個数 168×N の計算が必要になりそれだけで厳しい。
Ai の上限がそれなりに小さい(1次元配列を持てる程度)場合、SPFを使うことで1つあたり O(logAi) で求められる。そちらの方が高速。