やー、フェネックだよー。
残りの6問はどれも難しいけど、解説していくよ。
実装例は省略するから自分で考えて書いてみてねー。
U問題 Grouping
羽のうさぎをいくつかのグループに分ける。
うさぎのペアについて、とが同じグループに属していれば点を得る。
最高得点を求めよ。
制約:
解説
bitDPっぽいねー。
集合に属するウサギのグループ分けの最高得点
としてみようか。あとはN問題とほぼ同じで、「どこで切るか」を考えると良さそうだね。
ただし、N問題と違って「切らない」っていう選択肢もありだから注意が必要だよ。
実装はメモ化再帰の方が簡単かなあ。
bitDPは0-indexedの方が都合がいいから、入力は0-indexedで与えられているものとするねー。
#define bit(n,k) ((n>>k)&1) /*nのk bit目*/ long long f(int S){ if(flag[S])return dp[S]; flag[S]=1; //「切らない」場合を計算 temp=0; rep(i,0,N)rep(j,i+1,N)if(bit(S,i)&&bit(S,j))temp+=a[i][j]; fans=temp; //「どこで切るか」を考える for(Sの空でない真部分集合Tについて){ fans=max(fans,f(T)+f(S^T)); } return dp[S]=fans; } //ans=dp[(1<<N)-1];
for文で「部分集合」を動くところが問題なんだけど、すぐに思いつく方法としてはこういうのがあるね。
rep(T,0,S)if((T|S)==S){/* */}
でもこれは状態数、遷移だから全体でになってTLEなんだよねー。
実は「の部分集合」だけをちょうどループ出来るようなこういうテクニックが存在するよ。
for(int T=S;T>=0;T--){ T&=S; /* */ }
この演算は「-1」という操作を「1であるような一番下のbitを0に変えて、それより下のbitを全て1にする」という意味だと思うと納得できるんじゃないかな?
今回は、空集合と自身を除くからこうだね。
for(int T=S;T>=0;T--){ T&=S; if(T==S || T==0)continue; /* */ }
さて、計算量はどうなるかな?
このループは回されるから、全てのについて和を考えると
となって、つまり全体でだね。これなら間に合うよ。
今回みたいにbitDPとかで役に立つbit演算のテクニックはこの記事が詳しいねー。
ビット列による部分集合表現 【ビット演算テクニック Advent Calendar 2016 1日目】 - prime's diary
V問題 Subtree
頂点の木がある。頂点をいくつか選んで黒く塗る。このとき黒く塗られた頂点たちは連結であるようにする。
各頂点について、その頂点を黒く塗るような方法は何通りあるかで答えよ。
制約:、
解説
1つの頂点についてだけ答えればいいんだったら、P問題と同じようにしてで求められるけど、これを各頂点でやってたらになって間に合わないんだよねー。
この問題みたいに「木の各頂点について~~であるようなものを求めよ」って問題は全方位木DPっていう方法で解けることが多いよ。
こういう感じのアルゴリズムだね。
1.適当な頂点を根にして木DPで問題を解く
2.根を子に移して問題を解く。このとき、今までに求めた情報を使い回す
3.繰り返す
詳しいアルゴリズムについてはこのブログが詳しいよ。
†全方位木DP†について - ei1333の日記
他にもsnukeさんのツイートも参考になるねー。
全方位木DP、有向辺に関するDPだと思うのが分かりやすい。
— ꑄ꒖ꐇꌅꏂ🐈 (@snuke_) 2019年1月6日
1. 青い矢印は普通の木DPで下から順に求まる
2. 根から出る矢印は全部求まっているので、根に入る矢印も求められる(ここの計算量改善に総和とって引き算とか左右からの累積和とかが出てくる)
3. 同様に赤い矢印を上から順に求めていく pic.twitter.com/p4bl6rXzGn
実はこの問題は、全方位木DPを知ってても"やるだけ"じゃないんだよねー。
まずはこの図を見てもらおうか。
左側でまず頂点1を根として問題を解いて、次に根を頂点2に移して問題を解くことを考えるよ。
左側の図で頂点1を根として問題を解くときは、頂点1で集める情報は「頂点2、頂点3、頂点4」の部分木に関する情報だね。
だけど右側の図で頂点2を根とするときは、頂点1で集める情報は「頂点3、頂点4」の部分木に関する情報で、頂点2は親だから含まないねー。
同様に頂点3,4を根にすると、頂点1で集める情報は「頂点2,4」「頂点2,3」になるから、毎回情報を集め直してたら、ウニグラフ*1のときに全体でになっちゃうんだよねー。
かといって「頂点2,3,4」に関する情報を集めておいて、そこから「頂点2に関する情報を取り除く」ということも出来ないよ。部分木の答えがで0のときにゼロ除算になるからね。
どうすればいいかな?ちょっと問題をまとめ直しておこうか。
問題:頂点と隣接する頂点の集合をとする。に属する各頂点に対し、部分木に関する答えが求まっている。このとき、各についてを高速に求めよ。
これも「累積和」を使うと解くことが出来るんだよね。今回は掛け算だから「累積積」って言った方がいいかな?
こういう手順で求めるよ
1.の頂点に順序を付けてとする。
2.前方~の累積積と、後方~の累積積を計算しておく
3.各について求める答えは
こうすれば前計算、本計算でできるから、全ての頂点についての答えがで求められるよ。
ここまで工夫すれば全体での計算量はになって間に合うねー。いやー大変だよー。
…………ちなみに、私は本番ではこれを思いつかなくて、で解いたんだよねー。詳しくは解説しないけど、こんな感じの方法だよ。
1.の頂点に順序を付けてとする。
2.という関数を「区間に属する頂点について答えたい。この部分以外に関する積は」とする
3.区間の長さが1ならが答え。そうじゃないならとして、を再帰呼び出し
こうすると、二分法の要領で、全体だとで求められるねー。やー、競技プログラミングの問題は解ければ正義だから、ゴリ押しも大事だよー?
W問題 Intervals
個の区間と値が与えられる。
長さの01列に対して次のようにスコアを定めるとき、その最大値を求めよ。
条件:区間に1が存在すれば点を得る
制約:、
解説
やー、DPの問題としては全部の問題のなかでこれが一番難しいんじゃないかな?
この問題をDPで解くためには
の条件下で文字目まで決めたときの最高スコア
みたいな感じにしようと考えるのが自然だよねー。このとき、スコアは「に完全に含まれる区間のみ」を考えることにするよ。以降にはみ出す区間は、以降に影響を与えちゃうからね。
DPの状態として持つべき情報は何かを考えてみよう。を考える時には、の時から新たにという形の区間に関するスコアを考慮することになるから、この区間に1があるかどうかがわかる必要がありそうだねー。だから
文字目まで決めて、最後に1であるのが文字目であるようなものの最高スコア
というものを考えてみようか。
として一応は計算できるけど、これをこの形のまま実装するとかかっちゃうねー。
遷移をよく見ると、これは次の3つのステップに分解できるよ。
1.にをコピーする
2.とする
3.の各区間について、なるの範囲でにスコアを加える
ステップ1はとで同じ配列を使い回すことで省略できて、ステップ2は区間maxと1点更新、ステップ3は区間addだから、starry sky treeというものを使うとそれぞれ、全体でにできるよ。
starry sky treeに関する説明は省略するけど、区間addと区間maxができるようにちょっと工夫したセグメント木のことだねー。遅延セグメント木が上位互換になってるから、そっちが使えるならそれでも十分だよ。
X問題 Tower
個のブロックがあり、番目のブロックは重さ、丈夫さ、価値である。
これらのブロックを1列に積み重ねて、次の条件が満たされるような塔を作る。塔に含まれるブロックの価値の和の最大値を求めよ。
条件:塔を構成するどのブロックの丈夫さも、それより上にあるブロックの重さの合計以上である
制約:
解説
ナップサックDPみたいに
番目までのブロックを使って重さ以下で作れる塔の最大価値
としたいけど、丈夫さに関する条件を考えると、次のブロックが使えるかどうかわからないからうまくいかないねー。
実はこの問題は「ブロックをうまく並び替えることで、塔に使うブロックの順序を決めることができる(=次に使うブロックは、使うなら一番下)」らしいよ。やー、なんでこんなこと思いつくのかなー?
どういう順序で使えばいいか考えるためにこういうのを考えてみるよ。
そういうわけででソートしたら、あとは最初に言ったDPができるね。
ちなみにこれは「が大きいものの方が上になってたら、入れ替えても影響がない」という方針でも証明ができて、その方が簡単なんだけど、「が小さいものが上になったほうがよさそう」ってわかってないと思いつけないから、ちょっと難しいかもねー。
計算量はだよ。
Y問題 Grid 2
2次元のマスを移動する。からはかに移動できる。
ただし個の壁があり、そのマスには移動できない。番目の壁の座標はである。
からへ移動する場合の数をで求めよ。
制約:、
解説
壁は予め辞書順かマンハッタン距離順にソートしておこうか。それから二項係数もでてきそうだから「壁がない時(0,0)から(x,y)まで移動する場合の数=」のことをと書くことにしとくよ。階乗とその逆元をで前計算しておくことで、はで答えられるとしていいね。(やらなくても間に合うけどねー)
一瞬座標圧縮にも見えるけど、圧縮して無くなったマスだけを通る経路をうまく数えられないからそれはダメそうだねー。
が小さければ
番目の壁を通って、スタートからゴールまで移動する場合の数
として包除原理を使うことでも解けるけど、今回は無理だよ。
求めるものの余事象を考えると、「壁のマスを1回以上通って、ゴールまで行く場合の数」になるわけだけど、「1つ以上あるか?」とくれば、W問題と同じように「最後に/最初に〇〇したもののindex」を状態に持てば良さそうだねー。
そこで
最初にぶつかる壁がである場合の数
番目の壁を避けて番目の壁の位置にたどり着く場合の数
というのを考えるよ。
番目までの壁に1回以上ぶつかって壁の位置にたどり着く場合の数
で、
番目までの壁に1回以上ぶつかって壁の位置にたどり着く場合の数
壁で初めて壁にぶつかって壁の位置にたどりつく場合の数
番目の壁を避けて番目の壁の位置にたどり着く場合の数番目の壁の位置から番目の壁の位置まで壁を無視して移動する場合の数
となるから、遷移はでできるね。
ゴールまでの場合の数も同様にしてできるから、計算量は全体でだね。
Z問題 Frog 3
個の足場がある。
足場からは足場のいずれかに移動でき、足場から足場に移動するコストはである。
足場から足場に移動するまでの最小コストを求めよ。
制約:、、
解説
やー、この問題はCHT(Convex Hull Trick)を知ってれば、見た瞬間「CHTだ」ってなる問題だねー。いわゆる「知識ゲー」ってやつかな?
まずはCHTっていうのは、こういうのだねー。
区間と直線群が与えられたとき、次の2種類のクエリを高速に処理するアルゴリズムをCHTという。
1.直線追加クエリ:に直線を加える
2.最小値クエリ:に対しを求める
今回は追加される直線の傾きと最小値クエリで問われる値のどちらもが単調だから、dequeを使って書けるんだけど、せっかくだからLi Chao Segment Treeを実装したよ。これはどちらのクエリについても単調性を問わないものだねー。
実装はこのブログを参考にしたよ。
傾きの単調性が要らない Convex Hull Trick - kazuma8128’s blog
Li Chao Segment Tree
点集合と直線群が与えられたとき、次の2種類のクエリをで処理するデータ構造
1.直線追加クエリ:に直線を加える
2.最小値クエリ:に対しを求める
元の問題の解説に戻ろうか。
A問題と同じ素直なDPを考えると
足場にたどり着くまでの最小コスト
となって遷移がだから全体でになって間に合わないんだけど、遷移式をよく見て変形すると
となって、最後の部分をminの外に出して
となるねー。
わかりやすくするために、とおくと、minの部分は
つまり「という直線群のにおける最小値を求めよ」という問題になってCHTで解けるというわけさ。
計算量は、単調性を仮定したdequeを使った実装だと、単調性を使わないLi Chao Segment Treeだとだね。
やー、これで26問全部を解説したわけだけど、どうだったかなー?
私たちは本番ではTWXYZの5問を残して21完で、残った5問は主にsnukeさんの解説を参考にして解説ACしたよ。
ꑄ꒖ꐇꌅꏂ🐈 on Twitter: "DPコン、解法全部書いた。 https://t.co/BqSnOQIkbR"
解説AC含めて全問ACすれば黄色くらいの実力になるんじゃないかな? 私たちが黄色になったばっかりだしねー。
それじゃあねー。
*1:1つの頂点を中心にして、そこからたくさんの辺が出てるようなグラフ