東京海上日動 プログラミングコンテスト2020 C問を解いた
はじめに (ネタバレ無し)
こんにちは
ひぽたふです。
解きながらの考察、実際の解法をまとめておこうかなと思います。
今回解いた問題はこれです。
水色diffの問題です。
本番中に沼にどハマりしました。
以下はネタバレがあるので見る際は注意してください。
解きながらの考察 (ネタバレ 方針まで)
ここからはネタバレがあります。 問題を解いている際に考えていたことをまとめておこうかなと思います。
問題概要
電球がN個並んでいる。
電球は座標にあってに光ってる。
座標に光の強さの電球があるとき、その電球は ~ までの区間を照らす。
(両脇個の電球を照らす感じだね。)
以下の操作を回繰り返す。
- 1以上以下の各整数に対し、操作時に座標を照らしている電球の個数をとする。そして、各電球の光の強さをに変更する。
(操作時に、自身を照らしている電球の数で次に光る って感じだね。)
回の操作後、各電球の光の強さを求めよ。
考察
まずは素直に考えてみようかな。
1回目の操作を考えたとき、各電球がどれを照らしているかを考えればいいかな?
とするとかかるから、回の操作も考慮するとだね。
どれを照らしているかはいもす法を使って数えるのが典型かな。
それでだね。
まだこれだと1010以上にかかっちゃうからダメそうだね。
(ここからしばらく沼にハマりました。。。)
各電気が回の操作後にどうなってるかをで求めらると良さそうだよね。
とりあえずが小さい時から考えてみようかな。
・・・。
もしかして、電気の明るさは今よりも暗くなることはないのかな・・・?
直感的に、自分を照らしている遥か遠くの電球が影響しそうだよね。
0, 0, 0, 0, 0, ..., 100, 101 (N = 101)
とかの場合を考えてみるよ。
次の明るさは、
2, 2, 2, 2, 2, ..., 2, 2
になるね。
最後の二つは減っちゃうね。
他は増えて、おそらく今後も増え続けていくと思う。
最初に照らされている数がどうにも怪しい感じがするね。
(まだまだ沼は続きます。。。)
x回目の操作時にそれぞれの電球はどうなるんだろう。
1つ目の電球に注目したとき、1回目の操作時には絶対に1以上になるね(自身に照らされてるから)
2回目の操作時には、隣の電球が1以上になってるはずだから絶対に2以上になるね。
2つ目の電球に注目すると、2回目操作時は両脇が1以上になってるはずだから3以上になるね。
回目の操作時には、各電球が以上にはなってそう。
なんとなく規則性が見えてきた・・・かな?
それでも全くわからないよおおおお。。。
この後も沼にハマり続け、いもす法で実装したでシミュレートしていると・・・。
あれ・・?
回も操作しないうちに全部の値がになってる。
適当にを大きい値で試すと、指数関数的に必要な操作回数が減ってる事を発見!
結局、全部同じ値になったら操作を打ち切って、いもすそのままでAC!
あとがき + 解法(ネタバレ全開)
いもす法を使って、各操作ごとの電球の状態をで求めました。
いもす法は、範囲加算を考えたい時に使う手法ですね。
下図のように、範囲の始点に加算したい値を加算、終点 + 1に加算したい値を減算して、一番最後に累積和を行うと求めたい答えになっているというアルゴリズムです。
一度の範囲加算で2回しか操作がないので、計算量はになりますね。
問題の話に戻りますがを回行うので、このままだとかかりそうです。
ですが、実は電球の状態が収束するまでにかかる操作回数は非常に少ないです。
電球が個しかない事から、あり得る最大の明るさはである事がわかります。
また、を小さい値でシミュレートしてみると全ての電球が最大値に収束する事がわかると思います。
その状態に何回でなるのかをシミュレートしてみます。
すると、を大きくしても収束にかかる操作回数はほとんど増えない事がわかります。
を104くらいにしても29回で収束したので、計算量も余裕を持って間に合うと判断しました。
ソースコード
int main() { ll input(n, k); vector<ll> a(n); REP(i, n) cin >> a[i]; REP(j, k) { // --- いもす ここから vector<ll> table(n + 1, 0); REP(i, n) { // 範囲外を照らしている光を考えないようにする table[max(0ll, i - a[i])] += 1; // 範囲始点 table[min(n, i + a[i] + 1)] -= 1; // 範囲終端 } // 累積和 vector<ll> cumsum(n + 1, 0); partial_sum(table.begin(), table.end(), cumsum.begin()); // --- いもす ここまで REP(i, n) a[i] = cumsum[i]; // 電球が全てNの明るさになっていたらストップ ll sum = accumulate(a.begin(), a.end(), 0ll); if (sum == n * n) { cout << j << endl; break; } } REP(i, n - 1) { cout << a[i] << " "; } cout << a[n - 1] << endl; }
マクロに関しては別の記事でまとめてあります。
REPと入出力のみ使用しています。
REPは半開区間となっています。
https://atcoder.jp/contests/tokiomarine2020/submissions/14248610
いもす法の計算途中、電球の無い場所を照らしている電球が存在します。
範囲外の光は何にも影響を及ぼさないのでmaxやminで切り落としています。
終端の確認は、中身の合計がになっていたら止めています。
個の電球がそれぞれに光っているかどうか、ですね。
電球達の最小値がになっているか、などここに関しては色々方法があると思います。
それでは、またっ!