PTA刷题集之Battle Over Cities - Hard Version(C语言实现)

题目翻译(中文)

在战争中,用公路将所有城市连接起来至关重要。如果一座城市被敌军攻占,那么所有通往或来自该城市的公路都将被封闭。为了保持其余城市的连通性,我们必须以最小的代价修复一部分公路。另一方面,如果失去某座城市后,重建连通需要付出过高的代价,我们就必须对这座城市给予更多关注。

给定一张城市地图,其中所有已毁坏和仍完好的公路都已标注。你需要指出我们最需要重点关注的城市。

输入规格

每个输入文件包含一个测试用例。每个用例首先给出一行,包含两个数字 N(≤500)和 M,分别表示城市总数与公路数量。随后给出 M 行,每行用 4 个整数描述一条公路:城市1 城市2 费用 状态。其中,城市1 和 城市2 是该公路所连接两座城市的编号(城市编号从 1 到 N);费用 是在必要情况下修复该公路所需投入的成本;状态 取值为 0 或 1:0 表示该公路已毁坏,1 表示该公路正在使用中。

注意:题目保证在战争爆发之前,整个国家是连通的。

输出规格

对于每个测试用例,仅在一行中输出我们最需要守护的城市,也就是:当这座城市被敌军攻占时,重建连通需要付出最大努力的城市。

如果有多个城市需要输出,则按城市编号递增的顺序输出,彼此之间用一个空格分隔,但行末不得有多余空格。如果根本不需要修复任何公路,则直接输出 0。

样例输入 1

text 复制代码
4 5
1 2 1 1
1 3 1 1
2 3 1 0
2 4 1 1
3 4 1 0

样例输出 1

text 复制代码
1 2

样例输入 2

text 复制代码
4 5
1 2 1 1
1 3 1 1
2 3 1 0
2 4 1 1
3 4 2 1

样例输出 2

text 复制代码
0

一、题意精析

把城市看作顶点、公路看作边,每条边有两种状态:

Status 含义 建模后的边权
1 在用(完好) 0(无需花钱即可使用)
0 已毁坏 Cost(需支付修复费才能使用)

当城市 x 被攻占时:

  1. 删除顶点 x 以及所有与 x 相连的边;
  2. 在剩余的 N-1 个顶点上,求一棵最小生成树;
  3. MST 的总权值恰好等于"让剩余城市全部连通所需的最小修复费用";
  4. 若剩余图即使修复全部公路仍不连通,则该城市是致命的割点,费用记为无穷大 INF。

对 1..N 每个城市都算一遍,取费用最大者;最大费用为 0 时输出 0。

为什么 MST 权值就是最小修复费?

MST 是连接全部顶点的总权值最小的边子集。完好边权为 0,会被优先、免费地选入;剩下无法靠完好边连通的部分,必须选用毁坏边,其权值之和就是最少需要花的修复费。最小生成树的"最小性"保证了没有更省钱的修复方案。


二、解题思路

2.1 总体策略:枚举删点 + Kruskal

复制代码
对每个被攻占城市 banned = 1 .. N:
    初始化并查集
    按权值从小到大扫描所有边:
        跳过端点含 banned 的边
        若边的两端不在同一集合(加入不成环):
            选入该边,合并集合
            若为毁坏边,累加其修复费用
    选够 N-2 条边 → 连通,记录总费用
    否则          → 不连通,记录 INF
取所有城市费用的最大值,按规则输出

2.2 关键优化:排序只做一次

边的等效权值(status==1 ? 0 : cost)与删除哪个城市无关,因此全部边只需在枚举前排序一次,之后 N 轮 Kruskal 共用这份有序序列,避免了 N 次排序的开销。

2.3 样例 1 手工推演

等效权值:在用边权 0,毁坏边 2-3、3-4 权 1。

被删城市 剩余顶点 选入 MST 的边 修复费用
1 2,3,4 2-4(0)、2-3(1) 1
2 1,3,4 1-3(0)、3-4(1) 1
3 1,2,4 1-2(0)、2-4(0) 0
4 1,2,3 1-2(0)、1-3(0) 0

