ABC303 D - Shift vs. CapsLock [2次元配列を使う or 多重代入]
素直にDP(動的計画法)で解いていけばいい問題ではあるけれど、油断するとハマってしまうので気を付けたいというお話。(自分がやらかしたのでその記録ということで...)
(問題へのリンク)
問題要約- "A"と"a"からなる文字列
が与えられる。S - 以下のの3つの操作を繰り返すことで文字列
を入力したい。S -
を入力するのにかかるコストの最小値は?S
【操作】
- CapsLock がon[off] なら"A" ["a"]を打ち込む。コストは
。X - CapsLock がon[off] なら"a" ["A"]を打ち込む。コストは
。Y - CapsLock がon[off] ならCapsLockをoff [on]にする。コストは
。Z
制約
1\leq |S| \leq 3 \times 10^{5} 1\leq X, Y, Z \leq 10^{9}
解法
公式解説のページはこちらです。
|
|
|
---|---|---|
CapsLockがOnになっている | 操作1 or 操作3→操作2 | 操作2 or 操作3→操作1 |
CapsLockがOffになっている | 操作2 or 操作3→操作1 | 操作1 or 操作3→操作2 |
これより、
そこで、
-
dp[i][0]
: まで打ち込んだ後にCapsLockがonとなっているようにするための最小コスト。S_{i} -
dp[i][1]
: まで打ち込んだ後にCapsLockがoffとなっているようにするための最小コスト。S_{i}
として、上の表のような遷移をさせていくDPで解きます。
dpの生成と初期化
以下、Rubyで実装します。
大きさが nil
としておきます。また、dp[0][0]
とdp[0][1]
は、それぞれ
dp = Array.new(S.size+1) {Array.new(2)}
dp[0][0] = Z
dp[0][1] = 0
dpの更新
上の表に基づいて、dp
を更新します。各操作でCapsLockの状態が変わるかに気を付けます。また、S[i]
が
(S.size).times do |i|
if S[i] == "A"
dp[i+1][0] = [dp[i][0]+X, dp[i][1]+Z+X].min
dp[i+1][1] = [dp[i][0]+Z+Y, dp[i][1]+Y].min
elsif S[i] == "a"
dp[i+1][0] = [dp[i][0]+Y, dp[i][1]+Z+Y].min
dp[i+1][1] = [dp[i][0]+Z+X, dp[i][1]+X].min
end
end
出力
dp[|S|][0]
とdp[|S|][1]
のうち小さい方が答えとなります。dp[|S|]
はdp[-1]
でよいです。
puts dp[-1].min
全体のコード
通して書くと次のようになります。
X, Y, Z = gets.split.map(&:to_i)
S = gets.chomp
dp = Array.new(S.size+1) {Array.new(2)}
dp[0][0] = Z
dp[0][1] = 0
(S.size).times do |i|
if S[i] == "A"
dp[i+1][0] = [dp[i][0]+X, dp[i][1]+Z+X].min
dp[i+1][1] = [dp[i][0]+Z+Y, dp[i][1]+Y].min
elsif S[i] == "a"
dp[i+1][0] = [dp[i][0]+Y, dp[i][1]+Z+Y].min
dp[i+1][1] = [dp[i][0]+Z+X, dp[i][1]+X].min
end
end
puts dp[-1].min
別の実装
さて、動的計画法で解けたわけですが、今回はdp[i+1]
を得るためにdp[i]
の情報しか用いません。それを踏まえて次のような実装を考えます。
- はじめ、
t=Z
、u=0
とする。 -
の文字を先頭から1文字ずつ見ていき、S - 見た文字が"A"ならば
t
をt+X
とu+Z+X
のうち小さい方に、u
をt+Z+Y
とu+Y
のうち小さい方にそれぞれ置き換える。 - 見た文字が"a"ならば
t
をt+Y
とu+Z+Y
のうち小さい方に、u
をt+Z+X
とu+X
のうち小さい方にそれぞれ置き換える。
- 見た文字が"A"ならば
t=dp[i][0]
、u=dp[i][1]
としているわけです。1はdp[0][0]=Z
、dp[0][1]=0
とした部分に相当します。2はdp
の更新に相当します。ただし、更新するごとに更新前の情報を捨てています。
失敗例
上記の方針を基に、次のように実装してみます。するとWAが出ます。(こんな感じで私はやらかしました。)
X, Y, Z = gets.split.map(&:to_i)
S = gets.chomp.split('')
t = Z
u = 0
S.each do |s|
if s == "A"
t = [t+X, u+Z+X].min
u = [t+Z+Y, u+Y].min
elsif s == "a"
t = [t+Y, u+Z+Y].min
u = [t+Z+X, u+X].min
end
end
puts [t, u].min
問題はt
とu
の更新の部分です。この実装だと、先にt
を更新し、更新されたt
を用いてu
を更新しているため間違いとなります。
失敗例の改善
この問題を回避するには多重代入を利用すればよいです。そうすることで、t
とu
を同時に更新でき、問題を解消できます。
X, Y, Z = gets.split.map(&:to_i)
S = gets.chomp.split('')
t = Z
u = 0
S.each do |s|
if s == "A"
t, u = [t+X, u+Z+X].min, [t+Z+Y, u+Y].min
elsif s == "a"
t, u = [t+Y, u+Z+Y].min, [t+Z+X, u+X].min
end
end
puts [t, u].min
まとめ
ABC303 D - Shift vs. CapsLockの解法を考えてきました。素直に2次元配列を使ったDPで解けば問題ありません。しかし、2次元配列を使わずに解こうとすると、値の更新の仕方を気を付けなければやらかすという話です。
(ちなみに、「別の実装」の方がスピードは速いです。この問題ではどちらのコードでも余裕で制限時間に間に合いますが...)
教訓
- DPは素直に多次元配列を使おう
- 複数の変数を同時に更新したいときは多重代入を効果的に使おう
Discussion