paiza プログラミング

[Ruby|Python]paiza グラフ・DFSメニュー セクション2【パスの通れない頂点 2】

graph_dfs__path_all

paiza グラフ・DFSメニューから、セクション2「パスの通れない頂点 2」を解説します。

セクション2は、セクション1と問題内容がほぼ同じですが、グラフをDFS(深さ優先探索)した経路を列挙する問題となっています。

本記事で使用しているメソッド・アルゴリズムについて

解答例で使っているメソッドやアルゴリズムについて、下記の記事で詳しく解説していますので参考にしてみて下さい。

セクション1【パスの通れない頂点 2】

STEP1: 隣接頂点の出力 2 (paizaランク D 相当)

ステップ1は、頂点、枝(経路)、頂点 s が与えられ、頂点 s に隣接する頂点の番号を昇順の半角スペース区切りで出力する問題です。

入出力例

1行目で頂点数 n と頂点 s が与えられます。
続く 2 * i 行目には 頂点 i に隣接している頂点の数、 2 * i +1 行目には頂点 i に隣接している頂点番号が半角スペース区切りで与えられます。

INPUT1 = <<~"EOS"
  3 2
  1
  2
  2
  1 3
  1
  2
EOS
OUTPUT1 = <<~"EOS"
  1 3
EOS

INPUT2 = <<~"EOS"
  5 5
  2
  2 5
  3
  1 3 5
  3
  2 4 5
  2
  3 5
  4
  1 2 3 4
EOS
OUTPUT2 = <<~"EOS"
  1 2 3 4
EOS

INPUT1 はこんな感じ

01-01-INPUT1-01

② に隣接する頂点は ① , ③

02-01-INPUT1-02

INPUT2 はこんな感じ

01-01-INPUT2-01

⑤ に隣接する頂点は ①, ②, ③, ④

02-01-INPUT2-2

Ruby 解答例

入力データから隣接リスト ad_list を作成し、ad_list[s] の要素を半角スペースで連結して出力します。

def main(input_str)
  input_lines = input_str.split("\n")
  # n: 頂点数, s: 探索対象の頂点
  n, s = input_lines.shift.split.map(&:to_i)
  # 隣接リスト
  ad_list = {}
  input_lines.each.with_index(1) do |line, i|
    next if i.odd?
    ad_list[i / 2] = line.split.map(&:to_i)
  end

  # 頂点 s の隣接頂点を番号順に半角スペース区切りで出力
  ad_list[s].join(" ")
end