最大费用为 1,对应城市 1 2,与样例输出一致。

样例 2 中 3-4 变为在用边,删除任意城市后剩余顶点都能被权 0 的在用边连通,费用全为 0,故输出 0。


三、算法选型与复杂度

3.1 候选方案对比

方案 单次删点 总时间复杂度 适用评价
Kruskal + 并查集(排序一次复用) O(M·α(N)) O(M log M + N·M·α(N)) 稀疏/稠密都稳,天然处理 0 权边与连通性判定,首选
Prim(邻接矩阵) O(N²) O(N³) N=500 约 1.25 亿次操作,常数小,稠密图可接受,稀疏图浪费
Prim(邻接表 + 二叉堆) O(M log N) O(N·M log N) 稀疏图优秀,但代码更繁,且 0 权边优势不明显

α(N) 为反阿克曼函数,增长极慢,在 N ≤ 500 时可视为常数(α ≤ 3)。

3.2 为什么选 Kruskal

  1. 模型贴合:0 权完好边与正权毁坏边统一为加权边,Kruskal 直接求出最小修复费;
  2. 排序可复用:权值不随删点变化,一次排序供 N 轮使用;
  3. 连通性易判 :并查集选不够 N-2 条边即说明剩余图不连通,无需额外 DFS;
  4. 实现简单、常数小:配合路径压缩与按大小合并,性能可靠。

3.3 复杂度与实测

  • 时间复杂度 :O(M log M + N·M·α(N))
  • 空间复杂度 :O(N + M)(边数组 + 并查集数组)
  • 实测(N=500,gcc -O2):
数据形态 边数 运行耗时
链状稀疏图(含不连通割点) 499 约 6 ms
中等密度图 2,000 约 8 ms
完全稠密图 124,750 约 41 ms

均远低于 800 ms 限制。稠密图中"选够 N-2 条边即提前终止"的剪枝效果显著。


四、代码流程说明

程序由"边处理 + 并查集 + 枚举主控"三部分组成:

  1. 读入与存储 :读入 N、M,按 M 动态分配边数组(题目未限制 M 上限,不写死),逐条读入 u、v、cost、status;
  2. 一次排序 :qsort 按等效权值升序排列;权值相同时完好边(status=1)优先,保证免费边尽量先被考虑;
  3. 枚举被删城市 banned:
    • dsuInit 重置并查集,used(已选边数)与 totalCost(修复费)清零;
    • 顺序扫描边,端点含 banned 的边直接跳过;
    • findRoot 查找两端根节点:根相同说明会成环,跳过;根不同则 unite 合并,used++,且仅当该边毁坏时累加 cost;
    • 当 used == N-2 时提前结束扫描;
  4. 结果判定 :used == N-2 记录 totalCost,否则记录 INF_COST,同时维护最大值 worst;
  5. 输出 :worst == 0 输出 0;否则按编号升序输出所有费用等于 worst 的城市,用 first 标志控制空格,保证行末无多余空格。

五、解题流程图

