00-从共享房间到圆方树.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. #!/usr/bin/env python3
  2. """课前零食:从删点实验到圆方树的可操作小黑板。"""
  3. from __future__ import annotations
  4. from collections import deque
  5. VERTICES = tuple("ABCDEFG")
  6. EDGES = (
  7. ("A", "B"),
  8. ("B", "C"),
  9. ("C", "A"),
  10. ("C", "D"),
  11. ("D", "E"),
  12. ("E", "C"),
  13. ("C", "F"),
  14. ("F", "G"),
  15. )
  16. def make_graph() -> dict[str, list[tuple[str, int]]]:
  17. graph = {vertex: [] for vertex in VERTICES}
  18. for edge_id, (u, v) in enumerate(EDGES):
  19. graph[u].append((v, edge_id))
  20. graph[v].append((u, edge_id))
  21. return graph
  22. GRAPH = make_graph()
  23. def tarjan_blocks(with_trace: bool = False) -> tuple[list[list[int]], dict[str, int], dict[str, int], list[str]]:
  24. """返回按原边编号排序的点双边块、dfn、low 和可读日志。"""
  25. dfn: dict[str, int] = {}
  26. low: dict[str, int] = {}
  27. edge_stack: list[int] = []
  28. blocks: list[list[int]] = []
  29. trace: list[str] = []
  30. timer = 0
  31. def say(message: str) -> None:
  32. if with_trace:
  33. trace.append(message)
  34. def dfs(u: str, parent_edge: int | None) -> None:
  35. nonlocal timer
  36. timer += 1
  37. dfn[u] = low[u] = timer
  38. say(f"进入 {u}:dfn=low={timer}")
  39. for v, edge_id in GRAPH[u]:
  40. if edge_id == parent_edge:
  41. continue
  42. if v not in dfn:
  43. edge_stack.append(edge_id)
  44. say(f" 压边 {u}-{v};栈={edge_names(edge_stack)}")
  45. dfs(v, edge_id)
  46. low[u] = min(low[u], low[v])
  47. say(f" {v} 返回 {u}:low[{u}] 更新为 {low[u]}")
  48. if low[v] >= dfn[u]:
  49. block: list[int] = []
  50. while True:
  51. popped = edge_stack.pop()
  52. block.append(popped)
  53. if popped == edge_id:
  54. break
  55. say(
  56. f" low[{v}]={low[v]} ≥ dfn[{u}]={dfn[u]}:"
  57. f"弹出 {edge_names(block)},封成一个点双块"
  58. )
  59. blocks.append(block)
  60. elif dfn[v] < dfn[u]:
  61. edge_stack.append(edge_id)
  62. low[u] = min(low[u], dfn[v])
  63. say(
  64. f" 看见返祖边 {u}-{v}:压栈,"
  65. f"low[{u}] 更新为 {low[u]}"
  66. )
  67. for start in VERTICES:
  68. if start not in dfn:
  69. dfs(start, None)
  70. blocks.sort(key=min)
  71. return blocks, dfn, low, trace
  72. def edge_name(edge_id: int) -> str:
  73. u, v = EDGES[edge_id]
  74. return f"{u}-{v}"
  75. def edge_names(edge_ids: list[int]) -> str:
  76. return "[" + ", ".join(edge_name(edge_id) for edge_id in edge_ids) + "]"
  77. def block_members(block: list[int]) -> list[str]:
  78. return sorted({vertex for edge_id in block for vertex in EDGES[edge_id]})
  79. def build_block_cut_tree(blocks: list[list[int]]) -> dict[str, list[str]]:
  80. tree: dict[str, list[str]] = {vertex: [] for vertex in VERTICES}
  81. for index, block in enumerate(blocks, start=1):
  82. square = f"Q{index}"
  83. tree[square] = []
  84. for vertex in block_members(block):
  85. tree[vertex].append(square)
  86. tree[square].append(vertex)
  87. return tree
  88. def components_after_delete(removed: str) -> list[list[str]]:
  89. remaining = set(VERTICES) - {removed}
  90. components: list[list[str]] = []
  91. while remaining:
  92. start = min(remaining)
  93. queue = deque([start])
  94. remaining.remove(start)
  95. component = [start]
  96. while queue:
  97. u = queue.popleft()
  98. for v, _ in GRAPH[u]:
  99. if v in remaining and v != removed:
  100. remaining.remove(v)
  101. queue.append(v)
  102. component.append(v)
  103. components.append(sorted(component))
  104. return components
  105. def tree_path(tree: dict[str, list[str]], start: str, target: str) -> list[str] | None:
  106. if start not in tree or target not in tree:
  107. return None
  108. parent = {start: None}
  109. queue = deque([start])
  110. while queue:
  111. u = queue.popleft()
  112. if u == target:
  113. break
  114. for v in tree[u]:
  115. if v not in parent:
  116. parent[v] = u
  117. queue.append(v)
  118. if target not in parent:
  119. return None
  120. path: list[str] = []
  121. current: str | None = target
  122. while current is not None:
  123. path.append(current)
  124. current = parent[current]
  125. return list(reversed(path))
  126. def show(blocks: list[list[int]], dfn: dict[str, int], low: dict[str, int]) -> None:
  127. print("\n原图边:", ", ".join(edge_name(edge_id) for edge_id in range(len(EDGES))))
  128. print("DFS 账本:", " ".join(f"{u}: dfn={dfn[u]}, low={low[u]}" for u in VERTICES))
  129. print("点双块(按原边编号排序后命名):")
  130. for index, block in enumerate(blocks, start=1):
  131. print(f" Q{index}: 边 {edge_names(block)};成员 {block_members(block)}")
  132. tree = build_block_cut_tree(blocks)
  133. print("圆方树邻接表:")
  134. for node in list(VERTICES) + [f"Q{i}" for i in range(1, len(blocks) + 1)]:
  135. print(f" {node}: {' '.join(tree[node])}")
  136. def warmup() -> None:
  137. print("=== 任务 0:先当删点的暴力程序 ===")
  138. try:
  139. input("先预测删掉 C 后会有几块;写在纸上后按回车: ")
  140. except EOFError:
  141. pass
  142. print("核对:删 C 后是 {A,B}、{D,E}、{F,G} 三块。")
  143. try:
  144. input("再猜:谁可以同时属于 ABC 和 CDE 两个结实小团?按回车: ")
  145. except EOFError:
  146. pass
  147. print("核对:C。圆方树不复制 C,而是给每个块新建方点。")
  148. def main() -> None:
  149. blocks, dfn, low, trace = tarjan_blocks(with_trace=True)
  150. tree = build_block_cut_tree(blocks)
  151. print("圆方树小实验室:所有边都是固定的小图,目标是看清状态。")
  152. warmup()
  153. show(blocks, dfn, low)
  154. print("\n命令:show;delete C;path A G;trace;q。")
  155. while True:
  156. try:
  157. raw = input("\n> ").strip()
  158. except EOFError:
  159. raw = "q"
  160. if not raw or raw in {"q", "quit", "exit"}:
  161. return
  162. parts = raw.split()
  163. if parts == ["show"]:
  164. show(blocks, dfn, low)
  165. elif parts == ["trace"]:
  166. print("\nTarjan 边栈日志:")
  167. for line in trace:
  168. print(line)
  169. elif len(parts) == 2 and parts[0] == "delete" and parts[1] in VERTICES:
  170. removed = parts[1]
  171. components = components_after_delete(removed)
  172. print(f"删 {removed} 后:", " ".join("{" + ",".join(c) + "}" for c in components))
  173. print("连通块数:", len(components))
  174. elif len(parts) == 3 and parts[0] == "path":
  175. path = tree_path(tree, parts[1], parts[2])
  176. if path is None:
  177. print("节点名应为 A..G 或 Q1..Q4。")
  178. else:
  179. print("圆方树唯一路径:", " → ".join(path))
  180. cuts = [node for node in path if node in VERTICES and len(tree[node]) > 1]
  181. print("路径中的割点候选:", cuts or "无")
  182. else:
  183. print("小提示:试试 delete C、path A G、trace。")
  184. if __name__ == "__main__":
  185. main()