めもめも

このブログに記載の内容は個人の見解であり、必ずしも所属組織の立場、戦略、意見を代表するものではありません。

D - RGB Coloring 2 の解説

何の話かと言うと

atcoder.jp

上記の問題をネタに、(木構造とは限らない)単純無向グラフの基本的な取り扱いを説明します。

単純無向グラフは、多重リンクや自分から自分へのリンクを含まない無向グラフです。一般には、複数の連結成分に別れます。

データの持ち方

基本的には、各ノードに対して、リンクされたノードのリストを用意すればOKです。

  links = [[] for _ in range(N+1)] 
  for _ in range(M):
    a, b = list(map(int, f.readline().split()))
    links[a].append(b)
    links[b].append(a)

連結成分に対する処理

次のようにキューを用いると、あるノード n に対して、そこからすべての連結成分を辿ることができます。

    flag = [None] * (N+1) # 処理済みのノードを示す

    q = deque()
    flag[n] = True
    q.append(n)
    while q:
      i = q.pop()
      # ここでノード i についての処理をする
      for j in links[i]:
        if flag[j]:
          continue
        flag[j] = True
        q.append(j)

木構造の場合は、下記の記事で説明したように、親ノードの情報を与えることで「上から下にたどる」という順序を保証しました。

enakai00.hatenablog.com

今回は木構造とは限らないので、処理済みのノードを示すフラグを利用して、ループを避けています。木構造の場合も同じ手法を使うことができます。

連結部分ごとに個別に処理

連結とは限らないグラフに対して、連結部分ごとに処理をしたい場合は、次の様にすべてのノードについてループすればOKです。

  flag = [None] * (N+1) # 処理済みのノードを示す
  for n in range(1, N+1):
    if flag[n]:
      continue

    q = deque()
    flag[n] = True
    q.append(n)
    while q:
      i = q.pop()
      # ここでノード i についての処理をする
      for j in links[i]:
        if flag[j]:
          continue
        flag[j] = True
        q.append(j)

フラグのチェックによって、処理済みのノードはスキップされるので、同じ連結成分を2回処理することはありません。

問題の解法

で、最後に冒頭の問題の解法ですが、連結部分ごとに場合の数を計算して、それらの積で全体の答えになることから、上記のパターンが使えそうだと気がつきます。

それぞれの連結部分について、ノードを深さ優先探索でたどりながら、その時点で可能な配色を網羅していきます。

たとえば、

2 --- 1 --- 3
|-----------|

というループ状のグラフを 1, 2, 3 の順に処理する場合、

1. 可能な配色は、[R, None, None] (対称性から、G, B の場合はスキップして最後に3倍する。None は未定の意味)
2.可能な配色は、[R, G, None], [R, B, None]
3. 可能な配色は、[R, G, B], [R, B, G]

という様に順番に決まっていきます。あるノードを処理する際に、(深さ優先探索をしているので)そこからリンクされたノードの1つは確実に処理済みなので、追加する色の候補は、2種類以下になります。これにより、組み合わせの数を制限して、組合せ爆発を防いでいます。

具体的な実装としては、各ステップにおいて、その時点で可能性のある配色全体のリスト(candidate = [[R, G, None], [R, B, None]] みたいなやつ)をアップデートしていく感じになります。この配色リストをアップデートする処理を上記のパターンの

      # ここでノード i についての処理をする

という部分に当てはめればOKです。

ただし、この手のアップデートパターン(一種のDPですね)では、最初のノードに対する処理だけは別枠になるので、そのまま当てはめると場合分けの記述がごちゃごちゃします。そこで、まずは、深さ優先探索でグラフを辿る際のノード順のリストを作ります。

    tree_order = []
    q = deque()
    q.append(n)
    flag[n] = True
    while q:
      i = q.pop()
      tree_order.append(i)
      for j in links[i]:
        if flag[j]:
          continue
        flag[j] = True
        q.append(j)

この後、このリスト順にループを回せば、最初の要素 tree_order[0] に対する処理をきれいに分けて書くことができます。

完成版はこんな感じ。

import sys, copy
from collections import defaultdict, deque

def main(f):
  N, M = list(map(int, f.readline().split()))
  links = [[] for _ in range(N+1)] 
  for _ in range(M):
    a, b = list(map(int, f.readline().split()))
    links[a].append(b)
    links[b].append(a)

  flag = [None] * (N+1)
  result = 1
  for n in range(1, N+1):
    if flag[n]:
      continue

    tree_order = []
    q = deque()
    q.append(n)
    flag[n] = True
    while q:
      i = q.pop()
      tree_order.append(i)
      for j in links[i]:
        if flag[j]:
          continue
        flag[j] = True
        q.append(j)

    candidates = []
    i = tree_order[0]
    key = [None] * (N+1)
    key[i] = 0
    candidates.append(key)
    for i in tree_order[1:]:
      candidates_n = []
      for col_list in candidates:
        for c in range(3):
          ok = True
          for j in links[i]:
            if col_list[j] == c:
              ok = False
          if ok:
            col_list[i] = c
            candidates_n.append(copy.copy(col_list))
      candidates = candidates_n

    result *= len(candidates) * 3

  print(result)

main(sys.stdin)

atcoder.jp