#mermaid-svg-roqJnIkHjzBpe8dr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-roqJnIkHjzBpe8dr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-roqJnIkHjzBpe8dr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-roqJnIkHjzBpe8dr .error-icon{fill:#552222;}#mermaid-svg-roqJnIkHjzBpe8dr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-roqJnIkHjzBpe8dr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-roqJnIkHjzBpe8dr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-roqJnIkHjzBpe8dr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-roqJnIkHjzBpe8dr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-roqJnIkHjzBpe8dr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-roqJnIkHjzBpe8dr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-roqJnIkHjzBpe8dr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-roqJnIkHjzBpe8dr .marker.cross{stroke:#333333;}#mermaid-svg-roqJnIkHjzBpe8dr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-roqJnIkHjzBpe8dr p{margin:0;}#mermaid-svg-roqJnIkHjzBpe8dr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-roqJnIkHjzBpe8dr .cluster-label text{fill:#333;}#mermaid-svg-roqJnIkHjzBpe8dr .cluster-label span{color:#333;}#mermaid-svg-roqJnIkHjzBpe8dr .cluster-label span p{background-color:transparent;}#mermaid-svg-roqJnIkHjzBpe8dr .label text,#mermaid-svg-roqJnIkHjzBpe8dr span{fill:#333;color:#333;}#mermaid-svg-roqJnIkHjzBpe8dr .node rect,#mermaid-svg-roqJnIkHjzBpe8dr .node circle,#mermaid-svg-roqJnIkHjzBpe8dr .node ellipse,#mermaid-svg-roqJnIkHjzBpe8dr .node polygon,#mermaid-svg-roqJnIkHjzBpe8dr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-roqJnIkHjzBpe8dr .rough-node .label text,#mermaid-svg-roqJnIkHjzBpe8dr .node .label text,#mermaid-svg-roqJnIkHjzBpe8dr .image-shape .label,#mermaid-svg-roqJnIkHjzBpe8dr .icon-shape .label{text-anchor:middle;}#mermaid-svg-roqJnIkHjzBpe8dr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-roqJnIkHjzBpe8dr .rough-node .label,#mermaid-svg-roqJnIkHjzBpe8dr .node .label,#mermaid-svg-roqJnIkHjzBpe8dr .image-shape .label,#mermaid-svg-roqJnIkHjzBpe8dr .icon-shape .label{text-align:center;}#mermaid-svg-roqJnIkHjzBpe8dr .node.clickable{cursor:pointer;}#mermaid-svg-roqJnIkHjzBpe8dr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-roqJnIkHjzBpe8dr .arrowheadPath{fill:#333333;}#mermaid-svg-roqJnIkHjzBpe8dr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-roqJnIkHjzBpe8dr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-roqJnIkHjzBpe8dr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-roqJnIkHjzBpe8dr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-roqJnIkHjzBpe8dr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-roqJnIkHjzBpe8dr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-roqJnIkHjzBpe8dr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-roqJnIkHjzBpe8dr .cluster text{fill:#333;}#mermaid-svg-roqJnIkHjzBpe8dr .cluster span{color:#333;}#mermaid-svg-roqJnIkHjzBpe8dr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-roqJnIkHjzBpe8dr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-roqJnIkHjzBpe8dr rect.text{fill:none;stroke-width:0;}#mermaid-svg-roqJnIkHjzBpe8dr .icon-shape,#mermaid-svg-roqJnIkHjzBpe8dr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-roqJnIkHjzBpe8dr .icon-shape p,#mermaid-svg-roqJnIkHjzBpe8dr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-roqJnIkHjzBpe8dr .icon-shape .label rect,#mermaid-svg-roqJnIkHjzBpe8dr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-roqJnIkHjzBpe8dr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-roqJnIkHjzBpe8dr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-roqJnIkHjzBpe8dr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
是(成环)
否
是
否
否,继续扫描
是,或边已扫完
是,连通
否,不连通
是
否
是
否
开始
读入 N、M 及 M 条公路
计算等效权值:在用=0,毁坏=Cost
按权值升序排序(仅一次)
枚举被攻占城市 banned
初始化并查集;used=0,total=0
按序取下一条边
端点包含 banned?
两端是否同一集合?
合并集合,used 加 1
是否为毁坏边?
total 累加 Cost
used 等于 N-2?
used 等于 N-2?
needbanned = total
needbanned = INF
还有未枚举城市?
worst = max(need)
worst 等于 0?
输出 0
升序输出所有 need=worst 的城市
结束


六、代码流程图

