−目次
AtCoder Beginner Contest 207 D,E,F問題メモ
総合的になかなか高難易度の回だった。
D - Congruence Points
不必要な高速化をしてコーナーケースに引っかかる、あると思います。
問題
- 2次元平面上の点の集合 S,T があり、ともに要素数は N
- 2N 個の各点の座標は与えられる
- 以下の2つの操作を好きなように繰り返して、S を T に一致させられるか判定せよ
- 全点一斉に (dx,dy) だけ平行移動
- 全点一斉に原点を中心に角度 r だけ回転
- 1≤N≤100
- −10≤各点の座標≤10
- 入力は全て整数
解法
S,T から1点ずつ取り出して「この2点が一致するように操作するとき、他の点も一致するか」を考えるだけだと、平行移動と回転角度の組合せが一意に決まらない。
S,T から2点ずつ取り出すと(どっちをどっちに対応させるかで2通りあるが)一意に決まる。
しかしそれでは、他の点が正しいかのチェックも合わせると O(N4) とかになってしまい、間に合わない。
- 補足: 取り出した2点の距離が一致していないとその時点でアウトなので、実際にチェックが走る数が絞られることを考えると、間に合うみたい
まず平行移動
平行移動と回転移動は、
「回転移動の後に平行移動すると可能な配置だが、平行移動の後に回転移動では絶対になり得ない」みたいなのは存在しないので、
どちらも高々1回で済むと考えてよい。
また、S だけを操作するのでなく、T も操作して S に寄せていくと考えても判定には影響ない。
2つの操作で変わらないものといえば、「重心と各点との位置関係」がある。
S,T で N 点の重心をそれぞれ取って、平行移動で原点に持ってくれば、後は回転移動だけで一致(可能なら)させられる。
これで1点同士の対応関係に持ち込める。
回転移動で判定
重心を原点に移動した点集合を S′,T′ とする。
S′1 と対応させるのを T′i と固定すると、回転させるべき角度が決まる。
その角度で S′ の他の点も回転させて、T′ と一致するか判定すればよい。
判定の方法は愚直でよい。
つまり、S′1~S′N の回転移動後の座標を求め、
それぞれにつき T′ に許容誤差を満たす点があるか探すとよい。
制約が小さいので、小数のまま計算しても誤差はまず心配ない。 1つの S′i が2つ以上の T′ と近くなったり、その逆、ということは発生しないと思うが、 不安ならカウントしてちゃんと1対1対応になっているか調べてもよい。
計算量
- 事前計算の重心計算と平行移動に O(N)
- S′1 をどれと対応づけるかで O(N)、その判定で O(N2)
全体で O(N3) で、十分間に合う。
判定の高速化
平行移動後、S′,T′ の各点を偏角でソートすると両者で対応させるべき点が順に並ぶので、 どの位置から一致を確認していくかさえ決めれば判定を O(N) にできる……のだが。
重心と一致する点があった場合、偏角が定義できず予測できない値となるため、 その点だけは取り除いておかないとソート位置がずれる点に注意。
また公式Editorialによると、 偏角と原点からの距離でソートした上で (偏角1, 距離1, 偏角2, 距離2, …) という数列にそれぞれ変換してやると、 文字列検索に用いられる Z-Algorithm などが使えて、どの位置から一致するかを含めて O(N) で判定可能となる。
E - Mod i
問題
- 長さ N の数列 A1,A2,...,AN が与えられる
- 以下の条件を全て満たすように、先頭から順に空でない連続する部分列に切り分ける
- i 番目の部分列の総和は i で割り切れる
- 切り分け方の総数を \mod{10^9+7} で求めよ
- 1 \le N \le 3000
解法
3乗DPを、観察や発想で2乗DPに落とす。
3乗DP
まず3乗DPは比較的素直に構築できる。
- DP[i][k]=A_i までを k 個の部分列に分けるパターン数
もらうDPで考えたとき、A_{j+1}~A_i が k の倍数になる場合のみ、以下のように遷移できる。
- DP[i][k] += DP[j][k-1]
i 0 1 2 3 4 5 6 7 8 9 10 A 3 1 4 1 5 9 2 6 5 3 5 ... 区切り方の一例 番号 | 1 | 2 | 3 | 4 | 総和 | 3 | 22 | 6 | 8 | 他の例 番号 | 1 | 2 | 3 | 4 | 総和 | 4 | 4 | 15 | 16 | 例えば A9=3 が k=4 個目の部分列の終端となるには、 i=9から遡って累積和が4の倍数となるところから区間が始まる必要がある。 →その直前までを3個の区間に分割するパターン数が加算される 5+3 = 8 なので、DP[7][3] から遷移できる 2+6+5+3 = 16 なので、DP[5][3] から遷移できる 1+4+1+5+9+2+6+5+3 = 36 なので、DP[0][3] から遷移できる → DP[9][4] = DP[7][3] + DP[5][3] + DP[0][3]
だが、これだと各 i,k につき j の位置を全探索しないといけないので、O(N^3) となる。
計算量改善
その1
公式Editorialで直感的にわかりやすいのは、kyopro_friends氏のものかと思う。
上の例で DP[9][4] を求めたいとき、以下の情報を求めておけば、
- i=9 より前で一番直近の、i=9 から遡った累積和が k=4 の倍数になる位置はどこか?
この場合は j=7 となるが、それより前で累積和が 4 の倍数になる j=5 や j=0 で区切ったパターンは、実は DP[7][4] に既に含まれている。
だから更新は DP[7][3]+DP[7][4] でいいよね、ということ。
わかれば単純なものの、気付くのは結構難しいかも知れない。
その2
A の先頭からの累積和を C とする。
- ある区間 (l,r] の和が k で割り切れる \Longleftrightarrow C_l と C_r を k で割ったあまりが等しい
というのはよく出てくる言い換えである。
DPの更新にこれを適用すると、DP[i][k] は、 C_i と C_j を k で割ったあまりが等しいような j について、DP[j][k-1] の総和である。
再び上の例を使うと、 DP[5][4] = DP[0][3] DP[7][4] = DP[0][3] + DP[5][3] DP[9][4] = DP[0][3] + DP[5][3] + DP[7][3]
このように、i が進むにつれ、同じグループの中ではそれまでの参照箇所は変わらず、新しいのが追加されていくことになる。
このグループごと、つまり (k, C_i を k で割ったあまり) ごとにDPの結果を合計しておけば、 必要な値がすぐに取り出せるようになる。
- SUB[k][m]= 今までの時点で、C_i を k で割ったあまりが m であるような i についての DP[i][k] の総和
DP[i][k] について求める際、
- DP[i][k] は、その時点の SUB[k][C_i \% k] に入っている値
- SUB[k+1][C_i \% (k+1)] に DP[i][k] を足し込む
と、高速な更新が可能となる。
F - Tree Patrolling
問題
- N 頂点の木が与えられる
- ある頂点に監視員を置くと「その頂点」と「それに隣接する頂点」を監視してくれる
- 監視員の置き方は 2^N 通りあるが、少なくとも1人の監視員に監視される頂点数が k 個となる置き方の個数を知りたい
- k=0,1,...,N のそれぞれについて \mod{10^9+7} で求めよ
- 1 \le N \le 2000
解法
木DPで、適当に根を決めて子の情報を統合していけばよさそう。シンプルなのは以下が思いつく。
- DP[v][k]= 頂点 v 以下の部分木で、監視されている頂点数が k 個となるパターン数
だがこれだと情報が足りない。
- v に監視員を置いたとき、子が監視されてないなら新たに監視されることになるが、既に監視されていたなら増えない
- v に監視員を置かないとき、監視員の置かれた子が1つでもあれば、v も監視される
このあたりを区別するには、以下の情報を追加する。
- DP[v][f=0/1/2][k]= 頂点 v 以下の部分木で、状態が f で、監視されている頂点数が k 個となるパターン数
- f=0: v に監視員が置かれておらず、監視されてもいない
- f=1: v に監視員が置かれていないが、子に置かれた監視員より監視されている
- f=2: v に監視員が置かれている(当然監視もされている)
ここまではわかっても、いざ子の情報を統合するのも結構難しい。
たたみ込みの要領で行うのだが、その前後でちょっと加工してやる必要がある。
統合の基本処理
f のことは簡単のために一旦無視する。情報が足りない方のDPで説明する。
v の2つの子 u_1,u_2 を統合する処理を考える。(数値は適当)
k 0 1 2 3 DP[u1] [ 1 3 5 2 ] DP[u2] [ 1 0 2 ]
すると、v にとって(自身を除いて)監視されている頂点数がたとえば3個になるような場合の数は、
- u_1 から1個、u_2 から2個持ってきた場合
- DP[u_1][1] \times DP[u_2][2] = 3 \times 2 = 6
- u_1 から2個、u_2 から1個持ってきた場合
- DP[u_1][2] \times DP[u_2][1] = 5 \times 0 = 0
などとなり、これらを合計したものなので、
DP[u1][0]*DP[u2][3] + DP[u1][1]*DP[u2][2] + DP[u1][2]*DP[u2][1] + DP[u1][3]*DP[u2][0]
という要領で、たたみ込みになっている。
最初に DP[v]= [1] と初期化して、これをベースにそれぞれの子をたたみ込んでいくとよい。
実際にはここに、子の状態 f による場合分け、v にも監視員を置くかの場合分けが入る。
①f=0 の更新
v に監視員は置かれず、監視されてもいけない。
子に監視員が置かれたパターン(f=2)は使えないが、f=0,1 であれば それぞれの子がどちらであってもよく、子同士も干渉しない。
つまり、子 u ごとに以下を計算して、これでたたみ込みを行ったものとなる。
- (A) DP[u][0] + DP[u][1]
※DP[u][0] などは1次元配列を表していて、配列同士の +,- の演算は要素毎の和、差を表すとする
②f=1 の更新
v に監視員は置かれないが、少なくともどれか1つの子に監視員が置かれている。
これは、「どの子にも監視員が置かれていない」という①の余事象なので、
- (B) DP[u][0] + DP[u][1] + DP[u][2]
でのたたみ込みを計算すると、全ての子を統合した後で「(B)の結果 - (A)の結果 を計算し、最後に1つ右シフト」したものが DP[v][1] となる。
右シフトは、たたみ込みの結果に v 自身が考慮されていないため、それを考慮するための操作となる。
③f=2 の更新
v に監視員を置くことによって、 子 u のうち、u 自身が監視されていない状態のもの(f=0)があれば、新たに監視されるようになる。
つまり、DP[u][0][k] を DP[u][0][k+1] と見做して扱えばよい。
- (C)「DP[u][0] を1つ右シフトしたもの」+DP[u][1]+DP[u][2]
これでたたみ込みを行い、最後に1つ右シフトしたものが、DP[v][2] となる。
計算量
子を1つ統合するたびに3パターンのたたみ込み計算が必要となる。
統合する2つのDPテーブルのサイズを L,R とすると統合に O(LR) かかり、 それを N 個の頂点で行うので、、、なんとなく O(N^3) になりそうで間に合わない!
これは、2乗の木DPと呼ばれる計算量見積もりに関する知識で、以下の場合に
- 各頂点が持つDPテーブルのサイズは、その頂点の部分木のサイズに比例
- 子を統合する際にかかる計算量は、全ての子のDPテーブルのサイズの総積に比例
要素数が小さい内は統合の計算量も小さいので、全体で O(N^2) で済むよ、という証明が出来る。
この方法が使用できる。