00-从散步到欧拉回路.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. #!/usr/bin/env python3
  2. """课前零食:用很小的图观察欧拉路径和 Hierholzer 栈。"""
  3. from __future__ import annotations
  4. from collections import defaultdict, deque
  5. DEFAULT_EDGES = [
  6. ("A", "B"), ("B", "C"), ("C", "A"),
  7. ("A", "D"), ("D", "E"), ("E", "A"),
  8. ]
  9. def ask(prompt: str, default: str = "") -> str:
  10. try:
  11. value = input(prompt).strip()
  12. except EOFError:
  13. value = ""
  14. return value or default
  15. def parse_edges(raw: str) -> list[tuple[str, str]]:
  16. edges: list[tuple[str, str]] = []
  17. for token in raw.replace(",", ",").split(","):
  18. token = token.strip()
  19. if not token:
  20. continue
  21. if "-" not in token:
  22. raise ValueError(f"{token!r} 缺少连字符,例如 A-B")
  23. left, right = (part.strip() for part in token.split("-", 1))
  24. if not left or not right:
  25. raise ValueError(f"{token!r} 的端点不能为空")
  26. edges.append((left, right))
  27. if not edges:
  28. raise ValueError("至少输入一条边")
  29. return edges
  30. def build_graph(edges: list[tuple[str, str]]) -> dict[str, list[tuple[str, int]]]:
  31. graph: dict[str, list[tuple[str, int]]] = defaultdict(list)
  32. for edge_id, (u, v) in enumerate(edges):
  33. graph[u].append((v, edge_id))
  34. graph[v].append((u, edge_id))
  35. return graph
  36. def nonzero_connected(graph: dict[str, list[tuple[str, int]]]) -> bool:
  37. active = [vertex for vertex, neighbors in graph.items() if neighbors]
  38. if not active:
  39. return True
  40. seen = {active[0]}
  41. queue: deque[str] = deque([active[0]])
  42. while queue:
  43. u = queue.popleft()
  44. for v, _ in graph[u]:
  45. if v not in seen:
  46. seen.add(v)
  47. queue.append(v)
  48. return all(vertex in seen for vertex in active)
  49. def hierholzer_trace(
  50. graph: dict[str, list[tuple[str, int]]], start: str, edge_count: int
  51. ) -> list[str]:
  52. used = [False] * edge_count
  53. pointer = {vertex: 0 for vertex in graph}
  54. stack = [start]
  55. reversed_answer: list[str] = []
  56. print("\n--- 栈的动作日志 ---")
  57. while stack:
  58. u = stack[-1]
  59. while (
  60. pointer[u] < len(graph[u])
  61. and used[graph[u][pointer[u]][1]]
  62. ):
  63. pointer[u] += 1
  64. if pointer[u] == len(graph[u]):
  65. finished = stack.pop()
  66. reversed_answer.append(finished)
  67. print(f"{finished} 没有未用边:弹栈,倒序答案为 {reversed_answer}")
  68. continue
  69. v, edge_id = graph[u][pointer[u]]
  70. used[edge_id] = True
  71. stack.append(v)
  72. print(f"走边 #{edge_id}: {u}-{v},栈变成 {' -> '.join(stack)}")
  73. answer = list(reversed(reversed_answer))
  74. return answer
  75. def main() -> None:
  76. print("欧拉小散步:输入边时用 A-B,B-C 这种格式。")
  77. raw = ask("直接回车用双三角形;或输入自己的边: ")
  78. try:
  79. edges = DEFAULT_EDGES if not raw else parse_edges(raw)
  80. except ValueError as error:
  81. print(f"输入不太像一张图:{error}。这次先用预设图。")
  82. edges = DEFAULT_EDGES
  83. graph = build_graph(edges)
  84. degree = {vertex: len(neighbors) for vertex, neighbors in graph.items()}
  85. odd = [vertex for vertex, value in degree.items() if value % 2 == 1]
  86. print("\n边:", ", ".join(f"{u}-{v}" for u, v in edges))
  87. print("度数:", ", ".join(f"{v}:{degree[v]}" for v in sorted(degree)))
  88. print("奇度点:", odd or "没有")
  89. connected = nonzero_connected(graph)
  90. possible = connected and len(odd) in (0, 2)
  91. print("非孤立点连通:", "是" if connected else "否")
  92. if not possible:
  93. print("\n这张图没有欧拉路径:先别让栈背锅,条件已经不成立。")
  94. return
  95. start = odd[0] if odd else min(graph)
  96. print(f"\n条件成立,从 {start} 开始。注意:收集答案的顺序会反过来。")
  97. answer = hierholzer_trace(graph, start, len(edges))
  98. print("\n最终欧拉路线:", " -> ".join(answer))
  99. print(
  100. f"长度检查:{len(answer)} 个顶点,应该是边数 + 1 = {len(edges) + 1}。"
  101. )
  102. if __name__ == "__main__":
  103. main()