#mermaid-svg-uXdNP6xfwcghAIUm{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-uXdNP6xfwcghAIUm .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-uXdNP6xfwcghAIUm .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-uXdNP6xfwcghAIUm .error-icon{fill:#552222;}#mermaid-svg-uXdNP6xfwcghAIUm .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-uXdNP6xfwcghAIUm .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-uXdNP6xfwcghAIUm .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-uXdNP6xfwcghAIUm .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-uXdNP6xfwcghAIUm .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-uXdNP6xfwcghAIUm .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-uXdNP6xfwcghAIUm .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-uXdNP6xfwcghAIUm .marker{fill:#333333;stroke:#333333;}#mermaid-svg-uXdNP6xfwcghAIUm .marker.cross{stroke:#333333;}#mermaid-svg-uXdNP6xfwcghAIUm svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-uXdNP6xfwcghAIUm p{margin:0;}#mermaid-svg-uXdNP6xfwcghAIUm .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-uXdNP6xfwcghAIUm .cluster-label text{fill:#333;}#mermaid-svg-uXdNP6xfwcghAIUm .cluster-label span{color:#333;}#mermaid-svg-uXdNP6xfwcghAIUm .cluster-label span p{background-color:transparent;}#mermaid-svg-uXdNP6xfwcghAIUm .label text,#mermaid-svg-uXdNP6xfwcghAIUm span{fill:#333;color:#333;}#mermaid-svg-uXdNP6xfwcghAIUm .node rect,#mermaid-svg-uXdNP6xfwcghAIUm .node circle,#mermaid-svg-uXdNP6xfwcghAIUm .node ellipse,#mermaid-svg-uXdNP6xfwcghAIUm .node polygon,#mermaid-svg-uXdNP6xfwcghAIUm .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-uXdNP6xfwcghAIUm .rough-node .label text,#mermaid-svg-uXdNP6xfwcghAIUm .node .label text,#mermaid-svg-uXdNP6xfwcghAIUm .image-shape .label,#mermaid-svg-uXdNP6xfwcghAIUm .icon-shape .label{text-anchor:middle;}#mermaid-svg-uXdNP6xfwcghAIUm .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-uXdNP6xfwcghAIUm .rough-node .label,#mermaid-svg-uXdNP6xfwcghAIUm .node .label,#mermaid-svg-uXdNP6xfwcghAIUm .image-shape .label,#mermaid-svg-uXdNP6xfwcghAIUm .icon-shape .label{text-align:center;}#mermaid-svg-uXdNP6xfwcghAIUm .node.clickable{cursor:pointer;}#mermaid-svg-uXdNP6xfwcghAIUm .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-uXdNP6xfwcghAIUm .arrowheadPath{fill:#333333;}#mermaid-svg-uXdNP6xfwcghAIUm .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-uXdNP6xfwcghAIUm .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-uXdNP6xfwcghAIUm .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uXdNP6xfwcghAIUm .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-uXdNP6xfwcghAIUm .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uXdNP6xfwcghAIUm .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-uXdNP6xfwcghAIUm .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-uXdNP6xfwcghAIUm .cluster text{fill:#333;}#mermaid-svg-uXdNP6xfwcghAIUm .cluster span{color:#333;}#mermaid-svg-uXdNP6xfwcghAIUm div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-uXdNP6xfwcghAIUm .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-uXdNP6xfwcghAIUm rect.text{fill:none;stroke-width:0;}#mermaid-svg-uXdNP6xfwcghAIUm .icon-shape,#mermaid-svg-uXdNP6xfwcghAIUm .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-uXdNP6xfwcghAIUm .icon-shape p,#mermaid-svg-uXdNP6xfwcghAIUm .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-uXdNP6xfwcghAIUm .icon-shape .label rect,#mermaid-svg-uXdNP6xfwcghAIUm .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-uXdNP6xfwcghAIUm .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-uXdNP6xfwcghAIUm .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-uXdNP6xfwcghAIUm :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
否
否(同根成环)
是
是
否
扫描结束
全部枚举完
是
否
main() 主控
scanf 读入 N、M
malloc 动态分配 EdgeM
循环读入 M 条边
qsort(edges, cmpEdge) 一次排序
for banned = 1 .. N
dsuInit():父指针指向自身,集合大小=1
for 每条边

