CODE THANKS FESTIVAL 2018-G:Sum of cards
詰め切るのが大変。本番で通した人たちすごい。
解法
カードについて,どちらか一方のスコアを獲得できるという状況だ。こういう状況は,グラフにしてみたくなる。からの数字が頂点,辺をの間に結んだグラフを考えよう。このグラフは,どの頂点も次数がであり,円形のグラフの集まりになる。のどちらを表にするかということを辺の向きで表現しよう。のうち,辺が入る方を表にする,ということにする。するとこの問題は以下のように書ける。
- 頂点の,円形のグラフが集まったグラフがある。各頂点にはからの数字が書いてある。このグラフの各辺に向きをつけ,各辺について,その辺が入る頂点に書いてある数字だけスコアを得る。少なくとも1本は辺が入ってくる頂点が個以上あるという条件のもとで,スコアの最大値を求めよ。
この問題を解いていこう。
円形のグラフが1つのとき
まずは円形のグラフが1つのときを考えよう。これをどう解くかで既に難しく思えるが,なんとここでdpをする(初見厳しくないか...)。どのような状態を持つべきかを考えよう。まず,便宜的に頂点番号をとする。は円形を構成する頂点の数とする。今どの頂点をみているかということは当然必要だろう。また,辺が入ってくる頂点の数に制約があるので,これも状態としてもっておきたい。さらに,この「辺が入ってくる頂点の数」をカウントするために重要な情報がある。それは,頂点頂点の間の辺の向きである。この頂点の向き次第で,頂点から頂点に移るときに,辺が入ってくる頂点の数が変わってくる。では
- :頂点までで辺が入る頂点の数が個,は頂点と頂点間の辺の向きを表す。のときに頂点から頂点に辺が入る。のときはその逆とする。
で十分だろうか?とりあえずこの場合の状態遷移について考えてみよう。便宜上,から始め,まで見た後,をみることにする。
のとき
でのときは以下の図のようになる。には辺が入っていなかったが,から出る辺で入ってくるようになるので,の添え字が1増える。
また,でのときは以下の図のようになる。にもともと辺が入っていたので,の添え字は増えない。
のとき
同様に考えればよい。
で
で
というわけで遷移が書けた。めでたしめでたし...と言いたいのだが,ここには罠がある。そう,いま考えているグラフは円形であったから,頂点と頂点の間から順に見ていって,最後に頂点と頂点の間を見ることになるが,このときに,上で述べた遷移にはならないパターンがある。以下の2つの図を見比べてみよう。
頂点から頂点に辺が出ているとき
頂点から頂点に辺が出ているとき
からに辺を伸ばすときは基本的に辺が入る頂点の数は増えるはずであったが,一周して戻ってくるときだけは,そうならないのである。よって,この3次元DPを素直にやるだけではうまくいかないのだ。ではどうするか?答えはシンプルで,「からに辺が出ているか」それとも「からに辺が出ているか」ということを状態としてもてばよい。そうすればからの遷移の際にのみ気をつけることで,この問題を回避できる。つまり
- :頂点までで辺が入る頂点の数が個,は頂点と頂点間の辺の向きを表す。のときに頂点から頂点に辺が入る。のときはその逆とする。は頂点と頂点の間の辺の向きを表す。のときに頂点から頂点に辺が入る。のときはその逆とする。
これで円形1つのときの問題を解くことができた。頂点が1つのみのときに注意。
円形のグラフが複数あるとき
さて,この問題でグラフを作ったときに,個の頂点すべてが連結になるとは限らない。よって,上のようにして各円について問題を解くことはできるが,その結果をまとめてやる必要がある。しかしこれは難しくない。円が全部で個あるとしよう。各円について,辺が入る頂点が個あるときのスコアの最大値がわかっている。このとき,であることに注意しよう。それならば
- :個目の円の番目のありえる辺が入る頂点の個数まで考えたときに,合計で辺の入る頂点の個数がのときのスコアの最大値
「番目のありえる辺が入る頂点の個数」というのは,とすると,に新たにという番号を振りなおしたと考えればよい。遷移はナップサック問題のような形になる。このうち,がより大きいものの最大値が答えとなる。
この解法全体の計算量はである。
実装
Submission #4129625 - CODE THANKS FESTIVAL 2018
これが参考になるかわからないが,上で述べたことを書くとこんな感じになる。各円の結果をまとめる部分で,前の円の大きさを保存しておかないといけなかったりなど,実装上面倒なことは多い。もう少し楽な実装方針があるかもしれない。
大変だが,勉強になる問題であった。ちなみにうまくやると最大費用流になるらしい。
CODE THANKS FESTIVAL 2018 F-Coins on the tree
本番は誤読をした...。
解法
辞書順最小を作れという指示がある。辞書順最小ということは,前の数字が小さいことが正義である。ならば,先頭から「1にできるか?」を判定し,できるなら確定,できないなら「2にできるか?」ということを判定,というように貪欲に試していこうという方針は自然である。
それでは,試しに番目のコインを頂点に置けるかということを判定することを考えよう。まず,コインを頂点に置いたのだから,木の根からの距離を(ただし,とする)とすると,手数をだけ消費する。ここから,残りの枚を,手でピッタリ置ききる必要がある。このようなことが可能であるの条件がわかればよい。
まず,に置くと決めたら,の子孫たちはもう使えないことに注意しよう。つまり,1番目のコイン以後に使える頂点の数が減るのである。このとき,もし使える頂点の数がを下回るようなことがあれば,1番目のコインをに置くことはできない。
使える頂点の数はクリアしたとして,手数の問題を考える。残り手数をピッタリ消費する,という条件は扱いにくいが,このような場合は可能な手数の上限と下限を考えるとうまくいくことがある。まず,手数の上限は,到達可能な頂点のうち,を大きい順に個取ったときの和である。消費する手数をできるだけ多くするには,できるだけ根から深いところにコインを置くようにすればよいので,このことがわかる。同様に,手数の下限は,を小さい順に個取ったときの和である。理由は上限と同様である。そして,この上限と下限の間の手数の消費は必ず可能である。手数の下限の状態から,手数の上限の状態までコインを順番に動かすことを考えれば,このことがわかる。以上より,がこの上限と下限の間にあれば,コインを頂点に置くことが可能となる。
この判定は高速にできるだろうか。高速といっても,本問ではくらいかけてよい。それならば,到達可能な頂点をすべて訪問し,のリストを作る。リストのサイズがより大きいことを確認した後,これらの大きいほう,そして小さいほうからの和をそれぞれとり,と比較すればよい。sortを使ってもでできる。
1番目のコインをに置くことが可能だとわかったら,をだけ減らし,2枚目のコインについても同様にどの頂点に置けるかを番号の小さい順に調べる。このようなことを繰り返せば,辞書順最小のコインの置き方が求まる。もし,あるコインがどの頂点にも置けないということであれば,を出力すればよい。
以上をまとめて,次のようにすればこの問題が解ける。
- 最初に,各頂点についてを求める。
- 番目のコインを頂点に置けるか,ということをが小さい順に試す。到達可能な頂点を訪問しながら,のリストを作って,判定を行う。置ける条件は,①のリストのサイズが以上であること かつ ②の小さいほうから個の和を,大きいほうから個の和をとすると,であること の2つ。ただし,このは番目のコインを置こうとしている時点での残り手数を表している。頂点に置けるとわかれば,をだけ減らして,次のコインに進む。置けないなら,次の頂点を試す。どの頂点にも置けないとなれば,を出力して終わり。
- 枚目のコインまで置けたら,結果を出力して終わり。
全体の計算量はである。
実装
Submission #4123308 - CODE THANKS FESTIVAL 2018
dfsが3つあるが,それぞれの役割は以下にようになる。
- :を計算する関数。
- :判定パートで,到達可能な点を訪問する関数。というのは,それがtrueのときに頂点にはコインを置けないということを表してる。頂点を訪問したら判定用のリストにを追加する。の子がまたはがtrueなら,その子孫はもうコインが置けない頂点なので,その先には訪問しない。
- :ある頂点にコインを置くと決まった後,の子孫にはもうコインは置けないということを記録しに行く関数。頂点番号の小さい順から試すということをするため,この作業を怠ってはいけない。
判定パートが若干混乱するが,整理すると綺麗な問題だなあと思う。
NIKKEI Programing Contest-E:Weights on Vertices and Edges
良問だと思ったので記事にすることにした。Cさえなければ....。
解法
こういう重み付きの辺を追加/削除して連結成分が...という問題は,辺が一本もない状態から,一定の順番で辺を追加していくことを考えることで道が開けることがある。
頂点の辺が0本のグラフから,辺の重みが小さい順に辺を追加していくことを考えてみる。このときに,連結成分の重みをもちながら連結していく必要があるが,これはUnion-Findにちょっと書き足せばできる(コード参照)。さて,ある辺を追加したときにできた連結成分が,問題の条件を満たしたとしよう。すると,辺の重みが小さい順に追加していったという仮定から,この連結成分に属するすべての辺は,問題の条件を満たす。つまり,に含まれる辺は残してもよいことになる。このようにして,辺を追加していきながら,条件を満たす連結成分の集合がわかる。これらの和集合に属する辺たちを残しておけば,条件を満たすグラフが作れる。では,残すべき辺たちはどのようにして調べることができるだろうか。Union-Findしながら,重複をはじきつつ辺の数を数えるのは難しく見える。
ここで,の見方を変えてみる。辺の重みをと書くと,は,「から重みがの辺のみを使っていくことができる頂点の集合」であることがわかる。つまり,を求めるためには,が結ぶ頂点から適当にdfsやらbfsやらをするだけでよいことがわかる。本当に求めたいのはの和集合で,何も考えずにやるとかかってしまうが,のうち,の大きい順に調べていけば,かつのとき,になるので,で終わる。
以上のことより,次のようなアルゴリズムを考える。
- まず,重みの小さい辺から順にグラフに追加していく。もし,この辺を追加したことによってできた連結成分が条件を満たすなら,をリストに追加する。
- 上の作業が終わったら,リストに追加した辺のうち,重みの大きいものからを求め,残すべき辺の数を数える。
- が答え。
最後に,このアルゴリズムが正しい解を返すことを確認しよう。まず,グラフの重みが最大の辺が条件を満たさないなら,その辺は削除されなければならない(1の正当化)。満たす場合,その辺と同じ連結成分に属する他の辺も条件を満たすので,その連結成分については辺を削除する必要がない(2の正当化)。この議論に沿った手続きを再帰的に行っているのが上のアルゴリズムであり,正当性がわかる。(解説の通り帰納法で書いた方がすっきりするかも?)
提出
Submission #4117234 - 全国統一プログラミング王決定戦予選/NIKKEI Programming Contest 2019
- Union-Findのsという配列が,連結成分の重みをもっている。
- 2に使う辺の管理だが,小さい順にリストに追加して,大きい順に取り出すので,stackを用いるのが楽だと思った。
Cでハマってしまい本番はこの問題を詰め切れず,本選出場も逃したが,観戦権は得ることができた。よろしくお願いします。(誰に対して言ってるんだろう...。)
ABC116-D:Various Sushi
解法自体は素直なので400点なのだろうが,見た目はとっつきにくいので難しいと思った。
考察・解法
食べることにする寿司の集合を,のネタの種類をと書くと,の最大値を求めることになる。解法はさまざまだが,ここではの和にまず注目することにする。この部分を最大化するのは簡単で,が大きい個を取ればよい。これら個の集合をとしよう。これが最適解とは限らないというところが難しいのだが,これより良い解があるとしたら,それはネタの種類がより大きい場合に限る。だから,からネタの種類が増えるようにの要素の寿司を交換していくことを考えたくなる。さらにこのとき,おいしさの和の項ができるだけ大きくなるようにする(どうしても小さくなるわけだが,それを最小限にする)必要がある。これはどのようにすれば効率的な計算時間で可能だろうか?
まず,を決めるためにでソートしておく。これによりが大きい方から個とり,とを計算する。さらに,実際にに含まれるネタの種類とそれぞれの種類の寿司の数を記録しておく。ここまでやってスタートである。
ここからネタの種類を増やしていく。まず,番目にが大きい寿司のネタの種類を見る。同じネタがにあるなら,のどの寿司を入れ替えてもネタの種類は増えないし,おいしさの和は減るだけなので,交換する必要はない。がにない場合,何かと交換することでネタの種類が増える場合がある。この場合,交換に出すべきの寿司として最適なものは,「に2つ以上含まれるネタの寿司の中で,もっともおいしさが小さいもの」である。このような寿司を選ぶことで,おいしさの和が小さくなることを最小限に抑えることができる。に1つしか含まれない寿司を交換に出しても種類が変わらないことに注意しよう。
以上の手続きを繰り返すことで,おいしさの和を最小限に抑えながら,ネタの種類を増やしていくシミュレーションができる。食べる寿司の集合が更新されるごとに答えの候補として試し,最大値を出力する。計算時間はまたはでできる。
提出
Submission #4063050 - AtCoder Beginner Contest 116
これを実装してくださいと言われてもまだ大変かもしれない。上のコードでは,を求めたあと,とおき,これを交換する候補の最初のindexとしている。また,食べる寿司の集合のネタの種類と数はmapで管理した。ここから寿司が交換に出す寿司の候補としてふさわしいか判断し,だめなら寿司を試すということを繰り返している。となったら,もう交換に出せる寿司の種類はないということなので,処理を終了してよい。mapを使っているので計算時間はとなっている。
- ABC Only回としては難しい,でもこういう問題好きです。
- こどふぉと被っていて,こどふぉはratedなのでそっちに出ていた。
- 〇〇 Sushi という問題は難しめという謎の習慣...?
青色になるまでの軌跡
色が変わったら記事を書く人がそこそこいる競プロ界隈ですが,自分もその流れに便乗してみようと思います。ついでに,茶色からの話も少しだけしてみようかと思います。(本編は水色~青の話です。)
最初にこの記事の結論を言ってしまうと,「ちょっとレベルが高めの問題を解こう!」ということに尽きます。レベルを上げるためには自分のレベルより少し上に触れる必要があると思います。
茶色まで
200点までがサクッと解けて,300点もたまに解ければなれるラインだと思っています。こんな言い方ですが,決して簡単なラインではないと思います。200点までをしっかり埋めていきましょう。
緑まで
緑になるためには,計算量という概念についての理解と,それを改善するための具体的な手段をいくつか身につける,思いつけるようになる必要があります(300点で求められるラインです)。愚直にやるとTLEしてしまう解法をうまくやって計算量改善する,という営みの基本的な能力が求められます。300点を解きながらそういうところを意識して学んでいくとよいと思います。
あとは簡単な数学の知識が必要になります。特に整数分野が頻出な印象があります。ここは数学の勉強が必要になると思います。
水色まで
水色に求められるのは以下のようなレベルだと考えています。
- 300までが早い
- 400~500がたまに解ける
特に300までに早さに安定感があると,緑帯ならほとんどレートは下がりませんし,AGCなら1完で青パフォが出たりするのでお得です。僕は基本的に遅いのでどうすればいいかわかりません。
400~500あたりですが,知識としては蟻本2章+4章のゲーム・数え上げの節程度で十分だと思います。あとはもう問題を解きながら,経験を積んでいくのがよいと思います(投げやり)。
僕は緑で苦戦していた時期が長く,「競プロは地頭ゲーでは...」という気持ちになり正直苦しかったです。なんとか続けてここまで来れているのはよかったなあと思っています。今でも頭は大切だと思っていますが,勉強量でなんとかなる部分もあると思います。
余談ですが,水色になるまでに必要な知識やテクニックをまとめたpdfでも作ろうかな...とちょっと考えています。蟻本では触れられていない部分や,やや言語化しにくいテクニックをまとめたものってないなあという気持ちがあります。
青になるまで
青に求められるレベル
青に求められるものは以下レベルだと考えています。
- 400(~500)までが早い
- 500~600はそこそこ解ける
- 700~900がたまに解ける
頻度の書き方が雑ですが,気持ちとしてはそれくらいです。ちなみに僕は(グラフの通り)安定感は皆無ですが,AGCで800とか900をたまに通してレートが+100みたいなことをして青になったタイプの人です。もちろん,堅実に500~600あたりを通してレートを上げた回もあります。
パフォーマンスの推移
時折爆死していますが(),基本的には順調に伸びているように思ってます。どんどん伸ばすのが難しくなると思っているので,このぺースが維持できるかはわからないですね....。
さて,このグラフを見るとわかる通り,2018年の9月から突然黄色パフォや青パフォがよく出るようになりました。ここら辺であったことを簡単に述べたいと思います。
冒頭に述べた通り,2018年の8~9月頃から,自分のレベルより少し難しい問題を解き始めました。これは400~500が埋まってきたということもあるのですが,結局700あたりが解けるようになるためには,700あたりを知らないとどうしようもないわけです。敵を倒すには敵を知らなければいけません。
さて,当然自分のレベルより高い問題なのでだいたい自力で解けないのですが,積極的に解説ACをしていきました。この「過去問埋めのときに解けなかった問題の解説を見るべきか?」という問題はよく(?)話題になりますが,僕は積極的に見ることを推奨する派閥(?)です。コンテスト中に問題が解けるかどうかが全てだと考えているので,過去問埋めでは基本的に知識や経験を蓄えるべきと考えています。もちろん,解説を見ずに詰めるということも大切な経験ですが,コンテスト時間は高々2時間ですから,僕は2時間くらいで諦めることが多いです。
また,これはこれくらいの得点帯の問題あるあるですが,解説を見たらすぐACできるとかそんな甘いものではありません。解説は往々にして実装レベルに落とすまでの細かい部分が省略されています。また,当然いくつかの知識が仮定されてることが多いです。このような理由で,解説を見て理解して実装するということも十分負荷のある経験であり,実力を伸ばすことに大きく寄与すると考えています。
このような取り組みを始めるまでは,400~500も怪しかったですが,最近は400はほとんど落としませんし,700以上もたまに通せるようになってきました。点数の高い問題は踏むべき考察ステップが多く,1問埋めるだけで多くのことを得ることができます。その結果としてそれより低い問題の正答率にも貢献しているのだと思います。
精進の方法について1つの考え方を述べてみましたが,人それぞれではあるものの,結局自分が難しいと思うレベルの問題もこれから解けるようにならないといけないので,いくつか見ておくということは有用だと考えています。
今年中に「黄色になるまでの軌跡」というタイトルで記事が書けることを祈って本記事を締めくくりたいと思います。引き続き頑張りましょう。
AGC-030:Tree Burning
解けてよかった。
問題:
考察
まず,高橋君はどういう動きをすべきかを考えよう。直感的に,を中心として,左右に振れるような動きをしたくなる。つまり
- 最初は本分右に進んで,それ以降は左→右→...と進む。(1)
- 最初は本分左に進んで,それ以降は右→左→...と進む。(2)
これをで試したとき,その最大値が答えになる(この部分が非自明ですね,わかり次第(そして余裕ができ次第)追記します)。部分点解法は,を固定したあと,この動きを愚直にシミュレーションすればよい。
Submission #3894463 - AtCoder Grand Contest 030
では,満点解法を考えよう。要は,部分点解法ではシミュレーションにかけて答えを求めているが,これがでできれば満点だ。これはすぐにはわからないので,実験をしてみよう。例えばで,としてみよう。このとき,高橋君の歩いた距離は(実際に手を動かしてください)
さて,これを見てなんだかきれいそうな形をしているなあと思えないだろうか。実は,一般的には以下のような結果になる。対称性のため,(1)のパターンだけ述べることにする。
- が偶数のとき
とする。数列をの第項までの累積和とする。このとき,高橋君の歩いた距離は
である。
- が奇数のとき
とする。このとき,高橋君の歩いた距離は
である。
本当にこうなるかどうかに関しては, 実際に確認してみることを勧める(万が一違ったら,提出を参考にしてください)。(2)のパターンも同様の式で書ける(提出を参考にしてください)。
これは累積和を事前に計算すればでできるので,全体でで解ける。
提出
Submission #3895747 - AtCoder Grand Contest 030
- Garbage Collectorの公式の解説放送では,りんごさんが実際に具体例で計算することでキーとなる式を導いていた。これと同じことを試したら解けたので,具体例での計算は大切だと実感。
- 青にあと7届かず。
ABC115-D:Christmas
Pythonで解いた記念。
問題
レベルバーガーを以下のように定義する。
- のとき,パティ1枚。
- のとき,パン1枚,レベルバーガー,パティ1枚,レベルバーガー,パン1枚 を下から重ねたもの。
レベルバーガーの下から枚目までに,パティは何枚あるか。
考察
レベルバーガーが再帰的に定義されていることを使いたい。絵を描いてみるとこんな感じ。
これの番目が例えばここだとする。
これを見れば,「レベルバーガーの番目までのパティの数」は「レベルバーガーの番目までのパティの数」だとわかるだろう。同様に,レベルバーガーはこんな感じで,番目がここだとする。
これを見れば「レベルバーガーの番目までのパティの数」は
「レベルバーガーの番目までにパティの数レベルバーガーのパティの数」
とわかる。こう考えると,再帰的に問題を解いていけばよさそうという気持ちになれる。
では,具体的に再帰関数を書いていくことを考えよう。まず,レベルのバーガーの長さを,レベルのバーガーにあるパティの数をとすると,これらは次の漸化式を満たす。
これはバーガーの定義からわかるだろう。事前にこれらの値は求めておく。
そして,を次のように定義する。
- のとき,
- のとき,
- のとき,
- のとき, (ちょうど真ん中)
- のとき,
- のとき,
これを実装すればOK。計算量はである。