puts main(STDIN.read)
Python解答例
def main(input_str):
    input_lines = input_str.splitlines()
    # n: 頂点数, s: 探索対象の頂点
    n, s = map(int, input_lines[0].split())
    # 隣接リスト
    ad_list = {}
    for (i, line) in enumerate(input_lines[1:], 1):
        if i % 2 != 0:
            continue
        ad_list[i // 2] = list(map(int, line.split()))

    # 頂点 s の隣接頂点を番号順に半角スペース区切りで出力
    return " ".join(map(str, ad_list[s]))


print(main(open(0).read()))

 


STEP2: グラフのウォーク 2 (paizaランク C 相当)

ステップ2は、頂点、枝(経路)、起点 s 、移動回数 k が与えられ、 始点 s から k 回移動する経路を全て出力する問題です。

  • 頂点を再訪問してもOK (walk)
入出力例
INPUT1 = <<~"EOS"
  4 2 4
  1
  2
  3
  1 3 4
  2
  2 4
  2
  2 3
EOS
OUTPUT1 = <<~"EOS"
  25
  2 1 2 1 2
  2 1 2 3 2
  2 1 2 3 4
  2 1 2 4 2
  2 1 2 4 3
  2 3 2 1 2
  2 3 2 3 2
  2 3 2 3 4
  2 3 2 4 2
  2 3 2 4 3
  2 3 4 2 1
  2 3 4 2 3
  2 3 4 2 4
  2 3 4 3 2
  2 3 4 3 4
  2 4 2 1 2
  2 4 2 3 2
  2 4 2 3 4
  2 4 2 4 2
  2 4 2 4 3
  2 4 3 2 1
  2 4 3 2 3
  2 4 3 2 4
  2 4 3 4 2
  2 4 3 4 3
EOS
INPUT2 = <<~"EOS"
  5 5 3
  2
  2 5
  3
  1 3 5
  3
  2 4 5
  2
  3 5
  4
  1 2 3 4
EOS
OUTPUT2 = <<~"EOS"
  32
  5 1 2 1
  5 1 2 3
  5 1 2 5
  5 1 5 1
  5 1 5 2
  5 1 5 3
  5 1 5 4
  5 2 1 2
  5 2 1 5
  5 2 3 2
  5 2 3 4
  5 2 3 5
  5 2 5 1
  5 2 5 2
  5 2 5 3
  5 2 5 4
  5 3 2 1
  5 3 2 3
  5 3 2 5
  5 3 4 3
  5 3 4 5
  5 3 5 1
  5 3 5 2
  5 3 5 3
  5 3 5 4
  5 4 3 2
  5 4 3 4
  5 4 3 5
  5 4 5 1
  5 4 5 2
  5 4 5 3
  5 4 5 4
EOS

INPUT1 はこんな感じ

② からスタートする

02-02-INPUT1-00

② から ④ に移動する(移動1回目)

02-02-INPUT1-01

④ から ③ に移動する(移動2回目)

02-02-INPUT1-02

③ から ④ に移動する(移動3回目)

02-02-INPUT1-03

④ から ③ に移動する(移動4回目なので終了)

02-02-INPUT1-04

② → ④ → ③ → ④ → ③ という経路(walk)が出来たので記録する。
※ 残りの24経路についても同様に調べる。

INPUT2 はこんな感じ

⑤ からスタートする

02-02-INPUT2-00

⑤ から ④ に移動する(移動1回目)

02-02-INPUT2-01

④ から ⑤ に移動する(移動2回目)

02-02-INPUT2-02

⑤ から ④ に移動する(移動3回目なので終了)

02-02-INPUT2-03

⑤ → ④ → ⑤ → ④ という経路(walk)が出来たので記録する。
※ 残りの31経路についても同様に調べる。

Ruby解答例

以下の流れで実装しています。

  • 整数 n, s, k を受け取る
  • 隣接リスト ad_list を作成する
  • 探索結果 results を空で初期化する
  • 探索候補 walks始点 s で初期化する
  • walks が空になるまでループする
    • walks 末尾から walkpop する
    • walk の要素数(頂点の数) が k+1 (k 回移動) なら結果を results に記録してスキップ
    • cv (最後に訪問した頂点) の隣接リストを nv としてループする
      • walk 末尾に nv を加えた経路(walk)walks 末尾に追加する
  • results の各経路(walk)を半角スペース区切りで出力する。
Ruby解答例1 (再帰なし)
def main(input_str)
  input_lines = input_str.split("\n")
  # n: 頂点数, s: 起点, k: 回数
  n, s, k = input_lines.shift.split.map(&:to_i)
  # 隣接リスト
  ad_list = {}
  input_lines.each.with_index(1) do |line, i|
    next if i.odd?
    ad_list[i / 2] = line.split.map(&:to_i)
  end

  # k 回移動
  results = []
  walks = [[s]]
  while walks.length > 0
    walk = walks.pop
    # k 回移動したら経路を記録
    if walk.length == k + 1
      results << walk
      next
    end

    # 隣接頂点に移動する
    cv = walk.last
    ad_list[cv].each do |nv|
      walks << walk + [nv]
    end
  end

  # 経路数と経路を全て出力
  [results.length.to_s].concat(results.map { |e| e.join(" ") }).join("\n")
end

puts main(STDIN.read)
Ruby解答例2 (再帰あり)
def dfs(cv, nodes, k)
  if k == 0
    # k 回移動したら経路を記録
    $results << nodes.dup
  else
    $ad_list[cv].each do |nv|
      # nv を通る
      nodes << nv
      # 次の層を探索
      dfs(nv, nodes, k - 1)
      # nv を通らない
      nodes.pop
    end
  end
end

input_str = STDIN.read
input_lines = input_str.split("\n")
# n: 頂点数, s: 起点, k: 回数
n, s, k = input_lines.shift.split.map(&:to_i)
# 隣接リスト
$ad_list = {}
input_lines.each.with_index(1) do |line, i|
  next if i.odd?
  $ad_list[i / 2] = line.split.map(&:to_i)
end

# k 回移動
$results = []
dfs(s, [s], k)

# 経路数と経路を全て出力
puts [$results.length.to_s].concat($results.map { |e| e.join(" ") }).join("\n")
Python解答例1(再帰なし)
def main(input_str):
    input_lines = input_str.splitlines()
    # n: 頂点数, s: 起点, k: 回数
    n, s, k = map(int, input_lines[0].split())
    # 隣接リスト
    ad_list = {}
    for (i, line) in enumerate(input_lines[1:], 1):
        if i % 2 != 0:
            continue
        ad_list[i // 2] = list(map(int, line.split()))

    # 起点 s から k 回移動する経路
    results = []
    walks = [[s]]
    while len(walks) > 0:
        walk = walks.pop()
        # k 回移動したら経路を記録
        if len(walk) == k + 1:
            results.append(walk)
            continue

        # 隣接頂点に移動する
        cv = walk[-1]
        for nv in ad_list[cv]:
            walks.append(walk + [nv])

    # 経路数と経路を全て出力
    path_count = str(len(results))
    path_list = [" ".join(map(str, res)) for res in results]
    return "\n".join([path_count] + path_list)


print(main(open(0).read()))
Python解答例2(再帰あり)
def dfs(cv, nodes, k):
    if k == 0:
        # k 回移動したら経路を記録
        results.append(nodes.copy())
    else:
        for nv in ad_list[cv]:
            # nv を通る
            nodes.append(nv)
            # 次の層を探索
            dfs(nv, nodes, k - 1)
            # nv を通らない
            nodes.pop()


input_str = open(0).read()
input_lines = input_str.splitlines()
# n: 頂点数, s: 起点, k: 回数
n, s, k = map(int, input_lines[0].split())
# 隣接リスト
ad_list = {}
for (i, line) in enumerate(input_lines[1:], 1):
    if i % 2 != 0:
        continue
    ad_list[i // 2] = list(map(int, line.split()))

# 起点 s から k 回移動する経路
results = []
dfs(s, [s], k)

# 経路数と経路を全て出力
print(len(results))
[print(*result) for result in results]

 


STEP3: グラフのパス 2 (paizaランク C 相当)

ステップ3は、頂点数、枝(経路)、起点 s 、移動回数 k が与えられ、 k 回移動した場合の経路を全て出力する問題です。

  • 訪問済みの頂点は再訪問できない (path)
入出力例
INPUT1 = <<~"EOS"
  4 2 4
  1
  2
  3
  1 3 4
  2
  2 4
  2
  2 3
EOS
OUTPUT1 = <<~"EOS"
  0
EOS

INPUT2 = <<~"EOS"
  5 5 3
  2
  2 5
  3
  1 3 5
  3
  2 4 5
  2
  3 5
  4
  1 2 3 4
EOS
OUTPUT2 = <<~"EOS"
  4
  5 1 2 3
  5 2 3 4
  5 3 2 1
  5 4 3 2
EOS

INPUT3 = <<~"EOS"
  7 1 3
  6
  2 3 4 5 6 7
  2
  1 7
  2
  1 4
  2
  1 3
  2
  1 6
  2
  1 5
  2
  1 2
EOS
OUTPUT3 = <<~"EOS"
  0
EOS

 

INPUT1 はこんな感じ

② からスタートする

02-03-INPUT1-00

② から ④ に移動する(移動1回目)

02-02-INPUT1-01

④ から ③ に移動する(移動2回目)

02-02-INPUT1-02

③ からは移動出来ないのでスキップ(探索打ち切り)

02-02-INPUT1-03

同様に
② → ③ → ④
② → ①
となり、 k 回移動するpathが作れないので -1 を出力する

INPUT2 はこんな感じ

⑤ からスタートする

02-02-INPUT2-00

⑤ から ④ に移動する(移動1回目)

02-02-INPUT2-01

④ から ③ に移動する(移動2回目)

02-02-INPUT2-02

③ から ② に移動する(移動3回目なので終了)

02-02-INPUT2-03

⑤ → ④ → ③ → ② という経路(path)が出来たので記録する。

残りの3経路
⑤ → ③ → ② → ①
⑤ → ② → ③ → ④
⑤ → ① → ② → ③
についても同様に調べる。

 

Ruby解答例

ステップ2とほぼ同様ですが、次の頂点を加える前に未訪問かを確認しています。

  • 整数 n, s, k を受け取る
  • 隣接リスト ad_list を作成する
  • 探索結果 results を空で初期化する
  • 探索候補 paths始点 s で初期化する
  • paths が空になるまでループする
    • paths 末尾から path を pop する
    • path の要素数(頂点の数) が k+1 (k 回移動) なら結果を results に記録してスキップ
    • cv (最後に訪問した頂点) の隣接リストの要素を次の頂点 nv でループする
      • 次の頂点 nv が訪問済みならスキップする
      • path 末尾に nv を加えた経路(path)paths 末尾に追加する
  • results の各経路(path)を半角スペース区切りで出力する。
Ruby解答例1(再帰なし)
def main(input_str)
  input_lines = input_str.split("\n")
  # n: 頂点数, s: 起点, k: 回数
  n, s, k = input_lines.shift.split.map(&:to_i)
  # 隣接リスト
  ad_list = {}
  input_lines.each.with_index(1) do |line, i|
    next if i.odd?
    ad_list[i / 2] = line.split.map(&:to_i)
  end

  # k 回移動
  results = []
  paths = [[s]]
  while paths.length > 0
    path = paths.shift
    # k 回移動したら経路を記録
    if path.length == k + 1
      results << path
      next
    end
    
    # 隣接頂点に移動する
    cv = path.last
    ad_list[cv].each do |nv|
      # 訪問済の頂点はスキップ
      next if path.include?(nv)
      paths << path + [nv]
    end
  end
  
  # 経路数と経路を全て出力
  [results.length.to_s].concat(results.map { |e| e.join(" ") }).join("\n")
end

puts main(STDIN.read)
Ruby解答例2(再帰あり)
def dfs(cv, nodes, k)
  if k == 0
    # k 回移動したら経路を記録
    $results << nodes.dup
  else
    $ad_list[cv].each do |nv|
      # 訪問済の頂点はスキップ
      next if nodes.include?(nv)
      # nv を通る
      nodes << nv
      # 次の層を探索
      dfs(nv, nodes, k - 1)
      # nv を通らない
      nodes.pop
    end
  end
end

input_str = STDIN.read
input_lines = input_str.split("\n")
# n: 頂点数, s: 起点, k: 回数
n, s, k = input_lines.shift.split.map(&:to_i)
# 隣接リスト
$ad_list = {}
input_lines.each.with_index(1) do |line, i|
  next if i.odd?
  $ad_list[i / 2] = line.split.map(&:to_i)
end

# k 回移動
$results = []
dfs(s, [s], k)

# 経路数と経路を全て出力
puts [$results.length.to_s].concat($results.map { |e| e.join(" ") }).join("\n")
Python解答例1(再帰なし)
def main(input_str):
    input_lines = input_str.splitlines()
    # n: 頂点数, s: 起点, k: 回数
    n, s, k = map(int, input_lines[0].split())
    # 隣接リスト
    ad_list = {}
    for (i, line) in enumerate(input_lines[1:], 1):
        if i % 2 != 0:
            continue
        ad_list[i // 2] = list(map(int, line.split()))

    # 起点 s から k 回移動する経路
    results = []
    paths = [[s]]
    while len(paths) > 0:
        path = paths.pop()
        # k 回移動したら経路を記録
        if len(path) == k + 1:
            results.append(path)
            continue

        # 隣接頂点に移動する
        cv = path[-1]
        for nv in ad_list[cv]:
            # 訪問済の頂点はスキップ
            if nv in path:
                continue
            paths.append(path + [nv])

    # 経路数と経路を全て出力
    path_count = str(len(results))
    path_list = [" ".join(map(str, e)) for e in results]
    return "\n".join([path_count] + path_list)


print(main(open(0).read()))
Python解答例2(再帰あり)
def dfs(cv, nodes, k):
    if k == 0:
        # k 回移動したら経路を記録
        results.append(nodes.copy())
    else:
        for nv in ad_list[cv]:
            # 訪問済の頂点はスキップ
            if nv in nodes:
                continue
            # nv を通る
            nodes.append(nv)
            # 次の層を探索
            dfs(nv, nodes, k - 1)
            # nv を通らない
            nodes.pop()


input_str = open(0).read()
input_lines = input_str.splitlines()
# n: 頂点数, s: 起点, k: 回数
n, s, k = map(int, input_lines[0].split())
# 隣接リスト
ad_list = {}
for (i, line) in enumerate(input_lines[1:], 1):
    if i % 2 != 0:
        continue
    ad_list[i // 2] = list(map(int, line.split()))

# 起点 s から k 回移動する経路
results = []
dfs(s, [s], k)

# 経路数と経路を全て出力
print(len(results))
[print(*e) for e in results]

 


STEP4: グラフの s,t パス 2 (paizaランク B 相当)

ステップ4は、頂点、枝(経路)、起点 s 、終点 t が与えられ、 始点 s から 終点 t まで移動できる経路の総数と全ての経路を出力する問題です。

  • 同じ頂点は再訪問できない (path)
入出力例
INPUT1 = <<~"EOS"
  5 1 4
  2
  2 5
  3
  1 3 5
  3
  2 4 5
  2
  3 5
  4
  1 2 3 4
EOS
OUTPUT1 = <<~"EOS"
  7
  1 2 3 4
  1 2 3 5 4
  1 2 5 3 4
  1 2 5 4
  1 5 2 3 4
  1 5 3 4
  1 5 4
EOS
INPUT2 = <<~"EOS"
  5 5 3
  2
  2 5
  3
  1 3 5
  3
  2 4 5
  2
  3 5
  4
  1 2 3 4
EOS
OUTPUT2 = <<~"EOS"
  4
  5 1 2 3
  5 2 3
  5 3
  5 4 3
EOS

INPUT1 はこんな感じ

02-04-INPUT1-00

① から ④ へ行ける path は 7 通り

① → ⑤ → ④
① → ⑤ → ③ → ④
① → ⑤ → ② → ③ → ④
① → ② → ⑤ → ④
① → ② → ⑤ → ③ → ④
① → ② → ③ → ⑤ → ④
① → ② → ③ → ④

INPUT2 はこんな感じ

02-04-INPUT2-00

⑤ から ③ へ行ける path は 4 通り

⑤ → ④ → ③
⑤ → ③
⑤ → ② → ③
⑤ → ① → ② → ③

Ruby解答例

ステップ2とほぼ同様ですが、path の末尾が t のときに結果を記録します。

  • 整数 n, s, t を受け取る
  • 隣接リスト ad_list を作成する
  • 探索結果 results を空で初期化する
  • 探索候補 paths始点 s で初期化する
  • paths が空になるまでループする
    • paths 末尾から path を pop する
    • path の末尾要素(最後に訪問した頂点) が t なら結果を results に記録してスキップ
    • cv (最後に訪問した頂点) の隣接リストの要素を次の頂点 nv でループする
      • 次の頂点 nv が訪問済みならスキップする
      • path 末尾に nv を加えた経路(path)paths 末尾に追加する
  • results の各経路(path)を半角スペース区切りで出力する。
Ruby解答例1(再帰なし)
def main(input_str)
  input_lines = input_str.split("\n")
  # n: 頂点数, s: 起点, t: 終点
  n, s, t = input_lines.shift.split.map(&:to_i)
  # 隣接リスト
  ad_list = {}
  input_lines.each.with_index(1) do |line, i|
    next if i.odd?
    ad_list[i / 2] = line.split.map(&:to_i)
  end

  # s から t までの経路
  results = []
  paths = [[s]]
  while paths.length > 0
    path = paths.pop
    # t に着いたら経路を記録
    if path.last == t
      results << path
      next
    end

    # 隣接頂点に移動する
    cv = path.last
    ad_list[cv].each do |nv|
      # 訪問済の頂点はスキップ
      next if path.include?(nv)
      paths << path + [nv]
    end
  end

  # 経路数と経路を全て出力
  [results.length.to_s].concat(results.map { |e| e.join(" ") }).join("\n")
end

puts main(STDIN.read)
Ruby解答例2(再帰あり)
def dfs(cv, nodes)
  $ad_list[cv].each do |nv|
    # 訪問済の頂点はスキップ
    next if nodes.include?(nv)
    # nv を通る
    nodes << nv
    if nv == $t
      # t に着いたら経路を記録
      $results << nodes.dup
    else
      # 次の層を探索
      dfs(nv, nodes)
    end
    # nv を通らない
    nodes.pop
  end
end

input_str = STDIN.read
input_lines = input_str.split("\n")
# n: 頂点数, s: 起点, t: 終点
n, s, $t = input_lines.shift.split.map(&:to_i)
# 隣接リスト
$ad_list = {}
input_lines.each.with_index(1) do |line, i|
  next if i.odd?
  $ad_list[i / 2] = line.split.map(&:to_i)
end

# s から t までの経路
$results = []
dfs(s, [s])

# 経路数と経路を全て出力
puts [$results.length.to_s].concat($results.map { |e| e.join(" ") }).join("\n")
Python解答例1(再帰なし)
def main(input_str):
    input_lines = input_str.splitlines()
    # n: 頂点数, s: 起点, t: 終点
    n, s, t = map(int, input_lines[0].split())
    # 隣接リスト
    ad_list = {}
    for (i, line) in enumerate(input_lines[1:], 1):
        if i % 2 != 0:
            continue
        ad_list[i // 2] = list(map(int, line.split()))

    # s から t への経路
    results = []
    paths = [[s]]
    while len(paths) > 0:
        path = paths.pop()
        # t に着いたら経路を記録
        if path[-1] == t:
            results.append(path)
            continue

        # 隣接頂点に移動する
        cv = path[-1]
        for nv in ad_list[cv]:
            # 訪問済の頂点はスキップ
            if nv in path:
                continue
            paths.append(path + [nv])

    # 経路数と経路を全て出力
    path_count = str(len(results))
    path_list = [" ".join(map(str, e)) for e in results]
    return "\n".join([path_count] + path_list)


print(main(open(0).read()))
Python解答例2(再帰あり)
def dfs(cv, nodes):
    for nv in ad_list[cv]:
        # 訪問済の頂点はスキップ
        if nv in nodes:
            continue
        # nv を通る
        nodes.append(nv)
        if nv == t:
            # t に着いたら経路を記録
            results.append(nodes.copy())
        else:
            # 次の層を探索
            dfs(nv, nodes)
        # nv を通らない
        nodes.pop()


input_str = open(0).read()
input_lines = input_str.splitlines()
# n: 頂点数, s: 起点, t: 終点
n, s, t = map(int, input_lines[0].split())
# 隣接リスト
ad_list = {}
for (i, line) in enumerate(input_lines[1:], 1):
    if i % 2 != 0:
        continue
    ad_list[i // 2] = list(map(int, line.split()))

# s から t までの経路
results = []
dfs(s, [s])

# 経路数と経路を全て出力
print(len(results))
[print(*e) for e in results]

 


STEP5: パスの経由地 2 (paizaランク B 相当)

ステップ5は、頂点、枝(経路)、起点 s 、終点 t 、中継点 q が与えられ、 始点 s から 中継点 q を経由して 終点 t まで移動できる経路の総数と全ての経路を出力する問題です。(※ 本家では中継点 p となっていますが、 p という変数を使いたくないので p → q に変更しました)
・訪問済みの頂点は再訪問できない (path)
・経路が存在しない場合もある

入出力例
INPUT1 = <<~"EOS"
  5 1 4 3
  2
  2 5
  3
  1 3 5
  3
  2 4 5
  2
  3 5
  4
  1 2 3 4
EOS
OUTPUT1 = <<~"EOS"
  5
  1 2 3 4
  1 2 3 5 4
  1 2 5 3 4
  1 5 2 3 4
  1 5 3 4
EOS

INPUT2 = <<~"EOS"
  5 5 3 1
  2
  2 5
  3
  1 3 5
  3
  2 4 5
  2
  3 5
  4
  1 2 3 4
EOS
OUTPUT2 = <<~"EOS"
  1
  5 1 2 3
EOS

INPUT3 = <<~"EOS"
  8 8 4 1
  1
  2
  7
  1 3 4 5 6 7 8
  6
  2 4 5 6 7 8
  6
  2 3 5 6 7 8
  6
  2 3 4 6 7 8
  6
  2 3 4 5 7 8
  6
  2 3 4 5 6 8
  6
  2 3 4 5 6 7
EOS
OUTPUT3 = <<~"EOS"
  0
EOS

INPUT1 はこんな感じ

02-05-INPUT1-00

① から ③ を経由して ④ へ行ける path は 5 通り

① → ⑤ → ③ → ④
① → ⑤ → ② → ③ → ④
① → ② → ⑤ → ③ → ④
① → ② → ③ → ⑤ → ④
① → ② → ③ → ④

INPUT2 はこんな感じ

02-05-INPUT2-00

⑤ から ① を経由して ③ へ行ける path は 1 通り

⑤ → ① → ② → ③

INPUT3 はこんな感じ

02-05-INPUT3-00

⑧ から ① を経由して ④ へ行ける path は無い
(中継頂点 ① の隣接頂点が1個しかないから)

 

Ruby解答例

今回も前回のコードを流用します。
path の末尾が t  かつ、pathq を含む場合、結果を記録します。

  • 整数 n, s, t, q を受け取る
  • 隣接リスト ad_list を作成する
  • 探索結果 results を空で初期化する
  • 探索候補 paths始点 s で初期化する
  • paths が空になるまでループする
    • paths 末尾から path を pop する
    • path の末尾要素が t  かつ、 pathq を含むなら結果を results に記録してスキップ
    • cv (最後に訪問した頂点) の隣接リストの要素を次の頂点 nv でループする
      • 次の頂点 nv が訪問済みならスキップする
      • path 末尾に nv を加えた経路(path)paths 末尾に追加する
  • results の各経路(path)を半角スペース区切りで出力する。
Ruby解答例1(再帰なし)
def main(input_str)
  input_lines = input_str.split("\n")
  # n: 頂点数, s: 起点, t: 終点, q: 中継点
  n, s, t, q = input_lines.shift.split.map(&:to_i)
  # 隣接リスト
  ad_list = {}
  input_lines.each.with_index(1) do |line, i|
    next if i.odd?
    ad_list[i / 2] = line.split.map(&:to_i)
  end

  # s → q → t の経路
  results = []
  paths = [[s]]
  while paths.length > 0
    path = paths.pop
    # q を経由して t に着いたら経路を記録
    if path.last == t && path.include?(q)
      results << path
      next
    end

    # 隣接頂点に移動する
    cv = path.last
    ad_list[cv].each do |nv|
      # 訪問済の頂点はスキップ
      next if path.include?(nv)
      paths << path + [nv]
    end
  end

  # 経路数と経路を全て出力
  [results.length.to_s].concat(results.map { |e| e.join(" ") }).join("\n")
end

puts main(STDIN.read)
Ruby解答例2(再帰あり)
def dfs(cv, nodes)
  $ad_list[cv].each do |nv|
    # 訪問済の頂点はスキップ
    next if nodes.include?(nv)
    # nv を通る
    nodes << nv
    if nv == $t && nodes.include?($q)
      # q を経由して t に着いたら経路を記録
      $results << nodes.dup
    else
      # 次の層を探索
      dfs(nv, nodes)
    end
    # nv を通らない
    nodes.pop
  end
end

input_str = STDIN.read
input_lines = input_str.split("\n")
# n: 頂点数, s: 起点, t: 終点, q: 中継点
n, s, $t, $q = input_lines.shift.split.map(&:to_i)
# 隣接リスト
$ad_list = {}
input_lines.each.with_index(1) do |line, i|
  next if i.odd?
  $ad_list[i / 2] = line.split.map(&:to_i)
end

# s から t までの経路
$results = []
dfs(s, [s])

# 経路数と経路を全て出力
puts [$results.length.to_s].concat($results.map { |e| e.join(" ") }).join("\n")

 

Python解答例1(再帰なし)
def main(input_str):
    input_lines = input_str.splitlines()
    # n: 頂点数, s: 起点, t: 終点, q: 中継点
    n, s, t, q = map(int, input_lines[0].split())
    # 隣接リスト
    ad_list = {}
    for (i, line) in enumerate(input_lines[1:], 1):
        if i % 2 != 0:
            continue
        ad_list[i // 2] = list(map(int, line.split()))

    # s から t への経路
    results = []
    paths = [[s]]
    while len(paths) > 0:
        path = paths.pop()
        # q を経由して t に着いたら経路を記録
        if path[-1] == t and q in path:
            results.append(path)
            continue

        # 隣接頂点に移動する
        cv = path[-1]
        for nv in ad_list[cv]:
            # 訪問済の頂点はスキップ
            if nv in path:
                continue
            paths.append(path + [nv])

    # 経路数と経路を全て出力
    path_count = str(len(results))
    path_list = [" ".join(map(str, e)) for e in results]
    return "\n".join([path_count] + path_list)


print(main(open(0).read()))
Python解答例2(再帰あり)
def dfs(cv, nodes):
    for nv in ad_list[cv]:
        # 訪問済の頂点はスキップ
        if nv in nodes:
            continue
        # nv を通る
        nodes.append(nv)
        if nv == t and q in nodes:
            # q を経由して t に着いたら経路を記録
            results.append(nodes.copy())
        else:
            # 次の層を探索
            dfs(nv, nodes)
        # nv を通らない
        nodes.pop()


input_str = open(0).read()
input_lines = input_str.splitlines()
# n: 頂点数, s: 起点, t: 終点, q: 中継点
n, s, t, q = map(int, input_lines[0].split())
# 隣接リスト
ad_list = {}
for (i, line) in enumerate(input_lines[1:], 1):
    if i % 2 != 0:
        continue
    ad_list[i // 2] = list(map(int, line.split()))

# s から t までの経路
results = []
dfs(s, [s])

# 経路数と経路を全て出力
print(len(results))
[print(*e) for e in results]

 


FINAL: パスの通れない頂点 2 (paizaランク B 相当)

ステップ6は、頂点、枝(経路)、訪問出来ない頂点、起点 s 、終点 t が与えられ、 始点 s から 終点 t まで移動できる経路の総数と全ての経路を出力する問題です

  • 訪問済みの頂点は再訪問できない (path)
  • 経路が存在しない場合もある
入出力例
INPUT1 = <<~"EOS"
  5 1 4
  3
  2 3 5
  2
  2 5
  3
  1 3 5
  3
  2 4 5
  2
  3 5
  4
  1 2 3 4
EOS
OUTPUT1 = <<~"EOS"
  0
EOS

INPUT2 = <<~"EOS"
  5 5 3
  3
  1 2 4
  2
  2 5
  3
  1 3 5
  3
  2 4 5
  2
  3 5
  4
  1 2 3 4
EOS
OUTPUT2 = <<~"EOS"
  1
  5 3
EOS

INPUT3 = <<~"EOS"
  7 3 4
  1
  2
  6
  2 3 4 5 6 7
  3
  1 3 7
  3
  1 2 4
  3
  1 3 5
  3
  1 4 6
  3
  1 5 7
  3
  1 2 6
EOS
OUTPUT3 = <<~"EOS"
  5
  3 1 4
  3 1 5 4
  3 1 6 5 4
  3 1 7 6 5 4
  3 4
EOS

INPUT1 はこんな感じ

通れない頂点: ②, ③, ⑤

02-06-INPUT1-00

① から ④ へ行ける path は無い

INPUT2 はこんな感じ

通れない頂点: ①, ②, ④

02-06-INPUT2-00

⑤ から ③ へ行ける path は 1 通り

⑤ → ③

INPUT3 はこんな感じ

02-06-INPUT3-00

③ から ④ へ行ける path は 5 通り

③ → ④
③ → ① → ⑦ → ⑥ → ⑤ → ④
③ → ① → ⑥ → ⑤ → ④
③ → ① → ⑤ → ④
③ → ① → ④

 

 

 

Ruby解答例

ステップ4のコードを流用します。
訪問しない頂点を配列で記録しておき、探索候補の頂点が訪問しない頂点ならスキップする条件分岐を追加します。

  • 整数 n, s, t を受け取る
  • 隣接リスト ad_list を作成する
  • 通らない頂点のリスト unused_vertices を作成する
  • 探索結果 results を空で初期化する
  • 探索候補 paths始点 s で初期化する
  • paths が空になるまでループする
    • paths 末尾から path を pop する
    • path の末尾要素(最後に訪問した頂点) が t なら結果を results に記録してスキップ
    • cv (最後に訪問した頂点) の隣接リストの要素を次の頂点 nv でループする
      • 次の頂点 nv が訪問済みならスキップする
      • 次の頂点 nv が unused_vertices に含まれているならスキップする
      • path 末尾に nv を加えた経路(path)paths 末尾に追加する
  • results の各経路(path)を半角スペース区切りで出力する。
Ruby解答例1(再帰なし)
def main(input_str)
  input_lines = input_str.split("\n")
  # n: 頂点数, s: 起点, t: 終点
  n, s, t = input_lines.shift.split.map(&:to_i)
  # 通らない頂点
  unused_vertices = input_lines.shift(2).last.split.map(&:to_i)
  # 隣接リスト
  ad_list = {}
  input_lines.each.with_index(1) do |line, i|
    next if i.odd?
    ad_list[i / 2] = line.split.map(&:to_i)
  end

  # 始点 s から終点 t までの経路
  results = []
  paths = [[s]]
  while paths.length > 0
    path = paths.pop
    # t に着いたら経路を記録
    if path.last == t
      results << path
      next
    end

    # 未訪問の隣接頂点に移動する
    cv = path.last
    ad_list[cv].each do |nv|
      # 訪問済の頂点はスキップ
      next if path.include?(nv)
      # 通れない頂点はスキップ
      next if unused_vertices.include?(nv)
      paths << path + [nv]
    end
  end

  # 経路数と経路を全て出力
  [results.length.to_s].concat(results.map { |e| e.join(" ") }).join("\n")
end

puts main(STDIN.read)
Ruby解答例2(再帰あり)
def dfs(cv, nodes)
  $ad_list[cv].each do |nv|
    # 訪問済の頂点はスキップ
    next if nodes.include?(nv)
    # 通れない頂点はスキップ
    next if $unused_vertices.include?(nv)
    # nv を通る
    nodes << nv
    if nv == $t
      # t に着いたら経路を記録
      $results << nodes.dup
    else
      # 次の層を探索
      dfs(nv, nodes)
    end
    # nv を通らない
    nodes.pop
  end
end

input_str = STDIN.read
input_lines = input_str.split("\n")
# n: 頂点数, s: 起点, t: 終点
n, s, $t = input_lines.shift.split.map(&:to_i)
# 通らない頂点
$unused_vertices = input_lines.shift(2).last.split.map(&:to_i)
# 隣接リスト
$ad_list = {}
input_lines.each.with_index(1) do |line, i|
  next if i.odd?
  $ad_list[i / 2] = line.split.map(&:to_i)
end

# s から t までの経路
$results = []
dfs(s, [s])

# 経路数と経路を全て出力
puts [$results.length.to_s].concat($results.map { |e| e.join(" ") }).join("\n")

 

Python解答例1(再帰なし)
def main(input_str):
    input_lines = input_str.splitlines()
    # n: 頂点数, s: 起点, t: 終点
    n, s, t = map(int, input_lines[0].split())
    # 通らない頂点
    unused_vertices = list(map(int, input_lines[2].split()))
    # 隣接リスト
    ad_list = {}
    for (i, line) in enumerate(input_lines[3:], 1):
        # 経路数 v は使わない
        if i % 2 == 0:
            # 隣接リストに経路を格納
            ad_list[i // 2] = list(map(int, line.split()))

    # 始点 s から終点 t までの経路
    results = []
    paths = [[s]]
    while len(paths) > 0:
        path = paths.pop()
        # t に着いたら経路を記録
        if path[-1] == t:
            results.append(path)
            continue

        # 未訪問の隣接頂点に移動する
        cv = path[-1]
        for nv in ad_list[cv]:
            # 訪問済の頂点と通れない頂点はスキップ
            if nv in path or nv in unused_vertices:
                continue
            # 新しい経路を追加
            paths.append(path + [nv])

    # 経路数と経路を全て出力
    path_count = str(len(results))
    path_list = [" ".join(map(str, e)) for e in results]
    return "\n".join([path_count] + path_list)


print(main(open(0).read()))
Python解答例2(再帰あり)
def dfs(cv, nodes):
    for nv in ad_list[cv]:
        # 訪問済の頂点はスキップ
        if nv in nodes:
            continue
        # 通れない頂点はスキップ
        if nv in unused_vertices:
            continue
        # nv を通る
        nodes.append(nv)
        if nv == t:
            # t に着いたら経路を記録
            results.append(nodes.copy())
        else:
            # 次の層を探索
            dfs(nv, nodes)
        # nv を通らない
        nodes.pop()


input_str = open(0).read()
input_lines = input_str.splitlines()
# n: 頂点数, s: 起点, t: 終点
n, s, t = map(int, input_lines[0].split())
# 通らない頂点
unused_vertices = list(map(int, input_lines[2].split()))
# 隣接リスト
ad_list = {}
for (i, line) in enumerate(input_lines[3:], 1):
    if i % 2 != 0:
        continue
    ad_list[i // 2] = list(map(int, line.split()))

# s から t までの経路
results = []
dfs(s, [s])

# 経路数と経路を全て出力
print(len(results))
[print(*e) for e in results]

 


今回のまとめ

  • DFS(深さ優先探索)の基本部分の構造はだいたい一緒
  • スキップ条件、探索終了条件を変えることで色々な問題に対応できる
  • ウォーク (walk) は頂点の反復を許す経路
  • パス (path) は頂点の反復を許さない経路

今回は、別解として再帰呼び出しのコードも記載してみました。
セクション3からは、新しい経路の種類 トレイル (trail) を扱います!



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


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






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







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







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







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


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

© 2024 じゃいごテック