🚲

[ABC061 D] Score Attack

2024/03/06に公開

https://atcoder.jp/contests/abc061/tasks/abc061_d

START〜GOAL間にない負閉路は無視する問題。

要点

  • 負のコストを扱うのでベルマン-フォード法を用いる

例1のシミュレーション

1 2 3
0
1 4 7
2 収束したので終了する

1-2-3 と進んで答えは 7 になる。

例2のシミュレーション

1 2
0
1 2 1
2 4 3 負閉路検出のため中止する

頂点数回目も更新しているため inf となる。

ところが普通に書くと WA

N, M = gets.split.collect(&:to_i)                       # => [3, 3]
Edges = M.times.collect { gets.split.collect(&:to_i) }  # => [[1, 2, 4], [2, 3, 3], [1, 3, 5]]
START = 1
GOAL  = N

d = Hash.new(-Float::INFINITY)
d[START] = 0

success = catch(:abort) do
  N.times do |i|
    updated = false
    Edges.each do |from, to, score|
      v = d[from] + score
      if v > d[to]
        if i == N.pred
          throw :abort
        end
        d[to] = v
        updated = true
      end
    end
    unless updated
      break
    end
  end
  true
end

ans = success ? d[GOAL] : "inf"                         # => 7
puts ans

何度見直しても間違いはないはずだが一部で WA になる。どうやら負閉路が GOAL に絡んでいないのに負閉路と判断するのがまずいらしい。それは具体的にどのようなケースだろうか?

負閉路がゴールに絡まないケース

1 2 3 4
0
1 0 1 1 0
2 0 2 2 0
3 0 3 3 0
4 0 4 4 0 負閉路検出のため中止する

たしかに、これを見れば見当違いの方向にある負閉路で START〜GOAL 間がループしてしまうと判断するのはおかしいとわかる。

コード (WA)
N, M = gets.split.collect(&:to_i)                       # => [4, 4]
Edges = M.times.collect { gets.split.collect(&:to_i) }  # => [[1, 2, 0], [2, 3, 1], [3, 2, 0], [1, 4, 0]]
START = 1
GOAL  = N

d = Hash.new(-Float::INFINITY)
d[START] = 0

success = catch(:abort) do
  N.times do |i|
    updated = false
    Edges.each do |from, to, score|
      v = d[from] + score
      if v > d[to]
        if i == N.pred
          throw :abort
        end
        d[to] = v
        updated = true
      end
    end
    unless updated
      break
    end
  end
  true
end

ans = success ? d[GOAL] : "inf"                         # => "inf"
puts ans

修正後のコード (AC)

260 ms
N, M = gets.split.collect(&:to_i)                       # => [4, 4]
Edges = M.times.collect { gets.split.collect(&:to_i) }  # => [[1, 2, 0], [2, 3, 1], [3, 2, 0], [1, 4, 0]]
START = 1                                               # => 1
GOAL  = N                                               # => 4

d = Hash.new(-Float::INFINITY)  # distance
d[START] = 0

(N * 2).pred.times do |i|
  updated = false
  Edges.each do |from, to, score|
    v = d[from] + score
    if v > d[to]
      if i == N.pred
        v = Float::INFINITY     # 関連する経路だけに(本当の値ではなく)∞を伝搬する
      end
      d[to] = v
      updated = true
    end
  end
  unless updated
    break
  end
end

d                                                       # => {1=>0, 2=>Infinity, 3=>Infinity, 4=>0}
ans = d[GOAL].infinite? ? "inf" : d[GOAL]               # => 0
puts ans
更新
1 通常計算
2
3
4 N(4)回目で更新があれば負閉路検知のため∞の伝搬を開始する
5
6
7 ↓∞の伝搬終了

負閉路検知で∞を伝搬させれば 2, 3 だけが∞になり 1, 4 に影響が出ていないのがわかる。

関係ない経路を除去する解き方

コード (196 ms)
N, M = gets.split.collect(&:to_i)                       # => [4, 4]
Edges = M.times.collect { gets.split.collect(&:to_i) }  # => [[1, 2, 0], [2, 3, 1], [3, 2, 0], [1, 4, 0]]
START = 1                                               # => 1
GOAL  = N                                               # => 4

# スタートとゴールからたどりつけない辺を除外するか?
if true
  def dfs(edges, from, visited = {})
    unless visited[from]
      visited[from] = true
      edges[from].each { |to| dfs(edges, to, visited) }
    end
    visited
  end

  # スタートから辿りつけない辺を除外する
  if true
    start_to_goal = Hash.new { |h, k| h[k] = [] }
    Edges.each { |a, b| start_to_goal[a] << b }
    visited = dfs(start_to_goal, START)                 # => {1=>true, 2=>true, 3=>true, 4=>true}
    Edges = Edges.find_all { |from, to, _| visited[from] && visited[to] }
  end

  # ゴールから辿りつけない辺を除外する
  if true
    goal_to_start = Hash.new { |h, k| h[k] = [] }
    Edges.each { |a, b| goal_to_start[b] << a }
    visited = dfs(goal_to_start, GOAL)                  # => {4=>true, 1=>true}
    Edges = Edges.find_all { |from, to, _| visited[from] && visited[to] }
  end
end

d = Hash.new(-Float::INFINITY)  # distance
d[START] = 0

success = catch(:abort) do
  N.times do |i|
    updated = false
    Edges.each do |from, to, score|
      v = d[from] + score
      if v > d[to]
        if i == N.pred
          throw :abort
        end
        d[to] = v
        updated = true
      end
    end
    unless updated
      break
    end
  end
  true
end

ans = success ? d[GOAL] : "inf"                         # => 0
puts ans

あらかじめ START と GOAL から行ける辺に絞ることで計算量を減らせる。また負閉路を検出した場合は START〜GOAL 間にあるのはわかっているので即停止して問題ない。

類題

https://zenn.dev/megeton/articles/be0690514606dc

参考

Discussion