UVa 10989 Bomb Divide and Conquer
题目描述
敌人有 nnn 个城市,通过 mmm 条道路连接。我们拥有可以摧毁道路的轰炸机。"轰炸、分治"策略指出,如果我们将敌人的城市彼此分离,那么两个(或更多)部分将更容易控制。轰炸一条道路有相应的成本(燃料、风险因素等)。确保存在至少一对城市之间没有路径所需的最小总轰炸成本是多少?
输入格式:
- 第一行给出测试用例的数量 NNN
- 每个测试用例包含:
- 两个整数 nnn (2≤n≤1502 \leq n \leq 1502≤n≤150) 和 mmm (0≤m≤n(n−1)/20 \leq m \leq n(n-1)/20≤m≤n(n−1)/2)
- mmm 行,每行包含三个整数:两个不同的城市编号(111 到 nnn)和摧毁该道路的成本(111 到 100010001000)
输出格式:
- 对于每个测试用例,输出一行:
Case #x:后跟断开某对城市连接的总成本
题目分析
问题本质
题目要求通过摧毁一些道路,使得图变得不连通,即至少存在一对城市之间没有路径。这等价于将原图分成至少两个连通分量。
我们需要找到摧毁道路的最小总成本,使得图不再连通。
关键洞察
-
图连通性:如果原图已经是不连通的,那么不需要摧毁任何道路,答案为 000
-
最小割问题:对于连通图,我们需要找到最小割——将顶点分成两个集合 SSS 和 TTT,使得连接 SSS 和 TTT 的所有边的权重之和最小
-
最大流最小割定理:在流网络中,从源点 sss 到汇点 ttt 的最大流值等于分离 sss 和 ttt 的最小割的容量
算法选择
由于 n≤150n \leq 150n≤150,我们可以使用以下方法:
- 固定一个源点 sss
- 枚举所有其他顶点作为汇点 ttt
- 对于每个 (s,t)(s, t)(s,t) 对,计算最大流(即最小割)
- 取所有 (s,t)(s, t)(s,t) 对的最小割的最小值作为答案
我们选择 Dinic\texttt{Dinic}Dinic 算法 来计算最大流,因为它在实践中效率较高。
复杂度分析
- Dinic\texttt{Dinic}Dinic 算法复杂度:O(V2E)O(V^2E)O(V2E)
- 我们需要计算 n−1n-1n−1 次最大流
- 总复杂度:O(n⋅V2E)O(n \cdot V^2E)O(n⋅V2E),在 n≤150n \leq 150n≤150 时可行
解题思路详解
步骤 1:问题建模
将问题转化为图论模型:
- 顶点:城市(编号 000 到 n−1n-1n−1)
- 边:道路,边权 = 摧毁成本
- 目标:找到全局最小割
步骤 2:算法原理
根据全局最小割定理,无向图的全局最小割可以通过以下方式求得:
- 固定一个源点 sss(通常选择顶点 000)
- 对于每个其他顶点 ttt,计算 sss-ttt 最小割
- 全局最小割 = mint≠smincut(s,t)\min\limits_{t \neq s} \text{mincut}(s, t)t=sminmincut(s,t)
步骤 3:Dinic\texttt{Dinic}Dinic 算法实现
Dinic\texttt{Dinic}Dinic 算法包含三个主要部分:
-
分层图构建(BFS\texttt{BFS}BFS):
- 从源点开始,为每个顶点分配层级
- 只允许从低层级向高层级发送流量
-
阻塞流计算(DFS\texttt{DFS}DFS):
- 在当前分层图中寻找增广路径
- 使用当前弧优化避免重复检查边
-
迭代过程:
- 重复构建分层图和计算阻塞流,直到无法找到增广路径
步骤 4:特殊情况处理
- 如果原图不连通,最小割为 000
- 对于单边情况,最小割可能是该边的权重
- 对于复杂网络,需要计算多个 (s,t)(s, t)(s,t) 对的最小割
代码实现
// Bomb Divide and Conquer
// UVa ID: 10989
// Verdict: Accepted
// Submission Date: 2025-11-02
// UVa Run Time: 0.160s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net#include <bits/stdc++.h>using namespace std;/*** Dinic 最大流算法类* 用于计算无向图的最小割*/
struct DinicMaxFlow {// 边结构体struct FlowEdge {int targetNode; // 目标节点int reverseEdgeIndex; // 反向边在邻接表中的索引int capacity; // 边容量int currentFlow; // 当前流量};int totalNodes; // 总节点数vector<vector<FlowEdge>> graph; // 图的邻接表表示vector<int> nodeLevel; // 节点的层级(用于分层图)vector<int> currentEdgePointer; // 当前弧指针(用于优化)// 构造函数DinicMaxFlow(int nodeCount) : totalNodes(nodeCount), graph(nodeCount), nodeLevel(nodeCount), currentEdgePointer(nodeCount) {}/*** 添加无向边* @param from 起始节点* @param to 目标节点 * @param cap 边的容量*/void addUndirectedEdge(int from, int to, int cap) {// 添加正向边graph[from].push_back({to, (int)graph[to].size(), cap, 0});// 添加反向边(无向图,容量相同)graph[to].push_back({from, (int)graph[from].size() - 1, cap, 0});}/*** 使用 BFS 构建分层图* @param source 源点* @param sink 汇点* @return 如果汇点可达返回 true,否则 false*/bool buildLevelGraph(int source, int sink) {fill(nodeLevel.begin(), nodeLevel.end(), -1);nodeLevel[source] = 0;queue<int> bfsQueue;bfsQueue.push(source);while (!bfsQueue.empty()) {int currentNode = bfsQueue.front();bfsQueue.pop();for (const FlowEdge& edge : graph[currentNode]) {if (nodeLevel[edge.targetNode] == -1 && edge.currentFlow < edge.capacity) {nodeLevel[edge.targetNode] = nodeLevel[currentNode] + 1;bfsQueue.push(edge.targetNode);}}}return nodeLevel[sink] != -1;}/*** 使用 DFS 在分层图中寻找阻塞流* @param currentNode 当前节点* @param sink 汇点* @param minCapacity 路径上的最小容量* @return 实际推送的流量*/int findBlockingFlow(int currentNode, int sink, int minCapacity) {if (currentNode == sink) return minCapacity;for (int& edgeIndex = currentEdgePointer[currentNode]; edgeIndex < (int)graph[currentNode].size(); edgeIndex++) {FlowEdge& edge = graph[currentNode][edgeIndex];if (nodeLevel[edge.targetNode] == nodeLevel[currentNode] + 1 && edge.currentFlow < edge.capacity) {int pushedFlow = findBlockingFlow(edge.targetNode, sink, min(minCapacity, edge.capacity - edge.currentFlow));if (pushedFlow > 0) {edge.currentFlow += pushedFlow;graph[edge.targetNode][edge.reverseEdgeIndex].currentFlow -= pushedFlow;return pushedFlow;}}}return 0;}/*** 计算从 source 到 sink 的最大流* @param source 源点* @param sink 汇点* @return 最大流值(即最小割值)*/int computeMaxFlow(int source, int sink) {int totalFlow = 0;// 反复构建分层图并寻找阻塞流while (buildLevelGraph(source, sink)) {fill(currentEdgePointer.begin(), currentEdgePointer.end(), 0);while (int pushedFlow = findBlockingFlow(source, sink, INT_MAX)) {totalFlow += pushedFlow;}}// 重置所有边的流量,为下一次计算做准备resetFlow();return totalFlow;}private:/*** 重置图中所有边的流量为 0*/void resetFlow() {for (int i = 0; i < totalNodes; i++) {for (FlowEdge& edge : graph[i]) {edge.currentFlow = 0;}}}
};int main() {cin.tie(0), cout.tie(0), ios::sync_with_stdio(false);int T;cin >> T;for (int cs= 1; cs <= T; cs++) {int cityCount, roadCount;cin >> cityCount >> roadCount;// 初始化 Dinic 算法实例DinicMaxFlow flowSolver(cityCount);// 读取道路信息并构建图for (int i = 0; i < roadCount; i++) {int cityA, cityB, destroyCost;cin >> cityA >> cityB >> destroyCost;cityA--; cityB--; // 转换为 0-based 索引flowSolver.addUndirectedEdge(cityA, cityB, destroyCost);}// 计算全局最小割int minCutValue = INT_MAX;int sourceNode = 0; // 固定源点为第一个城市// 枚举所有其他城市作为汇点for (int sinkNode = 1; sinkNode < cityCount; sinkNode++) {minCutValue = min(minCutValue, flowSolver.computeMaxFlow(sourceNode, sinkNode));}// 处理图原本不连通的情况if (minCutValue == INT_MAX) minCutValue = 0;cout << "Case #" << caseNumber << ": " << minCutValue << '\n';}return 0;
}
总结
本题的关键在于将实际问题转化为图论中的最小割问题,并利用最大流最小割定理来求解。通过固定源点、枚举汇点的方法,结合高效的 Dinic\texttt{Dinic}Dinic 最大流算法,我们能够在合理的时间内解决问题。
算法的时间复杂度为 O(n⋅V2E)O(n \cdot V^2E)O(n⋅V2E),空间复杂度为 O(V+E)O(V + E)O(V+E),对于题目给定的数据范围 (n≤150n \leq 150n≤150) 是完全可行的。
