C - 積み木崩し / Toppling Blocks 解説 by admin
gemini-3.5-flash-high概要
この問題は、一列に並んだ積み木に対して「右隣の積み木が自分より高い場合に、左隣の積み木を取り除く」という操作を繰り返したとき、最終的に残る積み木の個数を求める問題です。
一見複雑なシミュレーションが必要に見えますが、「右から左へ」 順に考えていくことで、非常にシンプルな規則(スタックを用いた一方向の走査)で解くことができます。
考察
なぜ素朴なシミュレーションではダメなのか?
問題文の指示通りに、毎ラウンド配列を走査して要素を削除するシミュレーションを行うと、最悪の場合で \(O(N^2)\) の時間がかかってしまいます。 本問題では \(N \le 10^6\) と非常に大きいため、愚直なシミュレーションでは実行時間制限(TLE)になってしまいます。そのため、より効率的な法則を見つける必要があります。
重要な観察:右から左への「ドミノ倒し」
この問題の操作は、「右側にある高い積み木が、その左側にある自分より低い積み木を次々と消し去っていく」 という挙動をしています。
具体例として、積み木の高さが \(H = [3, 2, 4, 1]\) の場合を考えます。
- 一番右の \(1\): 右隣に積み木がないため、誰からも消されることはなく、必ず生き残ります。
- 右から2番目の \(4\): 右隣の \(1\) より大きいため、消されずに生き残ります。さらに、この \(4\) は左側にある自分より低い積み木(\(2\) や \(3\))を次々と消し去る力を持っています。
- 右から3番目の \(2\): 右側にある \(4\) よりも低いため、いずれ \(4\) に押し流されて(消されて)しまいます。
- 一番左の \(3\): これも右側にある \(4\) より低いため、同様に消されてしまいます。
このように、右から左へと見ていったとき、「それまでに生き残ることが確定した積み木(の高さの基準)」以上の高さを持つ積み木だけが生き残ることができます。
生き残る条件の整理
右から左へ順に見ていきます。 - 現在の積み木の高さを \(x\) とします。 - これまでに生き残ることが確定した、最も左にある積み木の高さを \(S\) とします。 - \(x \ge S\) であれば、右側のどの積み木も \(x\) を消すことはできません。したがって、 \(x\) は生き残ります。このとき、新たな基準は \(x\) に更新されます(\(S \leftarrow x\))。 - \(x < S\) であれば、 \(x\) は右側にある \(S\) にいずれ消されてしまうため、生き残りません。
この規則を適用すると、右から左へ1回走査するだけで、最終的に生き残る積み木をすべて特定できます。
具体例でのシミュレーション
\(H = [3, 2, 4, 1]\) を右から(逆順に)見ていきます。
- \(1\) を見る: スタックは空なので、生き残り確定。スタックに追加。
- スタック:
[1]
- スタック:
- \(4\) を見る: スタックの末尾(\(1\))以上なので、生き残り確定。スタックに追加。
- スタック:
[1, 4]
- スタック:
- \(2\) を見る: スタックの末尾(\(4\))未満なので、消滅。スタックには追加しない。
- スタック:
[1, 4]
- スタック:
- \(3\) を見る: スタックの末尾(\(4\))未満なので、消滅。スタックには追加しない。
- スタック:
[1, 4]
- スタック:
最終的にスタックに残ったのは [1, 4] の \(2\) 個となり、これが答えです。
アルゴリズム
スタック(Pythonではリスト)を用いて、以下の手順で解くことができます。
- 空のスタック
stackを用意する。 - 入力配列 \(H\) を末尾(右端)から先頭(左端)に向かって逆順に走査する。
- 各要素 \(x\) について:
stackが空、またはstackの末尾の要素が \(x\) 以下である場合:- \(x\) を
stackに追加する。
- \(x\) を
- そうでない場合(\(x\) が
stackの末尾の要素より小さい場合):- 何もしない(無視する)。
- 最終的な
stackの要素数(長さ)を出力する。
計算量
- 時間計算量: \(O(N)\) 配列を逆順に1回だけ走査するため、要素数 \(N\) に対して線形時間で処理が終了します。\(N = 10^6\) でも 0.1 秒未満で高速に動作します。
- 空間計算量: \(O(N)\) 入力データの保持および、最悪の場合(すべての積み木が生き残る場合など)にスタックに最大 \(N\) 個の要素を格納するためです。
実装のポイント
高速な入出力: Pythonで \(N = 10^6\) の入力を処理する場合、
input()を繰り返すと入出力がボトルネックになり TLE になる可能性があります。sys.stdin.read().split()を使用して、すべての入力を一括で取得することで高速化しています。逆順の走査:
reversed(H)を使用することで、元の配列を書き換えることなく、効率的に後ろから要素を取り出すことができます。ソースコード
import sys
def solve():
input_data = sys.stdin.read().split()
if not input_data:
return
N = int(input_data[0])
H = [int(x) for x in input_data[1:]]
stack = []
for x in reversed(H):
if not stack or stack[-1] <= x:
stack.append(x)
print(len(stack))
if __name__ == "__main__":
solve()
この解説は gemini-3.5-flash-high によって生成されました。
投稿日時:
最終更新: