paiza プログラミング

[Ruby]paiza DPメニュー 部分列

dpメニュー5_部分列

こんにちは!じゃいごテックのあつしです。

今回はpaiza DPメニュー 「セクション5【部分列】 STEP1, FINAL」 を解説します。
DPメニュー動的計画法(Dynamic Programming, DP)に関する6個のセクションで構成されています。

セクション5【部分列】は、配列からいくつかの要素を取り出して、隣り合う数列の値が常に大きい(または常に小さい)区間のうち、最長であるものの長さを求める問題です。
※ 部分列:飛び飛びはOKですが、要素の順番を変えてはいけません。

STEP問題 を解いてみる

簡単な解説は付けていますが、難しいと感じたら下記の記事も参考にしてみて下さい。

STEP1: 最長部分増加列 (paizaランク B 相当)

n本分の木の長さの配列aが与えられるので、最長の増加部分列の長さを求める問題です。

解答例
INPUT1 = <<~"EOS"
  5
  100
  102
  101
  91
  199
EOS
OUTPUT1 = <<~"EOS"
  3
EOS

def solve(input_lines)
  # 入力受け取り
  n, *a = input_lines.split("\n").map(&:to_i)

  # dpテーブル初期化
  dp = [1] * n

  # dpテーブル更新
  1.upto(n - 1) do |i|
    0.upto(i - 1) do |j|
      if a[j] < a[i]
        # 最後が木 j であるような増加部分列の末尾に木 i をくっつける
        dp[i] = [dp[i], dp[j] + 1].max
      end
    end
  end

  # 最大値を返す
  dp.max
end

puts solve(STDIN.read)

# 確認用コード
# puts solve(INPUT1)
# > 3
  • 整数n, 配列aを受け取る。
  • dp[1] * nを代入する。
    (要素単体での最長は1)
  • i = 1 ~ n-1で繰り返し処理を設定する。
    • j = 0 ~ i-1で繰り返し処理を設定する。
      • a[j] > a[i] なら dp[i] を [dp[i], dp[j] + 1].max で更新する
  • dpの最大値(増加部分列が最大になる区間の長さ)を返す
入出力例1の処理

DP5-1-1

dp = [1] * 5 で初期化

単体での増加部分列は1


ここからi = 1 ~ n-1のループ

DP5-1-2

木[1]より手前の項について調べる (i = 1, j = 0)

  • 木[0]: dp[0] = 1 , 木[0](100) < 木[1](102) -> True

木[j] < 木[1] を満たし、最初に出現する最大増加部分列に 木[1] を追加する

dp[1] = dp[0](1) + 1 = 2

木[1]が末尾の増加部分列の長さは[0, 1]を選んだ時の2


DP5-1-3

木[2]より手前の項について調べる (i = 2, j = 0, 1)

  • 木[0]: dp[0] = 1 , 木[0](100) < 木[2](101) -> True
  • 木[1]: dp[1] = 1 , 木[1](102) < 木[2](101) -> False

木[j] < 木[2] を満たし、最初に出現する最大増加部分列に 木[2] を追加する

dp[2] = dp[0](1) + 1 = 2

木[2]が末尾の増加部分列の長さは[0, 2]を選んだ時の2


DP5-1-4

木[3]より手前の項について調べる (i = 3, j = 0, 1, 2)

  • 木[0]: dp[0] = 1 , 木[0](100) < 木[3](91) -> False
  • 木[1]: dp[1] = 2 , 木[1](102) < 木[3](91) -> False
  • 木[2]: dp[2] = 2 , 木[2](101) < 木[3](91) -> False

木[j] < 木[3] を満たし、最初に出現する最大増加部分列に 木[3] を追加する

条件を満たすものがないのでdp[3]の更新なし

木[3]が末尾の増加部分列の長さは[3]を選んだ時の1


DP5-1-5

木[4]より手前の項について調べる (i = 4, j = 0, 1, 2, 3)

  • 木[0]: dp[0] = 1 , 木[0](100) < 木[4](199) -> True
  • 木[1]: dp[1] = 2 , 木[1](102) < 木[4](199) -> True
  • 木[2]: dp[2] = 2 , 木[2](101) < 木[4](199) -> True
  • 木[3]: dp[3] = 1 , 木[3](101) < 木[4](199) -> True

木[j] < 木[4] を満たし、最初に出現する最大増加部分列に 木[4] を追加する

dp[4] = dp[1](2) + 1 = 3

木[4]が末尾の増加部分列の長さは[0, 1, 4]を選んだ時の3


  • 配列aで増加部分列が最大になる要素の組み合わせは [0, 1, 4](100, 102, 199)
  • 配列aで増加部分列が最大になる区間の長さは3

FINAL問題: 最長減少部分列 (paizaランク B 相当)

※ paizaレベルアップ問題集 DPメニューより