且 used < N-2
端点等于 banned?
调用 unite(u, v)
findRoot():隔代路径压缩
合并是否成功?
按大小挂接;used++
status 等于 0?
totalCost += cost
used==N-2 ? 记 totalCost : 记 INF
求 worst
worst 等于 0?
printf 0
first 标志控制空格,升序输出
结束

并查集两个核心操作的原理

  • findRoot(x)(查找 + 路径压缩) :沿父指针向上找根,同时把沿途节点直接挂到祖父节点,树被不断摊平,后续查找近乎 O(1);
  • unite(a,b)(按大小合并):把较小集合的根挂到较大集合的根下,避免树退化成链;两节点同根则返回 0,正是 Kruskal 用来判环的依据。

七、代码实现(C11)

7.1 实现原理总述

  • 边权映射 :完好边权 0、毁坏边权 cost,把"是否修复"的决策转化为标准加权 MST 问题;
  • Kruskal 贪心:按权值从小到大选边,借助并查集拒绝成环边,直到选够生成树所需边数;
  • 并查集加速 :路径压缩 + 按大小合并,使每次查找/合并摊还代价为 α(N);
  • 不连通处理 :剩余 N-1 个点的生成树需要 N-2 条边,选不够即不连通,记 INF;
  • 类型安全 :修复费用用 long long 累加,防止大费用求和溢出;边数动态分配,兼容任意 M。

7.2 完整源码

c 复制代码
#include <stdio.h>
#include <stdlib.h>

/* 不连通时的"无穷代价":即使修复全部公路也无法连通,必然最大 */
#define INF_COST (1LL << 60)
#define MAX_N 505

/* 一条公路(图的一条边) */
typedef struct {
    int u;       /* 端点城市 1(1 起始编号) */
    int v;       /* 端点城市 2 */
    int cost;    /* 该公路毁坏时的修复费用 */
    int status;  /* 1 = 在用;0 = 已毁坏 */
} Edge;

/* 并查集:父指针与集合大小(按大小合并,保证树高对数级) */
static int parent[MAX_N];
static int dsuSize[MAX_N];

/* 边在最小生成树中的等效权值:在用公路免费(0),毁坏公路取修复费用 */
static int edgeWeight(const Edge *e) {
    return e->status == 1 ? 0 : e->cost;
}

/* qsort 比较器:先按等效权值升序;权值相同时在用边(status=1)优先 */
static int cmpEdge(const void *a, const void *b) {
    const Edge *ea = (const Edge *)a;
    const Edge *eb = (const Edge *)b;
    int wa = edgeWeight(ea);
    int wb = edgeWeight(eb);
    if (wa != wb) {
        return wa < wb ? -1 : 1;
    }
    return eb->status - ea->status;
}

/* 初始化并查集:每个城市各自成一个集合 */
static void dsuInit(int n) {
    for (int i = 1; i <= n; ++i) {
        parent[i] = i;
        dsuSize[i] = 1;
    }
}

/* 带路径压缩的查找:返回 x 所在集合的根 */
static int findRoot(int x) {
    while (parent[x] != x) {
        parent[x] = parent[parent[x]]; /* 隔代路径压缩,摊平查找树 */
        x = parent[x];
    }
    return x;
}

/* 合并两个集合:原本不同集合返回 1,已在同一集合(会成环)返回 0 */
static int unite(int a, int b) {
    a = findRoot(a);
    b = findRoot(b);
    if (a == b) {
        return 0;
    }
    /* 小集合挂到大集合之下,控制树高 */
    if (dsuSize[a] < dsuSize[b]) {
        int tmp = a;
        a = b;
        b = tmp;
    }
    parent[b] = a;
    dsuSize[a] += dsuSize[b];
    return 1;
}

