题目翻译(中文)
在战争中,用公路将所有城市连接起来至关重要。如果一座城市被敌军攻占,那么所有通往或来自该城市的公路都将被封闭。为了保持其余城市的连通性,我们必须以最小的代价修复一部分公路。另一方面,如果失去某座城市后,重建连通需要付出过高的代价,我们就必须对这座城市给予更多关注。
给定一张城市地图,其中所有已毁坏和仍完好的公路都已标注。你需要指出我们最需要重点关注的城市。
输入规格
每个输入文件包含一个测试用例。每个用例首先给出一行,包含两个数字 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 被攻占时:
- 删除顶点
x以及所有与x相连的边; - 在剩余的
N-1个顶点上,求一棵最小生成树; - MST 的总权值恰好等于"让剩余城市全部连通所需的最小修复费用";
- 若剩余图即使修复全部公路仍不连通,则该城市是致命的割点,费用记为无穷大
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
- 模型贴合:0 权完好边与正权毁坏边统一为加权边,Kruskal 直接求出最小修复费;
- 排序可复用:权值不随删点变化,一次排序供 N 轮使用;
- 连通性易判 :并查集选不够
N-2条边即说明剩余图不连通,无需额外 DFS; - 实现简单、常数小:配合路径压缩与按大小合并,性能可靠。
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 条边即提前终止"的剪枝效果显著。
四、代码流程说明
程序由"边处理 + 并查集 + 枚举主控"三部分组成:
- 读入与存储 :读入
N、M,按M动态分配边数组(题目未限制 M 上限,不写死),逐条读入u、v、cost、status; - 一次排序 :
qsort按等效权值升序排列;权值相同时完好边(status=1)优先,保证免费边尽量先被考虑; - 枚举被删城市
banned:dsuInit重置并查集,used(已选边数)与totalCost(修复费)清零;- 顺序扫描边,端点含
banned的边直接跳过; findRoot查找两端根节点:根相同说明会成环,跳过;根不同则unite合并,used++,且仅当该边毁坏时累加cost;- 当
used == N-2时提前结束扫描;
- 结果判定 :
used == N-2记录totalCost,否则记录INF_COST,同时维护最大值worst; - 输出 :
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 稠密/稀疏性能测试。
八、易错点与边界情况
- 完好边绝不能忽略:它虽然不花钱(权 0),但必须参与并查集合并,否则连通分量判断完全错误;
- 目标边数是
N-2而不是N-1:删除一个顶点后只剩N-1个点,生成树边数随之减一; - 不连通必须记
INF且要参与输出 :这类城市是图的割点,反而最危险,INF一定大于任何真实费用; - 全 0 情形输出
0:所有城市修复费都为 0 时,按题意输出单独的0,而不是输出全部城市编号; - 并列最大值的格式 :按编号升序、单个空格分隔、行末无空格 ,用
first标志统一处理; N=1/N=2边界 :targetEdges取max(0, N-2)。剩 0 个或 1 个城市时天然"连通",费用为 0;- 修复费用可能为 0 :题目未规定
Cost > 0,毁坏边也可能权 0,比较器需正确处理同权情况; - 整数溢出 :多条边费用累加可能超过
int范围,累计变量与need数组使用long long; - 重边与自环 :题目未保证无重边。Kruskal 对重边自然选最便宜的一条;自环
u==v时unite返回 0 被安全跳过; - 排序复用的前提:只有"边权不随被删城市变化"成立时才能只排一次------本模型中该前提成立,不要在每轮内部重新排序;
M=0的空输入 :动态分配与qsort都要兼容 0 条边,避免对NULL解引用;- 提前终止剪枝 :
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 最小生成树的延伸问题
- MST 关键边 / 关键活动:判定一条边是否出现在所有 MST 中(先求 MST,再对每条非树边检查能否以同权替换树边);
- 次小生成树 / 严格次小生成树 :在 MST 上预处理路径最大边与次大边,枚举非树边替换,
O(N² + M); - 最小生成森林:图不连通时,Kruskal 对每个连通分量各生成一棵树,正是本题不连通场景下的实际产物;
- MST 敏感性分析(顶点删除):即本题的理论化。进阶做法是先求一棵 MST,删除顶点后用"替换边(replacement edge)"重连断裂处,避免每轮全量重算,可把总复杂度进一步降低。
9.4 并查集的进阶用法
- 可回滚并查集(带撤销):不做路径压缩、用栈记录合并操作,支持离线"加边/删边"问题;
- 离线反向思维 :并查集只能合并不能拆分,遇到"逐个删除"类问题常把过程倒过来变成"逐个加入"(如城市被攻占顺序的逆过程);
- 带权并查集 / 种类并查集:在父指针之外维护到根的权值或敌对关系,用于食物链、账户对账等问题。
9.5 进阶思考
能否只算一次 MST 就回答所有城市?
可以部分复用:若被删城市在 MST 中是叶子,直接摘除即可,费用不变;若删除后 MST 断裂成若干子树,则只需在连接这些子树的非树边中选最小替换边重连。预先用 Tarjan/倍增处理 MST 路径信息后,单次查询可降到 O(log N) 量级,这是从"暴力枚举"走向"工程优化"的典型路径,也是本题 35 分之上值得继续深挖的方向。