🐸

[鉄則B18] Subset Sum with Restoration

2024/03/26に公開

https://atcoder.jp/contests/tessoku-book/tasks/tessoku_book_cq

二次元の動的計画法の応用で経路を復元する問題。

問題の要約

  • N 枚のカードから何枚かを選択して合計が S になるか?
  • なるのであれば、それは何のカードたちを選択したときか?

例1で問題を理解する

  • 2 2 3 の値が書かれた 3 枚のカードから 2 2 3 を選択したとき合計 7 になる
  • したがってカード番号 1 2 3 が答えになる

現実的には 2 2 3 の方を知りたい気がするがこの手の問題はカード番号を答えることが多い。

2つの方法の特徴

  • dp を逆に辿る方法
    • 可能かどうかを求める処理部分はそのままでよい
    • 復元が複雑になる
  • 足跡を逆に辿る方法
    • 可能かどうかを求める処理に足跡を記録する処理を入れる
    • 復元がシンプルになる

どちらにも一長一短ある。

参考:

https://qiita.com/drken/items/0c7bab0384438f285f93

dp を逆に辿る方法 (AC)

N, S = gets.split.collect(&:to_i)  # => [10, 100]
A = gets.split.collect(&:to_i)     # => [14, 23, 18, 7, 11, 23, 23, 5, 8, 2]
A.unshift(nil)                     # => [nil, 14, 23, 18, 7, 11, 23, 23, 5, 8, 2]
dp = Array.new(N.next) { Array.new(S.next) }
dp[0][0] = true
(1..N).each do |y|
  (0..S).each do |x|
    dp[y][x] = dp[y - 1][x] || (x >= A[y]) && dp[y - 1][x - A[y]]
  end
end
dp[N][S]                           # => true

# ------------ ここまでは基礎問題と同じ ------------

# 復元 (面倒)
x = S
routes = []
N.downto(1) do |y|
  if dp[y][x]
    if x >= A[y]
      if dp[y - 1][x - A[y]] == dp[y][x] # 左上から来ているか?
        x = x - A[y]               # => 98, 90, 85, 62, 39, 32, 14, 0
        routes.unshift(y)
      end
    end
  end
end

routes.size                        # => 8
routes                             # => [1, 3, 4, 6, 7, 8, 9, 10]
routes.sum { |e| A[e] }            # => 100

if routes.empty?
  p -1
else
  p routes.size
  puts routes * " "
end
  • 右下から左上に遡っていく
  • Y 軸は常にデクリメントする
  • X 軸はカードを選択したときだけ左に移動する
  • x < A[y] の場合 y 番目のカードを選択できていないケースなので見なくてよい

足跡を逆に辿る方法 (AC)

N, S = gets.split.collect(&:to_i)  # => [10, 100]
A = gets.split.collect(&:to_i)     # => [14, 23, 18, 7, 11, 23, 23, 5, 8, 2]
A.unshift(nil)                     # => [nil, 14, 23, 18, 7, 11, 23, 23, 5, 8, 2]
if true
  predecessor = Array.new(N.next) { Array.new(S.next) } # 足跡用
end
dp = Array.new(N.next) { Array.new(S.next) }
dp[0][0] = true
(1..N).each do |y|              # (1..カード枚数)
  (0..S).each do |x|            # (0..合計値)
    dp[y][x] = dp[y - 1][x] || (x >= A[y]) && dp[y - 1][x - A[y]]
    # カードを選択したのは左上から来た場合なのでその場合のみ足跡を記録する
    if true
      if x >= A[y] && dp[y][x] == dp[y - 1][x - A[y]]
        if dp[y][x]
          predecessor[y][x] = [y - 1, x - A[y]] # 足跡を記録
        end
      end
    end
  end
end
dp[N][S]                           # => true

# 復元 (足跡を見るだけなので簡単)
routes = []
x = S                              # => 100
N.downto(1) do |y|
  if predecessor[y][x]
    routes.unshift(y)
    _, x = predecessor[y][x]
  end
end

routes.size                        # => 8
routes                             # => [1, 3, 4, 6, 7, 8, 9, 10]
routes.sum { |e| A[e] }            # => 100

if routes.empty?
  p -1
else
  p routes.size
  puts routes * " "
end
  • 使う場合のみ足跡を残す
  • 足跡用の変数を別に用意するのではなく dp に足跡を一緒に入れる方法もあり
  • dp[N][S] が偽の時点で -1 としてもよい

Discussion