Educational Codeforces Round 26
久しぶりにブログを書きます。
4完でした。D問題のDPを通せたのは個人的になかなか成長したのではないかと思える回でした。
A. Text Volume
問題概要
いくつかの単語で構成される文が与えられる。
各単語の音量はその単語に含まれる大文字の数で決まり、その文の音量はそれらの単語の音量の最大値で決まる。
与えられた文の音量を求めよ。
感想
これはそのままやるだけですね。強いて言うなら、最初大文字判定を'A' <= chみたいに書いててバグらせたのがアレでした。
asciiコードでは大文字が先に割り振られてるので'A' <= chだと小文字も全部カウントしちゃうんですね。
なので書くとしたらch <= 'Z'じゃないとダメでした。そもそも横着せずにちゃんと'A' <= ch && ch <= 'Z'と書いておけばよいですね。
D言語パワーを使ってスマートに書くとこんな感じ。
void main() {
readln;
readln.split.map!(w => w.count!(isUpper)).reduce!(max).writeln;
}
そもそもisUpperっていう大文字判定関数みたいなのがありました。
B. Flag of Berland
問題概要
R, G, Bのみで構成される旗が与えられる。
それがロシアとかイタリアみたいな国旗になっているか判定せよ。
感想
場合分けでゴリ押しました。結構めんどくさくて、めんどくさかったです。
丁寧に場合分けしないとコーナーケースとかで落とされて、割とHackで落とされた人が多かったみたいです。
長いだけなのでコードは省略します。
C. Two Seals
問題概要
a * bの土台となる長方形がある。n枚のシールを持ってて、シールiはx[i] * y[i]の長方形である。
この土台に2枚のシールを貼りたい。シールははみ出しちゃダメで、各辺は土台の辺と並行になるように貼らなければならず、2枚のシールが重なってもいけない。
このとき貼れる2枚のシールの面積の総和の最大値を求めよ。貼れる2枚が無いときは0とせよ。
制約
- 1 <= n, a, b <= 100
- 1 <= x[i], y[i] <= 100
感想
nが小さいので2つのシールの全ての組合せを試すO(n^2)解で十分間に合うのでそれをやりました。
1つのシールを左上隅に固定して貼ったとすると、2つの長方形を重ねたような多角形が残るので
そのどちらかの長方形に収まるようにもう一方のシールを貼れるか、みたいな判定をするみたいな。
int n, a, b; int[] x, y; void main() { scan(n, a, b); x = new int[](n); y = new int[](n); foreach (i ; 0 .. n) { scan(x[i], y[i]); } bool check(int U, int V, int u, int v) { return (u <= U && v <= V) || (u <= V && v <= U); } int ans; foreach (i ; 0 .. n) { foreach (j ; 0 .. n) { if (i == j) continue; if (x[i] <= a && y[i] <= b) { if (check(a - x[i], b, x[j], y[j]) || check(a, b - y[i], x[j], y[j])) { ans = max(ans, x[i]*y[i] + x[j]*y[j]); } } if (x[i] <= b && y[i] <= a) { if (check(a - y[i], b, x[j], y[j]) || check(a, b - x[i], x[j], y[j])) { ans = max(ans, x[i]*y[i] + x[j]*y[j]); } } } } writeln(ans); }
D. Round Subset
問題概要
n個の整数 a_1, ..., a_n が与えられる。
この中からk個選んで全ての積を取ったときの末尾の連続する0の個数を最大化したい。
その最大値を求めよ。
制約
- 1 <= k <= n <= 200
- 1 <= a_i <= 10^18
感想
まず、k個選んだときの末尾の0の個数がどうやって決まるかって話ですがこれは10で何回割れるかっていうことなので
各整数が2を何回含むか、5を何回含むかを前処理で求めておいて、選んだk個のそれらの値をそれぞれ足して、最後にminを取ることで求まります。
それで最大値問題ってことで最初は二分探索かなーとか思って考えてました。何か平均最大化問題みたいな感じで基準を定めてうんたらかんたらみたいな。
あれも「n個からk個とるとき」って感じだったのでそういうパターンかなって思ったのですがどうにも上手くいきそうにありませんでした。
それで次に考えたのがDPでした。「n個からk個とるとき」みたいな問題をDPで解いたことあったなあーとか思い出したので。
なんかちょっとぱっと出てこないんですがありますよね。dp[ここまで見た数][ここまでで選んだ数]みたいなDP問題。
ただこのまま
dp[ここまで見た数][ここまでで選んだ数] = (2の数, 5の数)
として、min(2の数, 5の数)が上回るとき更新、みたいにするとちょっとまずそうだなって思います。
例えば
3 2
4 10 25
みたいなケースで上手くいかなくなるはずです。
だからこのままではまずいけど、
dp[ここまで見た数][ここまで選んだ数][ここまでで取った5の数] = (2の数)
にして、2の数が大きくなるとき更新、にすると上手くいきそうっていうのは何となく分かるかなと思います。
別にこうじゃなくて
dp[ここまで見た数][ここまで選んだ数][ここまでで取った2の数] = (5の数)
としてもいいんですけど、状態数はなるべく少なくしたほうが計算量も少なくなるのでこれより上の奴のほうがいいと思います。
それで実際計算量は大丈夫なのでしょうか?
ここまで見た数 = 200,
選んだ数 = 200,
ここまで取った5の数 = 200 * 25
が最大ケースっぽいのでこれを掛け合わせると2 * 10^8くらいになるっぽいです。
Pythonだとまず間違いなくTLEしますが、Dの力を信じてサブミットするとあっさり300msぐらいで通っちゃいました。凄い速い。
実際DPテーブルを2 * 10^8作るとMLEしそうなので、1個目の変数は1個前しか参照しないから2個でいいっていう奴をやってます。
int n, k; long[] a; void main() { scan(n, k); a = readln.split.to!(long[]); auto two = new int[](n); auto five = new int[](n); foreach (i ; 0 .. n) { while (a[i] % 2 == 0) { a[i] /= 2; two[i]++; } while (a[i] % 5 == 0) { a[i] /= 5; five[i]++; } } int lim = n * 25; auto dp = new int[][][](2, k + 1, lim + 1); fillAll(dp, -1); dp[0][0][0] = 0; foreach (i ; 0 .. n) { foreach (j ; 0 .. k + 1) { foreach (f ; 0 .. lim + 1) { if (dp[i & 1][j][f] == -1) continue; dp[(i & 1) ^ 1][j][f] = max(dp[(i & 1) ^ 1][j][f], dp[i & 1][j][f]); if (j + 1 <= k) { dp[(i & 1) ^ 1][j + 1][f + five[i]] = max(dp[(i & 1) ^ 1][j + 1][f + five[i]], dp[i & 1][j][f] + two[i]); } } } } int ans; foreach (f ; 0 .. lim + 1) { ans = max(ans, min(f, dp[n & 1][k][f])); } writeln(ans); }
AOJ/Atcoder-JOI
何かここの難易度5 ~ 6ぐらいに良い感じのDP問題がたくさんあるのでオススメです。
これで少しDPに慣れていたおかげでこの問題が解けたかなと思ってます。
E. Vasya's Function
問題概要
f(a, 0) = 0
f(a, b) = f(a, b - gcd(a,b)) + 1 (b > 0)
という関数がある。f(x, y)の値を求めよ。
制約
- 1 <= x, y <= 10^12
感想
少し言い換えると「(a, b) -> (a, b - gcd(a, b))という操作がbが0になるまで何回出来るか?」という問題になることが分かります。
最悪実際にシミュレーションするって解がありますがx, yがめちゃくちゃでかくなるので明らかにTLEしそうでやりたくないですよね。
x = 1だったらy回シミュレーションしなきゃいけないわけだし……
それでaが素数のときはf(a, b) = a / b + a % bで~とか考えましたが結局全然分からなかったので答えを見ました。
操作で変化するのはbだけで、古いgcd(a, b)は新しいgcd(a, b)に必ず含まれているってのがポイントらしかったです。
まずaに含まれる互いに異なる素因数を列挙しておいて、
次にg = gcd(a, b)を求めて、このgcdが変化するときっていうのはaに含まれる素因数を新たに巻き込むときってことなので
そこまでにかかるステップ数を求めて、最後にそれらの最小値を取って、答えに加算して、また最初に戻って……
ってのを繰り返すみたいです。難しいですね。
計算量的には上の一回のループがaに含まれる異なる素因数分かかって、ループする回数はgcdが倍倍倍で増えていくからlogぐらいでみたいな感じであんまりかからなさそうです(雑計算)
結局最初にaの素因数分解にかかる時間が一番大きいような気がします。
import std.stdio, std.string, std.conv, std.algorithm; import std.range, std.array, std.math, std.typecons, std.container, core.bitop; import std.numeric; immutable long inf = 10L^^13; long x, y; void main() { scan(x, y); auto xf = factrize(x); debug { writeln("xf:", xf); } long ans; while (y > 0) { long g = gcd(x, y); if (g == x) { ans += y / g; break; } long m = inf; foreach (p ; xf) { long mp = (y - (y / (p * g)) * p * g) / g; if (0 < mp && mp < m) { m = mp; } } ans += m; y = y - m * g; } writeln(ans); } auto factrize(long x) { auto res = new long[](0); for (long p = 2; p*p <= x; p++) { if (x % p == 0) { res ~= p; while (x % p == 0) { x /= p; } } } if (x > 1) { res ~= x; } return res; }