問題

n 本の木が横一列に並んでいます。左から i 番目の木を木 i と呼ぶことにします。木 i の高さは a_i [cm] です。

あなたは、何本かの木を伐採することによって、残った木を左から順に見ると高さが単調減少になっているようにしたいと考えています。つまり、残った木を左から 木 k_1, 木 k_2, ... , 木 k_m とすると、a_{k_1} > a_{k_2} > ... > a_{k_m} が満たされているようにしたいです。なるべく多くの木が残るように工夫して伐採する木を選んだとき、伐採されずに残る木の本数が最大でいくつになるか求めてください。

なお、最初から n 本の木が単調減少に並んでいる場合は、1本も伐採しなくてよいものとします。

入力される値

n
a_1
a_2
...
a_n

  • 1行目に、横一列に並んでいる木の本数 n が与えられます。
  • 続く n 行のうち i 行目では、木 i の高さ a_i が与えられます。

入力値最終行の末尾に改行が1つ入ります。

期待する出力

残った木を左から見ると高さが単調減少になっているように木を伐採したとき、伐採されずに残る木の本数の最大値を出力してください。

また、末尾に改行を入れ、余計な文字、空行を含んではいけません。

条件

すべてのテストケースにおいて、以下の条件をみたします。

  • 1 ≦ n ≦ 5,000
  • 1 ≦ a_i ≦ 1,000,000,000
  • a_i ≠ a_j (i ≠ j)
入力例1
5
109
110
108
103
100
出力例1
4
攻略ポイント

ポイント

  • 前の項より値が低いものを選択して最大の長さの列を作る。
問題を解く流れ

入出力例をコピペしてヒアドキュメントで変数に代入し、データを確認します。正しく受け取れていれば確認用コードは削除します。

INPUT1 = <<~"EOS"
  5
  109
  110
  108
  103
  100
EOS
OUTPUT1 = <<~"EOS"
  4
EOS

# 確認用コード
p INPUT1
# > "5\n109\n110\n108\n103\n100\n"
p OUTPUT1
# > "4\n"

続いて、問題を解くsolveメソッドを変数の下に定義し、入力データを受け取る処理を書きます。

def solve(input_lines)
  # 入力受け取り
  n, *a = input_lines.split("\n").map(&:to_i)

  # 確認用コード
  [n, a]
end

# 確認用コード
p solve(INPUT1)
# > [5, [109, 110, 108, 103, 100]]

データが正しく受け取れていれば、solve メソッドに処理を追加していきます。
※ 不等号の向きが変わるだけでSTEP1と同じ問題です。

    • 整数n, 配列aを受け取る。
    • dp[1] * nを代入する。
      (要素単体での部分列は1)
    • i = 1 ~ n-1で繰り返し処理を設定する。
      •  j = 1 ~ i-1で繰り返し処理を設定する。
        • もし a[j] > a[i] かつ dp[i] < dp[j] + 1 なら dp[i] を dp[j] + 1 で更新する
    • dpの最大値(減少部分列が最大になる区間の長さ)を返す
def solve(input_lines)
  # 入力受け取り
  n, *a = input_lines.split("\n").map(&:to_i)

  # dpテーブル初期化
  dp = [1] * n

  # dpテーブル更新
  1.upto(n - 1).each do |i|
    0.upto(i - 1).each do |j|
      if a[j] > a[i]
        # 最後が木 j であるような減少部分列の末尾に木 i をくっつける
        dp[i] = [dp[i], dp[j] + 1].max
      end
    end
  end

  # 確認用コード
  dp
end

# 確認用コード
p solve(INPUT1)
# > [1, 1, 2, 3, 4]
p solve(INPUT1) == OUTPUT1
# > false

dpテーブルの内容が良さそうなら、出力を整えれば完成です。
p solve(INPUT1) == OUTPUT1 -> true を確認するために、出力を文字列に変換して末尾に改行を加えます。

def solve(input_lines)
  # 入力受け取り
  n, *a = input_lines.split("\n").map(&:to_i)

  # dpテーブル初期化
  dp = [1] * n

  # dpテーブル更新
  1.upto(n - 1).each do |i|
    0.upto(i - 1).each do |j|
      if a[j] > a[i]
        # 最後が木 j であるような減少部分列の末尾に木 i をくっつける
        dp[i] = [dp[i], dp[j] + 1].max
      end
    end
  end

  # 減少部分列が最大になる区間の長さを文字列に変換し末尾に改行を追加
  dp.max.to_s << "\n"
end

# 確認用コード
p solve(INPUT1)
# > "4\n"
p solve(INPUT1) == OUTPUT1
# > true

入出力例での動作確認が出来ましたので、標準入力からのデータ受け取りに変更して、手動で動作確認をしたら提出します。
複数行のデータ受け取りなので STDIN.read を使います。(入力終了は Ctrl+D 又は Ctrl+Z)