int main(void) {
    int N, M;
    if (scanf("%d %d", &N, &M) != 2) {
        return 0;
    }

    /* 边数题目未给上限,按实际 M 动态分配 */
    Edge *edges = NULL;
    if (M > 0) {
        edges = (Edge *)malloc((size_t)M * sizeof(Edge));
        if (edges == NULL) {
            return 0;
        }
    }
    for (int i = 0; i < M; ++i) {
        if (scanf("%d %d %d %d", &edges[i].u, &edges[i].v,
                  &edges[i].cost, &edges[i].status) != 4) {
            free(edges);
            return 0;
        }
    }

    /* 所有轮次共用同一份排序结果:等效权值不随被删城市变化,只需排一次 */
    if (M > 0) {
        qsort(edges, (size_t)M, sizeof(Edge), cmpEdge);
    }

    long long need[MAX_N]; /* need[i]:城市 i 被攻占后的最小修复代价 */
    long long worst = 0;   /* 全部城市中的最大代价 */

    /* 删除一个城市后剩 N-1 个点,其生成树需要 N-2 条边;N<=2 时为 0 */
    int targetEdges = N - 2;
    if (targetEdges < 0) {
        targetEdges = 0;
    }

    /* 枚举每一座"被攻占"的城市 */
    for (int banned = 1; banned <= N; ++banned) {
        dsuInit(N);
        int used = 0;                 /* 已选入生成树的边数 */
        long long totalCost = 0;      /* 已选毁坏公路的修复费用累计 */

        /* Kruskal:按权值从小到大扫描,跳过所有关联被删城市的边 */
        for (int i = 0; i < M && used < targetEdges; ++i) {
            if (edges[i].u == banned || edges[i].v == banned) {
                continue;
            }
            if (unite(edges[i].u, edges[i].v)) {
                ++used;
                /* 在用边权值为 0,不产生费用;仅毁坏边累加修复费 */
                if (edges[i].status == 0) {
                    totalCost += edges[i].cost;
                }
            }
        }

        /* 选够 N-2 条边说明剩余城市连通;否则剩余图不连通,记为无穷代价 */
        need[banned] = (used == targetEdges) ? totalCost : INF_COST;
        if (need[banned] > worst) {
            worst = need[banned];
        }
    }

    free(edges);

    /* 最大代价为 0:任何城市失守都无需修复,按题意输出 0 */
    if (worst == 0) {
        printf("0\n");
        return 0;
    }

    /* 按编号升序输出所有代价等于最大值的城市,行末无多余空格 */
    int first = 1;
    for (int city = 1; city <= N; ++city) {
        if (need[city] != worst) {
            continue;
        }
        if (!first) {
            printf(" ");
        }
        printf("%d", city);
        first = 0;
    }
    printf("\n");
    return 0;
}

编译与验证

bash 复制代码
gcc -std=c11 -Wall -Wextra -Werror -O2 battle.c -o battle

已在以下场景全部验证通过:两个官方样例、N=1、N=2、删点后不连通(INF)、全毁坏边并列、N=500 稠密/稀疏性能测试。


八、易错点与边界情况

  1. 完好边绝不能忽略:它虽然不花钱(权 0),但必须参与并查集合并,否则连通分量判断完全错误;
  2. 目标边数是 N-2 而不是 N-1 :删除一个顶点后只剩 N-1 个点,生成树边数随之减一;
  3. 不连通必须记 INF 且要参与输出 :这类城市是图的割点,反而最危险,INF 一定大于任何真实费用;
  4. 全 0 情形输出 0 :所有城市修复费都为 0 时,按题意输出单独的 0,而不是输出全部城市编号;
  5. 并列最大值的格式 :按编号升序、单个空格分隔、行末无空格 ,用 first 标志统一处理;
  6. N=1 / N=2 边界 :targetEdges 取 max(0, N-2)。剩 0 个或 1 个城市时天然"连通",费用为 0;
  7. 修复费用可能为 0 :题目未规定 Cost > 0,毁坏边也可能权 0,比较器需正确处理同权情况;
  8. 整数溢出 :多条边费用累加可能超过 int 范围,累计变量与 need 数组使用 long long;
  9. 重边与自环 :题目未保证无重边。Kruskal 对重边自然选最便宜的一条;自环 u==v 时 unite 返回 0 被安全跳过;
  10. 排序复用的前提:只有"边权不随被删城市变化"成立时才能只排一次------本模型中该前提成立,不要在每轮内部重新排序;
  11. M=0 的空输入 :动态分配与 qsort 都要兼容 0 条边,避免对 NULL 解引用;
  12. 提前终止剪枝 :used == N-2 后立即停止扫描,稠密图下可大幅减少无谓遍历。

