题目描述
敌人有 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) 对的最小割
代码实现
cpp
// 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) 是完全可行的。