Educational Codeforces Round 74 E. Keyboard Purchase
お題箱より。ただ、この問題は元々書こうと思っていました。
問題概要
文字のアルファベットからなる長さ の文字列 が与えられる。アルファベット 個を並べる順列 を1つ決めた時、「 の隣り合う2文字のペアそれぞれについて、そのアルファベット2つの での距離を求め、合計したもの」を合計コストとする。この合計コストの最小値を求めよ。
制約
解法
まず異なるアルファベットのペア 通りについて、そのペアが において何回隣り合っているかを前もって集計しておきます。こうすれば以降の計算で文字列の長さ は関係なくなります。アルファベットのペア に対するペアの登場回数を と書くことにします。これは隣り合っている順番を区別せず両方含むことにします、すなわち です。
また順列 上での の距離を と書くことにします。そうすると合計コストは を全てのペアについて合計したものです。
順列 に並べるアルファベットを順に1つずつ決めていくようなDPを考えてみます。このとき単純に考えると、アルファベットを足したことによるコストの増加量を「新しく追加したアルファベット と既に追加済みのアルファベット たちについて、 を足す」と計算したくなります。ただし を計算するには既に追加したアルファベットの順番を覚えておく必要があり、状態数が爆発します。
何とかして順番を覚えなくても良いようにしたいです。順番を忘れることができれば、これは集合に対するDP、つまり状態数が になるようなbit DPとして解けそうです。
ここで発想を変えましょう。「アルファベットを1つ足そうとして、 の置き場所の境目を1つまたぐときに、追加済みのアルファベットと未追加のアルファベットのペア全部について距離が1増える」と考えます。言わんとすることは図を見てください。
こうするとペア数自体は多くなってしまうのですが、「追加済みのアルファベット」を一括りにできるので順番を覚えておく必要がなくなります。
こうやって計算されるコストの増加量は、アルファベットの集合 に対する「 に含まれる要素 と含まれない要素 を組み合わせた全てのペアについての、 の合計値」です。この値を と書くことにすると、DPの遷移は
- 集合 を遷移元として、 含まれていない要素 それぞれについて、 と更新(より小さかったら採用)する
と行うことが出来ます。ここでDPテーブルの定義は、「集合 に含まれている要素を既に並べていて、既に通り過ぎた境目について、アルファベットのペアがそこをまたぐ時のコスト増加量を合計したものの最小値」です。
遷移の合計回数が なので、あとは を良い感じに前計算できれば間に合いそうです。
の前計算は、こちらも の状態数が なので1個あたり くらいで計算できれば勝ちです。これもDPで計算できます。
ある集合 について既に が求まっているとします。ここで に含まれていない要素 を新しく に足すと、 に対する変化量は
- に含まれている要素 について、 が新たに同じ集合に入るので が減算される。
- に含まれていない 以外の要素 について、 が違う集合に分かれるので が加算される。
として計算できるので、 から を差分計算できます。これは でできます。
これで前計算も で出来たので、間に合います。
ACコード
Submission #62145546 - Codeforces
数え上げ問題で、よく「全てのAについてのBの合計を求めよ」という問題を「Bの加算要素それぞれについて、それが含まれるAの個数を掛けたものの合計を求めよ」と言い換えるものがあります。今回も「アルファベットのペア」と「 における置き場所の境目」について同じことをしている、と考えることが出来ますね。
第一回日本最強プログラマー学生選手権決勝 C - Maximize Minimum
お題箱より。同じ問題に2件リクエストがありました。
簡潔に飛ばし気味ではあるものの公式解説の説明が分かりやすいと思うので、流れは公式解説に沿いつつ図や行間を補って書いていきます。
解法
イチゴの最適な折り返し方
まずクエリについては忘れます。「ケーキを左半分と右半分で鏡写しのように見たときに、イチゴを何回でも反対側に折り返していいので、一番近いイチゴ間の距離をできるだけ遠くしてください」という問題です。
何回でも折り返していいので、まずは左半分の側に全てのイチゴをまとめて、それからいくつかのイチゴを選んで折り返す最適な選び方を考えてみましょう。イチゴの個数を として、左側に全てまとめたときのイチゴの座標を小さい方から とします。ここで です。
まず簡単なケースから考えましょう。以下のようにイチゴが間隔1で等間隔に並んでいて、ケーキが十分長いようなケースです。
このときの最適な並べ方は、 のイチゴを折り返すことです。つまり座標の小さいものから左右交互に配置していきます。
こうすることで一番近い距離が2になります。もし交互になっていないところがあると距離1が登場してしまうので、これが最適です。
この「全部片側に寄せて、座標の小さいものから左右交互」という考え方を実は一般化できて、このような置き方が常に最適になります。そのときのイチゴ間の距離は、 を用いると
- 1つ飛ばしでのイチゴの距離
- 最も右側(ケーキの中心)に近い2つのイチゴを、左右逆側に配置した時の距離
になります(2.を忘れがちなので注意ですね)。これらのうち最も小さい値が答えになります。
最適性の証明は公式解説の通りです。連続する3つのイチゴ を考えると、これらのうちどれか2個のペアは左右同じ側に置かないといけないので、 の少なくとも1つはイチゴ間の距離として採用しないといけません。それならば、この中で一番大きい を常に採用する左右交互の並べ方が最適ということになります。
2.の最も右側のイチゴ2つを左右同じ側ではなく左右逆側に配置したほうが常に得であることも、逆側に配置したときの距離 と同じ側に配置した時の距離 の大小を評価すれば分かります。( であることを使います)
クエリの処理を考える
ではクエリの処理方法を考えます。
まずは各クエリでイチゴを入れるのか除くのか判断するため、今あるイチゴの「元の」座標を管理する集合1を作ります。
それとは別に、折り返した座標である を管理できる集合2を作ります。鏡写しで同じ座標にイチゴが2個あることもあるので、多重集合のほうが適しています。
各クエリでは集合1を使って要素を出すのか入れるのか判断し、集合1, 2に要素を出し入れします。そして答えを求めるために「1つ飛ばしでのイチゴの距離」のうち最も小さいものを求める必要があります。これは以下のようにして求めることができます。
集合2を順序付き多重集合(C++の multiset
など)として持ち、常に要素がソートされているようにします。そうすると要素を1個挿入または削除したときには、「1つ飛ばしでのイチゴの距離」はその周囲でだけ変化します。具体的には当該要素の左側2つ・右側2つまでの座標だけ分かれば、変化する値は以下のように計算できます。
つまり1回のクエリは、「その時点での "1つ飛ばしでのイチゴの距離" たちの集合」に対する高々5個の要素の出し入れとして処理できます。これも multiset
として管理しておけば、各操作の後にその最小値を求めることができます。
multiset
において「ある要素の2つ左の要素」などを求めるには、いったん lower_bound
などでその要素の場所を求めた後にイテレータ操作をすれば良いです。順序付き集合が標準で存在しない言語の場合は、自前の平衡二分探索木を使うか座標圧縮+セグメント木+二分探索などの処理が必要になると思われます…。
これで1つ飛ばしでのイチゴの距離の最小値が分かります。また集合2の大きいほうから2要素を見ることで も計算できます。これで各クエリに で答えることができて間に合います。
ACコード
Submission #7801071 - 第一回日本最強プログラマー学生選手権決勝(オープンコンテスト)
実装も、「慣れ」が必要なタイプだと思います。特に集合2の操作時に周囲の要素を見るときは、それが端っこだったりすると場合分けが面倒なので番兵などを使っていますが…読む分には逆に謎の値がいきなり登場して分かりにくいかもしれません。
Kodamanと愉快な仲間たち S - 五等分の分銅
お題箱より。
Programming Problems and Competitions :: HackerRank
解法
分数のままでは考えにくいので、通分します。 の最大公約数は なので、まず重りを全て 倍して とします。そして目標の重さも に置き換えます。するとこの問題は「重さ の重りをそれぞれ 個以下使って合計 を作る場合の数を求めよ」という問題と見なせます。もし が整数にならない場合は0通りです。
これは「部分和問題」と呼ばれるもので、特に今回は
- 個数制限付き:各要素が何個まで使えるか指定されている
- 数え上げ: が作れるかどうかの判定だけでなく、その作り方の場合の数を求める必要がある
という特徴があります。
これを公式解説のDPで解きます。以下のようなDPテーブルを定義します。
- 重り までの使い方を決めて、その合計が であるような重りの使い方の場合の数
この遷移を具体例で説明します。重り の重さが で、これが 個まで使えるとします。このときの から からの更新方法を考えます。
更新の順番として、 ごとに更新をしていくという方法を取ります。つまり
- をこの順に更新する。
- をこの順に更新する。
- …
- をこの順に更新する。
という流れです。
いま重さ の重りを 個まで使える場合を考えているので、 に遷移できるのは の3点からです。例えば を更新する時には、以下の図のような遷移になります。
つまり遷移元が一定の長さを保ったままスライドしていくので、例えば の遷移元と の遷移元の違いは両端だけです。このように範囲に入るものと出るものを差分更新することで、遷移元の値を効率的に管理することが出来ます。(解説の「しゃく取り法っぽく」はこれを指しています)
このようにすれば重り1種類についての遷移の計算量は です。 で分類しているだけで結局は遷移先の を1回ずつ見ていることになり、遷移元の計算は差分更新で毎回 でできているからです。重りの種類数を として全体計算量は になります。今回の問題では であり、 は制約より600万以下です。
ACコード
HackerRankは提出が非公開らしいので、ベタ書きします。
#include <bits/stdc++.h> using namespace std; int main(){ int A[5], P, Q; for(int i=0; i<5; i++) cin >> A[i]; int W[5] = {60, 30, 20, 15, 12}; cin >> P >> Q; if(60%Q){ cout << 0 << endl; return 0; } int S = P*60/Q; static int64_t dp[6][6000001]; dp[0][0] = 1; for(int i=0; i<5; i++){ for(int r=0; r<W[i]; r++){ int64_t sum = 0; for(int s=r; s<=S; s+=W[i]){ sum += dp[i][s]; int out = s - (A[i]+1)*W[i]; if(out >= 0) sum -= dp[i][out]; dp[i+1][s] = sum; } } } cout << dp[5][S] << endl; return 0; }
AtCoder Beginner Contest 054 D - Mixing Experiment
お題箱より。
半分全列挙解法を…とのリクエストだったので、そちらを書きます。
解法
個の薬品をグループ0・グループ1に分け、それぞれについて薬品の選び方を決めたとします。これらを混ぜてちょうど混合比 になっているかどうかを判定する式を立てましょう。
グループ で選んだ薬品に含まれるタイプAの薬品量を 、タイプBの薬品量を と表記することにします( は または )。両グループの薬品を混ぜて混合比が となる条件は
という数式で表現できます。これを通分すると
となりますが、さらにこれを移項して並び替えると
となります。このように変形した目的は、グループ0に関する値とグループ1に関する値に分離するためです。
つまり両グループで選んだ薬品を混ぜて欲しい混合比になる条件は、グループ に対して と定義すると、 という数式で表現できます。これはつまり、それぞれのグループで薬品の選び方を決めた時の具体的な薬品量は忘れてしまって良くて、 の値さえ覚えておけば判定ができるということです。
求めたいのは欲しい混合比になるための最小価格でした。なのでそれぞれのグループごとに選び方を全列挙して、以下の値を求めます。
グループ の薬品を1つ以上選んで、その薬品量 について の値が となるときの、最小の合計価格。ただしそのような選び方が存在しない場合はINFとする。
1つ以上という点に注意しないといけない理由は、この条件を考慮しないと両グループともに薬品を1つも選ばないケースが含まれてしまうからです。薬品を1つも選ばないと は必ず満たされてしまうので、合計価格0が必ず答えに出てきてしまいます。
欲しい混合比を作れる最小の価格は、以下の3パターンのうちの最も小さいものになります。ただし全ての結果がINF以上になってしまった場合は、ほしい混合比を作れる薬品の選び方は存在しません。
- グループ0に属する薬品だけで作る場合。その価格は である。
- グループ1に属する薬品だけで作る場合。その価格は である。
- 両グループの薬品をともに1つ以上選ぶ場合。その価格は取りうる全ての に対する の最小値である。
これで解くことができました。ポイントは判定式を立てた後に項をグループごとに分離する、以下の形への式変形ですね。
ACコード
において が負になるため、実装上は負のインデックスを回避しないといけません。最小値が0以上になるように固定値を足して管理する(オフセット)か、mapを使うかのどちらかが良いと思います。mapを使うと計算量に が追加で乗る点には注意しましょう。
Codeforces Round #587 (Div. 3) F. Wi-Fi
お題箱より。
セグメント木を使わない解法を…とのリクエストだったので、何通りか解法を紹介します。
問題概要
個の部屋が並んでいて、順に番号 が付けられている。これらの部屋全てにインターネット回線を提供したい。
回線提供には2つの方法がある。
- それぞれの部屋 について、 コイン払うことでその部屋に単独でインターネット回線を提供できる。
- いくつかの部屋は無線ルーターを持っていて、その部屋 については コイン払うことで区間 に含まれる全ての部屋にインターネット回線を提供できる。
整数 と各部屋の無線ルーターの所持有無が与えられるので、全ての部屋にインターネット回線を提供するために払う合計コインの最小値を求めよ。
制約
解法1:セグメント木
この問題は結局、
(区間, その区間を使うコスト)の組がいくつか与えられるので、整数点 全てを覆う最小コストを求めよ。
という問題です。この問題をパターンとして捉えセグメント木で処理するのがたぶん一番楽です。
を「整数点 が全て覆われるための最小コスト」とします。予め区間を右端で分類しておき、 を更新するときには を右端とするような区間それぞれに対して、
- その区間の左端を 、コストを とするとき、 で を更新(今の値より小さければ採用)する
とすれば良いです。少なくとも までが覆われている状態を遷移元とすることで、区間 を新たに採用して までを覆うことができます。
区間最小が取れるセグメント木にDPの値を入れることで高速に の計算ができます。セグメント木のクエリ1回が なので全体計算量は です。
この解法は後に紹介するようなこの問題特有の単調性が無くても使えるので、そういった意味でもこの解法を知っておくのがオススメです。
ACコード1
Submission #60990629 - Codeforces
解法2:スライド最小値
ルーターが覆う区間の長さは左右にはみ出る場合を除けば一定で、ルーターを置く部屋を右にずらしていくと左端も右端も単調に変化します。このような場合、先ほどの の計算はスライド最小値という方法を用いて高速化できます。この場合、計算量はDP全体でならし となります。
ルーターを使わない場合の遷移は単に1つ前の1点だけが遷移元になるので各点ごとに で計算できます。結局全体で になります。
ACコード2
Submission #61186419 - Codeforces
解法3:コストの単調性を使ってgreedy
リクエスト的にはこれが本命ですかね。このコメント を参考にしました。今回の場合はコストが右の部屋にいくほど高くなっているので、DPの遷移をgreedyに取ることができます。
DPを左(添え字の小さい方)から回すのではなく、右から回します。 を「整数点 が全て覆われるための最小コスト」と定義します。
から 方向への遷移を考えます。つまり右から までが覆われているので、次に採用する区間で少なくとも は覆う必要があります。
1つの方法は単に部屋 単独で回線を引くことです。そしてルーターに関しては、実は部屋 をカバー範囲に含むルーターのうち最も左にあるものだけを考えれば十分です。これがまだカバーされていない範囲を最も遠くまでカバーし、加えて左側の部屋のルーターを使ったほうがコストが安いので、「右から までは既に覆われている」という前提のもとでは完全に上位互換になっているからです。
※もしDPを左から回すと、ここで「遠くまでカバーするものほどコストが高い」という状態になり、1つに確定できません。
結局 からの遷移は、
- 単独回線を使い に遷移する。
- 部屋 をカバー範囲に含むルーターのうち最も左のものを使い、その左端を として に一気に遷移する。
2つだけを考えれば十分です。これで全体 でのDPができます。
このDPでは選ばなかったルーター区間について遷移を飛ばしてしまうため、厳密には各点において「整数点 が全て覆われるための最小コスト」になっていません。そのため正当性が少し理解しづらいのですが、各ステップで他と比べて必ず損をするものを切り捨てているので最終的には正しい答えが得られます。
ACコード3
Codeforces Round #587 (Div. 3) E2. Numerical Sequence (hard version)
問題概要
正整数 に対して、 を十進表記で書いたものをこの順に連結した文字列を とする。例えば "1234567891011"
である。
そして、 をこの順に連結した無限の長さの文字列を考える。与えられる 個の整数 に対して、この文字列の 番目の文字を答えよ。
制約
解法
各クエリに対する を単に と記します。また、問題概要に記したように文字列 を定めます。
まず、 番目の文字がどの に属するか調べる必要があります。これは の長さの合計を とすると、 を満たす最小の として求められるので二分探索できます。判定問題を解くために、ある固定した値 に対する の効率的な求め方を考えましょう。
の桁数を として、 なる桁数 を考えます。整数 に含まれる 桁の値を抜き出します。この最小値、最大値、値の個数をそれぞれ と書くことにすると、これらは以下のように計算できます。
やりたいことは文字列 の長さ合計を求めることです。これを について合計したものが になります。
ある についての計算を、さらに分けて考えます。 なる桁数 について、文字列 の中に 桁の整数が合計でいくつ入っているかを求めます。
- のとき、 それぞれに 桁の整数全てが1つずつ含まれています。 桁の整数は全部で 種類あるので、合計で 個の整数があります。
- のとき、 桁の整数は に1個、 に2個、...、 に 個含まれています。つまり等差数列の公式から、合計で 個の整数があります。
これに を掛けたものを全ての について合計すると の長さ合計が求められます。それをさらに について合計したものが になります。
これを用いて二分探索することで、 番目の文字がどの に属するか求められました。ここで求められた値を引き続き と記します。ここで とすると、求めたいのは の 番目の文字です。
ここで知りたいのは として並べられた整数 のうちどれに 番目の文字が含まれているかで、これはまたしても二分探索することで求められます。つまり 番目の文字を含むのは、 を満たす最小の です。
の計算方法は先ほどの に比べると簡単で、 の中に 桁の数がいくつ含まれるかを同じように計算し、それに を掛けたものを全て合計すれば良いです。
これで も特定できれば、その十進表記における 番目の数字が求める文字です。
ACコード
Submission #61031718 - Codeforces
計算量ですが、 が小さいので10の冪乗などの前計算などをしておかなくても余裕があります。二分探索・桁数 の全探索・10の冪乗で が4つくらい付いていると思います。
またオーバーフローに注意する必要があります。二分探索の初期値としてあまり大きな値を取りすぎると の値がオーバーフローしますが、実際に計算すると がだいたい になって の制約を超えるので、 の上側の初期値を としておくと良いでしょう。
AtCoder Grand Contest 038 B - Sorting a Segment
公式解説と少し違う方法で解いたので記録。
解法
ある長さ の区間を昇順に並び替えた時に、値が変更される最左のインデックスを 、最右のインデックスを とします。この時操作の結果は、閉区間 内の要素を昇順に並び替えたものとなります。つまりこの区間 は「操作によって実質的に並び替えられたのはどの区間か」というものを表します。
求めたい答えは、 通りの全ての操作パターンについてこのようなインデックスの組 を求めた時の、それらの相異なる要素数となります。ただし1つも要素が変更されない場合もあるので、その場合は適当に などとしておきます。(開区間と紛らわしいので値の組は と表記します)
では各操作パターンについて、長さ の区間 を操作したときの はどのように求めれば良いでしょうか。左右同様の方法で求められるので について考えます。
左端から見て、要素が変更されていない区間 に注目します。この区間に属する全ての値 が満たすべき条件は、 より小さい要素が区間 に存在しないことです。
このような条件を満たす区間のうち最も長いものを求めることができれば、その区間を として の値を求めることができます。そしてこれは二分探索で求めることができます。
具体的には、あらかじめ各インデックス について「 より小さい要素が右側で次に現れるのはどこか」を求めておきます(存在しない場合は適当に大きな数とします)。この値を としましょう。
そうすると区間 の値が変更されない必要十分条件は、区間内の全ての について が区間 の外にあること、つまり
が成立することなので、 の値をSparse Tableやセグメント木などに入れておくことで効率的にこの判定が出来て、二分探索をすることができます。
また の値も、 そのものをSparse Tableに入れて二分探索したり、 を右から見ていきながらセグメント木に入れていくことで求めることができます。
についても、左/右や小さい/大きいを逆にして考えると同様に計算できます。これで各操作に対する の値が求められるので、 値の組 をset等に入れて相異なる要素数を数えることで答えを求めることが出来ます。