九、同类变体与延伸

9.1 直接相关的姊妹题

题目 关系 解法差异
PAT Advanced 1013 Battle Over Cities(25 分) 本题的简单版 不考虑费用,只统计删点后的连通分量数 k,答案即 k-1;DFS/BFS 或并查集即可
PAT Top 1001(本题) 进阶版 在数分量的基础上引入边权,用 MST 求最小修复费

9.2 割点与桥(Tarjan 系列)

  • 本题中"删点后不连通"的顶点,本质就是无向图的关节点(割点);
  • Tarjan 割点算法 只需一次 DFS,O(N+M) 即可求出所有割点及其分裂出的分量数,可用来快速预判哪些城市会得到 INF;
  • 对应的**割边(桥)**算法用于判断删除某条边是否导致图不连通,是 MST 关键边判定的基础。

9.3 最小生成树的延伸问题

  1. MST 关键边 / 关键活动:判定一条边是否出现在所有 MST 中(先求 MST,再对每条非树边检查能否以同权替换树边);
  2. 次小生成树 / 严格次小生成树 :在 MST 上预处理路径最大边与次大边,枚举非树边替换,O(N² + M);
  3. 最小生成森林:图不连通时,Kruskal 对每个连通分量各生成一棵树,正是本题不连通场景下的实际产物;
  4. MST 敏感性分析(顶点删除):即本题的理论化。进阶做法是先求一棵 MST,删除顶点后用"替换边(replacement edge)"重连断裂处,避免每轮全量重算,可把总复杂度进一步降低。

9.4 并查集的进阶用法

  • 可回滚并查集(带撤销):不做路径压缩、用栈记录合并操作,支持离线"加边/删边"问题;
  • 离线反向思维 :并查集只能合并不能拆分,遇到"逐个删除"类问题常把过程倒过来变成"逐个加入"(如城市被攻占顺序的逆过程);
  • 带权并查集 / 种类并查集:在父指针之外维护到根的权值或敌对关系,用于食物链、账户对账等问题。

9.5 进阶思考

能否只算一次 MST 就回答所有城市?

可以部分复用:若被删城市在 MST 中是叶子,直接摘除即可,费用不变;若删除后 MST 断裂成若干子树,则只需在连接这些子树的非树边中选最小替换边重连。预先用 Tarjan/倍增处理 MST 路径信息后,单次查询可降到 O(log N) 量级,这是从"暴力枚举"走向"工程优化"的典型路径,也是本题 35 分之上值得继续深挖的方向。

相关推荐
吴长建先生重名了1 小时前
电商订单系统在Docker和Kubernetes中部署的高
算法
Дерек的学习记录2 小时前
Unreal Engine 5:第三人称运动系统和人物动画
开发语言·学习·ue5
Ramble_Naylor2 小时前
Rust深度解读:异步流(Async Streams)与生成器的处理艺术
开发语言·后端·rust
努力实干2 小时前
简单工厂模式(Simple Factory Pattern)——C++
开发语言·c++·简单工厂模式
熊猫钓鱼>_>2 小时前
平行视界兼容模式与魔法变量配置避坑【鸿蒙心迹】
运维·服务器·开发语言·笔记·华为·harmonyos·openharmony
大鹏说大话2 小时前
PHP 大文件上传解决方案:分片上传完整思路
开发语言·php
Wang's Blog3 小时前
Java 项目实战:交友项目 - 我的访客:需求说明
java·开发语言·交友
東隅已逝,桑榆非晚4 小时前
算法2. 双指针法-复写零
c++·笔记·学习·算法
布莱克6054 小时前
C++ 中堆和栈的区别详解
开发语言·c++·栈·堆