解答コード
def solve(input_lines)
  # 入力受け取り
  n, *a = input_lines.split("\n").map(&:to_i)

  # dpテーブル初期化
  dp = [1] * n

  # dpテーブル更新
  1.upto(n - 1).each do |i|
    0.upto(i - 1).each do |j|
      if a[j] > a[i]
        # 最後が木 j であるような減少部分列の末尾に木 i をくっつける
        dp[i] = [dp[i], dp[j] + 1].max
      end
    end
  end

  # 減少部分列が最大になる区間の長さを文字列に変換し末尾に改行を追加
  dp.max.to_s << "\n"
end

puts solve(STDIN.read)
入出力例1の処理

DP5-f-1

dp = [1] * 5 で初期化

単体での減少部分列は1


ここからi = 1 ~ n-1のループ

DP5-f-2

木[1]より手前の項について調べる (i = 1, j = 0)

  • 木[0]: dp[0] = 1 , 木[0](109) > 木[1](110) -> False

木[j] > 木[1] を満たし、最初に出現する最大減少部分列に 木[1] を追加する

条件を満たすものがないのでdp[1]の更新なし

木[1]が末尾の減少部分列の長さは[1]を選んだ時の1


DP5-f-3

木[2]より手前の項について調べる (i = 2, j = 0, 1)

  • 木[0]: dp[0] = 1 , 木[0](109) > 木[2](108) -> True
  • 木[1]: dp[1] = 1 , 木[1](110) > 木[2](108) -> False

木[j] > 木[2] を満たし、最初に出現する最大減少部分列に 木[2] を追加する

dp[2] = dp[0](1) + 1 = 2

木[2]が末尾の減少部分列の長さは[0, 2]を選んだ時の2


DP5-f-4

木[3]より手前の項について調べる (i = 3, j = 0, 1, 2)

  • 木[0]: dp[0] = 1 , 木[0](109) > 木[3](103) -> True
  • 木[1]: dp[1] = 1 , 木[1](110) > 木[3](103) -> True
  • 木[2]: dp[2] = 2 , 木[2](108) > 木[3](103) -> True

木[j] > 木[3] を満たし、最初に出現する最大減少部分列に 木[3] を追加する

dp[3] = dp[2](2) + 1 = 3

木[3]が末尾の減少部分列の長さは[0, 2, 3]を選んだ時の3


DP5-f-5

木[4]より手前の項について調べる (i = 4, j = 0, 1, 2, 3)

  • 木[0]: dp[0] = 1 , 木[0](109) > 木[4](100) -> True
  • 木[1]: dp[1] = 1 , 木[1](110) > 木[4](100) -> True
  • 木[2]: dp[2] = 2 , 木[2](108) > 木[4](100) -> True
  • 木[3]: dp[3] = 3 , 木[3](103) > 木[4](100) -> True

木[j] > 木[4] を満たし、最初に出現する増加部分列に 木[4] を追加する

dp[4] = dp[3](3) + 1 = 4

木[4]が末尾の減少部分列の長さは[0, 2, 3, 4]を選んだ時の4

  • 配列aで減少部分列が最大になる要素の組み合わせは [0, 2, 3, 4](109, 110, 103, 100)
  • 配列aで減少部分列が最大になる区間の長さは4

今回のまとめ

セクション5は配列から選択した要素が連続して増加(または減少)する区間の数を求める問題でした。

いきなりこの問題に挑戦すると難しいですが、セクション1から順番に解いてきたらコツがつかめたかと思います!



【PR】アルゴリズム学習でお世話になった本


アルゴリズム関連の技術書は大抵C/C++で書かれていますが、ある程度プログラムが出来れば、処理の流れは理解することが出来ます。






通称: 螺旋本。C++で解説されています。AOJ(Aizu Online Judge)を運営している会津大学の渡辺准教授が書いた本です。データ構造や計算量の内容から丁寧に書いてありますのでアルゴリズム学習の最初の参考書としてオススメです。







通称: 蟻本。C++で解説されています。競技プログラミング中級者の定番書と言われていて、競技プログラミングで利用できる色々なアルゴリズムを学ぶことが出来ます。かなり高度な内容も含まれているので1冊分を完全に理解するのにはかなりの時間がかかりそうですが、手元に置いて何度も読み返しています。







通称: チーター本。C#, C++, JAVAでTopcoderの問題を解説しています。初心者~中級者向けに書かれているので解説が非常に丁寧です。







Python3でアルゴリズムを解説している本です。講義スタイルの本で、図やフローチャートを使ってアルゴリズムとデータ構造についてしっかりと説明されています。代わりにコードは少なめです。Pythonコードが読めなくても十分理解できると思います。


-paiza, プログラミング
-, , , ,

© 2024 じゃいごテック