题目分析
问题描述
我们有一个包含 nnn 个女孩的社交网络,编号从 000 到 n−1n-1n−1,其中 000 号女孩是消息的起点。女孩之间可以通过有向的电话呼叫传播消息,每个呼叫 (u,v,w)(u, v, w)(u,v,w) 表示 uuu 可以打电话给 vvv,费用为 www 分。
目标:从 000 号女孩出发,通过一系列电话呼叫让所有女孩都听到消息,并且总费用最小。
约束:
- 如果无法让所有女孩都收到消息,输出 Possums!\texttt{Possums!}Possums!
- 输入规模:n≤1000n \leq 1000n≤1000,m≤40000m \leq 40000m≤40000
问题本质
这是一个典型的有向图最小生成树问题,更准确地说,是最小树形图(Minimum Spanning Arborescence\texttt{Minimum Spanning Arborescence}Minimum Spanning Arborescence)问题:
- 有向性:电话呼叫是单向的
- 根节点固定:必须从 000 号女孩开始
- 连通性要求:必须到达所有节点
算法选择
对于有向图的最小生成树问题,常用的算法是朱-刘算法(Chu–Liu/Edmonds’ algorithm\texttt{Chu–Liu/Edmonds' algorithm}Chu–Liu/Edmonds’ algorithm),该算法专门解决有向图中从固定根节点出发的最小树形图问题。
朱-刘算法详解
算法步骤
- 寻找最小入边:对于每个非根节点,选择权值最小的入边
- 检测环:检查这些边是否形成环
- 如果无环,则已找到最小树形图,算法结束
- 如果有环,继续步骤3
- 缩环:将环收缩为一个超级节点,更新相关边的权值
- 重复过程:在收缩后的图上重复上述步骤
算法复杂度
- 时间复杂度:O(n×m)O(n \times m)O(n×m)
- 空间复杂度:O(n+m)O(n + m)O(n+m)
关键点说明
- 入边选择:每个非根节点必须至少有一条入边,否则无解
- 权值更新:缩环时,指向环内节点的边权值需要减去环内该节点入边的权值
- 解的存在性:如果某个非根节点没有入边,则无法形成树形图
代码实现
// Teen Girl Squad
// UVa ID: 11183
// Verdict: Accepted
// Submission Date: 2025-10-24
// UVa Run Time: 0.030s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int MAX_NODE = 1005;
struct PhoneCall {
int caller; // 呼叫者
int receiver; // 接收者
int cost; // 呼叫费用
};
int parentNode[MAX_NODE]; // 记录每个节点的前驱节点
int nodeId[MAX_NODE]; // 节点在新图中的编号
int visited[MAX_NODE]; // 访问标记数组
int minIncomingCost[MAX_NODE]; // 每个节点的最小入边费用
/**
* 朱-刘算法求解有向图最小树形图
* @param rootNode 根节点编号
* @param nodeCount 节点总数
* @param callRecords 电话呼叫记录列表
* @return 最小总费用,无解返回-1
*/
int findMinCallCost(int rootNode, int nodeCount, vector<PhoneCall>& callRecords) {
int totalCost = 0;
int callCount = callRecords.size();
while (true) {
// 步骤1:为每个节点寻找最小入边
fill(minIncomingCost, minIncomingCost + nodeCount, INF);
for (int i = 0; i < callCount; i++) {
int caller = callRecords[i].caller;
int receiver = callRecords[i].receiver;
if (caller != receiver && callRecords[i].cost < minIncomingCost[receiver]) {
parentNode[receiver] = caller;
minIncomingCost[receiver] = callRecords[i].cost;
}
}
// 检查非根节点是否都有入边
for (int i = 0; i < nodeCount; i++) {
if (i != rootNode && minIncomingCost[i] == INF) {
return -1; // 存在节点不可达,无解
}
}
// 步骤2:检测环
int newIdCount = 0;
fill(nodeId, nodeId + nodeCount, -1);
fill(visited, visited + nodeCount, -1);
minIncomingCost[rootNode] = 0; // 根节点无入边
for (int i = 0; i < nodeCount; i++) {
totalCost += minIncomingCost[i];
int currentNode = i;
// 沿着前驱链寻找环
while (visited[currentNode] != i && nodeId[currentNode] == -1 && currentNode != rootNode) {
visited[currentNode] = i;
currentNode = parentNode[currentNode];
}
// 找到新环,进行标记
if (currentNode != rootNode && nodeId[currentNode] == -1) {
for (int node = parentNode[currentNode]; node != currentNode; node = parentNode[node]) {
nodeId[node] = newIdCount;
}
nodeId[currentNode] = newIdCount++;
}
}
if (newIdCount == 0) break; // 无环,算法结束
// 为不在环中的节点分配新编号
for (int i = 0; i < nodeCount; i++) {
if (nodeId[i] == -1) {
nodeId[i] = newIdCount++;
}
}
// 步骤3:缩环,更新边权
for (int i = 0; i < callCount; i++) {
int originalCaller = callRecords[i].caller;
int originalReceiver = callRecords[i].receiver;
callRecords[i].caller = nodeId[originalCaller];
callRecords[i].receiver = nodeId[originalReceiver];
if (callRecords[i].caller != callRecords[i].receiver) {
callRecords[i].cost -= minIncomingCost[originalReceiver];
}
}
nodeCount = newIdCount;
rootNode = nodeId[rootNode];
}
return totalCost;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int testCaseCount;
cin >> testCaseCount;
for (int caseIndex = 1; caseIndex <= testCaseCount; caseIndex++) {
int girlCount, callCount;
cin >> girlCount >> callCount;
vector<PhoneCall> callRecords(callCount);
for (int i = 0; i < callCount; i++) {
cin >> callRecords[i].caller >> callRecords[i].receiver >> callRecords[i].cost;
}
int minCost = findMinCallCost(0, girlCount, callRecords);
cout << "Case #" << caseIndex << ": ";
if (minCost == -1) {
cout << "Possums!\n";
} else {
cout << minCost << "\n";
}
}
return 0;
}
代码说明
数据结构
PhoneCall:存储电话呼叫信息minIncomingCost:记录每个节点的最小入边费用parentNode:记录最小入边的来源节点nodeId:节点在缩环后的新编号visited:访问标记,用于环检测
关键函数
findMinCallCost:实现朱-刘算法核心逻辑- 循环执行找最小入边、检测环、缩环的过程
- 返回最小总费用或无解标记
复杂度分析
- 最坏情况下需要 O(n)O(n)O(n) 次迭代
- 每次迭代处理 mmm 条边
- 总复杂度 O(n×m)O(n \times m)O(n×m),在题目限制内可行
总结
本题通过朱-刘算法优雅地解决了有向图最小生成树问题。算法的核心在于不断检测并收缩环,同时更新边权,直到得到无环的树形结构。该算法在有向图连通性检测和最小代价传播路径选择方面具有重要应用价值。


被折叠的 条评论
为什么被折叠?



