| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- #!/usr/bin/env python3
- """课前零食:从删点实验到圆方树的可操作小黑板。"""
- from __future__ import annotations
- from collections import deque
- VERTICES = tuple("ABCDEFG")
- EDGES = (
- ("A", "B"),
- ("B", "C"),
- ("C", "A"),
- ("C", "D"),
- ("D", "E"),
- ("E", "C"),
- ("C", "F"),
- ("F", "G"),
- )
- def make_graph() -> dict[str, list[tuple[str, int]]]:
- graph = {vertex: [] for vertex in VERTICES}
- for edge_id, (u, v) in enumerate(EDGES):
- graph[u].append((v, edge_id))
- graph[v].append((u, edge_id))
- return graph
- GRAPH = make_graph()
- def tarjan_blocks(with_trace: bool = False) -> tuple[list[list[int]], dict[str, int], dict[str, int], list[str]]:
- """返回按原边编号排序的点双边块、dfn、low 和可读日志。"""
- dfn: dict[str, int] = {}
- low: dict[str, int] = {}
- edge_stack: list[int] = []
- blocks: list[list[int]] = []
- trace: list[str] = []
- timer = 0
- def say(message: str) -> None:
- if with_trace:
- trace.append(message)
- def dfs(u: str, parent_edge: int | None) -> None:
- nonlocal timer
- timer += 1
- dfn[u] = low[u] = timer
- say(f"进入 {u}:dfn=low={timer}")
- for v, edge_id in GRAPH[u]:
- if edge_id == parent_edge:
- continue
- if v not in dfn:
- edge_stack.append(edge_id)
- say(f" 压边 {u}-{v};栈={edge_names(edge_stack)}")
- dfs(v, edge_id)
- low[u] = min(low[u], low[v])
- say(f" {v} 返回 {u}:low[{u}] 更新为 {low[u]}")
- if low[v] >= dfn[u]:
- block: list[int] = []
- while True:
- popped = edge_stack.pop()
- block.append(popped)
- if popped == edge_id:
- break
- say(
- f" low[{v}]={low[v]} ≥ dfn[{u}]={dfn[u]}:"
- f"弹出 {edge_names(block)},封成一个点双块"
- )
- blocks.append(block)
- elif dfn[v] < dfn[u]:
- edge_stack.append(edge_id)
- low[u] = min(low[u], dfn[v])
- say(
- f" 看见返祖边 {u}-{v}:压栈,"
- f"low[{u}] 更新为 {low[u]}"
- )
- for start in VERTICES:
- if start not in dfn:
- dfs(start, None)
- blocks.sort(key=min)
- return blocks, dfn, low, trace
- def edge_name(edge_id: int) -> str:
- u, v = EDGES[edge_id]
- return f"{u}-{v}"
- def edge_names(edge_ids: list[int]) -> str:
- return "[" + ", ".join(edge_name(edge_id) for edge_id in edge_ids) + "]"
- def block_members(block: list[int]) -> list[str]:
- return sorted({vertex for edge_id in block for vertex in EDGES[edge_id]})
- def build_block_cut_tree(blocks: list[list[int]]) -> dict[str, list[str]]:
- tree: dict[str, list[str]] = {vertex: [] for vertex in VERTICES}
- for index, block in enumerate(blocks, start=1):
- square = f"Q{index}"
- tree[square] = []
- for vertex in block_members(block):
- tree[vertex].append(square)
- tree[square].append(vertex)
- return tree
- def components_after_delete(removed: str) -> list[list[str]]:
- remaining = set(VERTICES) - {removed}
- components: list[list[str]] = []
- while remaining:
- start = min(remaining)
- queue = deque([start])
- remaining.remove(start)
- component = [start]
- while queue:
- u = queue.popleft()
- for v, _ in GRAPH[u]:
- if v in remaining and v != removed:
- remaining.remove(v)
- queue.append(v)
- component.append(v)
- components.append(sorted(component))
- return components
- def tree_path(tree: dict[str, list[str]], start: str, target: str) -> list[str] | None:
- if start not in tree or target not in tree:
- return None
- parent = {start: None}
- queue = deque([start])
- while queue:
- u = queue.popleft()
- if u == target:
- break
- for v in tree[u]:
- if v not in parent:
- parent[v] = u
- queue.append(v)
- if target not in parent:
- return None
- path: list[str] = []
- current: str | None = target
- while current is not None:
- path.append(current)
- current = parent[current]
- return list(reversed(path))
- def show(blocks: list[list[int]], dfn: dict[str, int], low: dict[str, int]) -> None:
- print("\n原图边:", ", ".join(edge_name(edge_id) for edge_id in range(len(EDGES))))
- print("DFS 账本:", " ".join(f"{u}: dfn={dfn[u]}, low={low[u]}" for u in VERTICES))
- print("点双块(按原边编号排序后命名):")
- for index, block in enumerate(blocks, start=1):
- print(f" Q{index}: 边 {edge_names(block)};成员 {block_members(block)}")
- tree = build_block_cut_tree(blocks)
- print("圆方树邻接表:")
- for node in list(VERTICES) + [f"Q{i}" for i in range(1, len(blocks) + 1)]:
- print(f" {node}: {' '.join(tree[node])}")
- def warmup() -> None:
- print("=== 任务 0:先当删点的暴力程序 ===")
- try:
- input("先预测删掉 C 后会有几块;写在纸上后按回车: ")
- except EOFError:
- pass
- print("核对:删 C 后是 {A,B}、{D,E}、{F,G} 三块。")
- try:
- input("再猜:谁可以同时属于 ABC 和 CDE 两个结实小团?按回车: ")
- except EOFError:
- pass
- print("核对:C。圆方树不复制 C,而是给每个块新建方点。")
- def main() -> None:
- blocks, dfn, low, trace = tarjan_blocks(with_trace=True)
- tree = build_block_cut_tree(blocks)
- print("圆方树小实验室:所有边都是固定的小图,目标是看清状态。")
- warmup()
- show(blocks, dfn, low)
- print("\n命令:show;delete C;path A G;trace;q。")
- while True:
- try:
- raw = input("\n> ").strip()
- except EOFError:
- raw = "q"
- if not raw or raw in {"q", "quit", "exit"}:
- return
- parts = raw.split()
- if parts == ["show"]:
- show(blocks, dfn, low)
- elif parts == ["trace"]:
- print("\nTarjan 边栈日志:")
- for line in trace:
- print(line)
- elif len(parts) == 2 and parts[0] == "delete" and parts[1] in VERTICES:
- removed = parts[1]
- components = components_after_delete(removed)
- print(f"删 {removed} 后:", " ".join("{" + ",".join(c) + "}" for c in components))
- print("连通块数:", len(components))
- elif len(parts) == 3 and parts[0] == "path":
- path = tree_path(tree, parts[1], parts[2])
- if path is None:
- print("节点名应为 A..G 或 Q1..Q4。")
- else:
- print("圆方树唯一路径:", " → ".join(path))
- cuts = [node for node in path if node in VERTICES and len(tree[node]) > 1]
- print("路径中的割点候选:", cuts or "无")
- else:
- print("小提示:试试 delete C、path A G、trace。")
- if __name__ == "__main__":
- main()
|