题目描述
K \texttt{K} K 联赛(原韩国职业足球联赛)正在进行中,每支球队都有支持者为其助威。在赛季进行到某个阶段后,支持者们想知道他们支持的球队 S S S 是否还有可能赢得冠军(允许并列冠军)。具体来说,问题是:给定当前每支球队的胜场数 w i w_i wi 和负场数 d i d_i di ,以及每对球队之间剩余的比赛场数 a i , j a_{i,j} ai,j ,能否为剩余的所有比赛分配胜者,使得最终没有球队的总胜场数超过球队 S S S 的总胜场数?
输入格式:
- 第一行:测试用例数 T T T
- 每个测试用例包含三行:
- 球队数 n ( 1 ≤ n ≤ 25 ) n(1 \leq n \leq 25) n(1≤n≤25)
- 2 n 2n 2n 个非负整数 w 1 , d 1 , w 2 , d 2 , … , w n , d n w_1,d_1,w_2,d_2,\ldots,w_n,d_n w1,d1,w2,d2,…,wn,dn ,每个 ≤ 100 \leq 100 ≤100
- n 2 n^2 n2 个非负整数 a 1 , 1 , a 1 , 2 , … , a n , n a_{1,1},a_{1,2},\ldots,a_{n,n} a1,1,a1,2,…,an,n ,每个 ≤ 10 \leq 10 ≤10 ,表示剩余比赛场数,保证 a i , j = a j , i a_{i,j}=a_{j,i} ai,j=aj,i 且 a i , i = 0 a_{i,i}=0 ai,i=0
输出格式:
对于每个测试用例,输出一行,按编号递增顺序输出所有可能夺冠的球队编号。
题目分析
这个问题本质上是一个可行性判断问题:对于每支候选球队 x x x ,判断是否存在一种剩余比赛的胜负分配方案,使得所有球队的最终胜场数都不超过球队 x x x 的最大可能胜场数。
关键观察
-
胜场数上限:对于球队 x x x ,假设它剩余的所有比赛全胜,那么它的最大可能胜场数为:
m a x W i n = w x + ∑ j = 1 n a x , j maxWin = w_x + \sum_{j=1}^{n} a_{x,j} maxWin=wx+j=1∑nax,j -
约束条件:对于其他任何球队 i i i ,其最终胜场数不能超过 m a x W i n maxWin maxWin ,即:
w i + ( 球队 i 在剩余比赛中获得的胜场数 ) ≤ m a x W i n w_i + (\text{球队$i$在剩余比赛中获得的胜场数}) \leq maxWin wi+(球队i在剩余比赛中获得的胜场数)≤maxWin
等价于:
球队 i 在剩余比赛中最多还能赢 m a x W i n − w i 场 \text{球队$i$在剩余比赛中最多还能赢} \quad maxWin - w_i \quad \text{场} 球队i在剩余比赛中最多还能赢maxWin−wi场 -
比赛分配:每场剩余比赛必须在两个参赛队伍之间分配一个胜场。
转化为网络流问题
这是一个典型的多源多汇分配问题,可以转化为最大流问题来判断可行性。
建图方法
对于每个待检查的球队 x x x ,我们构建这样一个流网络:
-
节点类型:
- 源点 s o u r c e source source
- 每场剩余比赛一个节点(如果 a i , j > 0 a_{i,j} > 0 ai,j>0 且 i < j i < j i<j)
- 每支球队一个节点
- 汇点 s i n k sink sink
-
边与容量:
- 从源点到每场比赛节点:容量为该比赛剩余场数 a i , j a_{i,j} ai,j
- 从比赛节点到对应的两支球队节点:容量为无穷大(表示这场比赛的胜场可以分配给任意一支参赛队伍)
- 从球队节点到汇点:容量为 m a x W i n − w i maxWin - w_i maxWin−wi(表示该球队最多还能赢多少场)
-
可行性判断:
计算从源点到汇点的最大流。如果最大流等于所有剩余比赛的总场数,说明存在一种分配方案,使得所有球队的胜场数都不超过 m a x W i n maxWin maxWin ,即球队 x x x 可能夺冠。
为什么这样是正确的?
- 从源点到比赛节点的流量表示这场比赛被"使用"的次数(即需要分配胜负的场次)。
- 从比赛节点到球队节点的边保证了每场比赛的胜者必须是参赛的两支球队之一。
- 从球队节点到汇点的容量限制了每支球队最多能增加的胜场数。
- 如果最大流等于总剩余比赛数,意味着所有比赛都被成功分配,且没有球队超过胜场限制。
算法复杂度
对于每支待检查的球队:
- 节点数:最多 C 25 2 + 25 + 2 ≈ 327 C_{25}^2 + 25 + 2 \approx 327 C252+25+2≈327 个
- 边数:约为比赛节点数的 3 3 3 倍
- 使用 Dinic 算法求最大流: O ( E V ) O(E\sqrt{V}) O(EV) 或 O ( V 2 E ) O(V^2E) O(V2E) 在实际中表现良好
总复杂度: O ( n ⋅ maxflow ( V , E ) ) O(n \cdot \text{maxflow}(V,E)) O(n⋅maxflow(V,E)) ,在 n ≤ 25 n \leq 25 n≤25 的情况下完全可行。
解题思路步骤
- 读取输入:处理 T T T 个测试用例。
- 检查每支球队:对于每支球队
i
i
i ,调用判断函数
canWin(i)。 - 判断函数实现:
- 计算目标球队的最大可能胜场 m a x W i n maxWin maxWin 。
- 快速检查:如果有任何球队当前胜场
w
j
>
m
a
x
W
i
n
w_j > maxWin
wj>maxWin ,直接返回
false。 - 构建流网络:
- 统计所有剩余比赛( a u , v > 0 a_{u,v} > 0 au,v>0 且 u < v u < v u<v)。
- 创建源点、比赛节点、球队节点、汇点。
- 添加相应边。
- 计算最大流,判断是否等于总剩余比赛数。
- 输出结果:按编号递增顺序输出所有可能夺冠的球队。
代码实现
// The K-League
// UVa ID: 1306
// Verdict: Accepted
// Submission Date: 2025-12-15
// UVa Run Time: 0.120s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
struct Edge {
int to, rev; // 目标节点, 反向边在邻接表中的索引
int flow, cap; // 当前流量, 容量
Edge(int t, int r, int f, int c) : to(t), rev(r), flow(f), cap(c) {}
};
const int INF = 1e9; // 无穷大常量
// Dinic 最大流算法实现
struct Dinic {
vector<vector<Edge>> graph; // 邻接表存图
vector<int> level, iter; // 分层图中的层级, 当前弧优化
int n; // 节点数
Dinic(int nodes) {
n = nodes;
graph.resize(nodes);
level.resize(nodes);
iter.resize(nodes);
}
// 添加有向边 from->to, 容量为cap
void addEdge(int from, int to, int cap) {
Edge a(to, graph[to].size(), 0, cap);
Edge b(from, graph[from].size(), 0, 0); // 反向边初始容量为0
graph[from].push_back(a);
graph[to].push_back(b);
}
// BFS构建分层图
bool bfs(int s, int t) {
fill(level.begin(), level.end(), -1);
queue<int> q;
level[s] = 0;
q.push(s);
while (!q.empty()) {
int cur = q.front(); q.pop();
for (Edge& e : graph[cur]) {
if (level[e.to] == -1 && e.flow < e.cap) {
level[e.to] = level[cur] + 1;
q.push(e.to);
}
}
}
return level[t] != -1; // 如果汇点可达, 返回true
}
// DFS寻找增广路
int dfs(int cur, int t, int f) {
if (cur == t) return f;
for (int& i = iter[cur]; i < (int)graph[cur].size(); ++i) {
Edge& e = graph[cur][i];
if (e.flow < e.cap && level[e.to] == level[cur] + 1) {
int pushed = dfs(e.to, t, min(f, e.cap - e.flow));
if (pushed > 0) {
e.flow += pushed;
graph[e.to][e.rev].flow -= pushed; // 更新反向边
return pushed;
}
}
}
return 0;
}
// 计算从s到t的最大流
int maxFlow(int s, int t) {
int flow = 0;
while (bfs(s, t)) {
fill(iter.begin(), iter.end(), 0);
while (int pushed = dfs(s, t, INF)) {
flow += pushed;
}
}
return flow;
}
};
// 判断指定队伍targetTeam是否可能夺冠
bool canWin(int targetTeam, vector<int>& wins, vector<vector<int>>& remain, int n) {
// 计算目标队伍的最大可能胜场数(假设剩余比赛全胜)
int maxWin = wins[targetTeam];
for (int i = 0; i < n; ++i) {
maxWin += remain[targetTeam][i];
}
// 快速检查:如果有队伍当前胜场已超过maxWin, 直接返回false
for (int i = 0; i < n; ++i) {
if (wins[i] > maxWin) return false;
}
// 统计剩余比赛:只考虑i<j的情况, 避免重复
int matchCount = 0;
vector<pair<int, int>> matches; // 存储比赛双方编号
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
if (remain[i][j] > 0) {
matchCount++;
matches.push_back({i, j});
}
}
}
// 节点编号分配:
// 0 = 源点source
// 1 ~ matchCount = 比赛节点
// matchCount+1 ~ matchCount+n = 球队节点
// matchCount+n+1 = 汇点sink
int totalNodes = 2 + matchCount + n;
int source = 0;
int sink = totalNodes - 1;
int matchStart = 1;
int teamStart = matchStart + matchCount;
// 创建Dinic实例
Dinic dinic(totalNodes);
// 计算总剩余比赛场数
int totalRemainGames = 0;
// 添加边:源点 -> 比赛节点
for (int i = 0; i < matchCount; ++i) {
int u = matches[i].first;
int v = matches[i].second;
int games = remain[u][v];
dinic.addEdge(source, matchStart + i, games);
totalRemainGames += games;
// 比赛节点 -> 两支参赛队伍节点(容量为INF,表示可以任意分配)
dinic.addEdge(matchStart + i, teamStart + u, INF);
dinic.addEdge(matchStart + i, teamStart + v, INF);
}
// 添加边:球队节点 -> 汇点
for (int i = 0; i < n; ++i) {
int cap = maxWin - wins[i]; // 该队伍最多还能赢几场
if (cap < 0) return false; // 不应该发生(前面已检查过)
dinic.addEdge(teamStart + i, sink, cap);
}
// 计算最大流
int flow = dinic.maxFlow(source, sink);
// 如果最大流等于总剩余比赛数,说明所有比赛都能被分配
return flow == totalRemainGames;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr); // 加速输入输出
int testCases;
cin >> testCases;
while (testCases--) {
int n;
cin >> n;
vector<int> wins(n);
vector<int> defeats(n); // 负场数,实际上在解题中用不到
// 读取胜场和负场
for (int i = 0; i < n; ++i) {
cin >> wins[i] >> defeats[i];
}
// 读取剩余比赛矩阵
vector<vector<int>> remain(n, vector<int>(n));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
cin >> remain[i][j];
}
}
// 检查每支队伍是否可能夺冠
vector<int> result;
for (int i = 0; i < n; ++i) {
if (canWin(i, wins, remain, n)) {
result.push_back(i + 1); // 队伍编号从1开始
}
}
// 输出结果(按编号递增顺序)
for (size_t i = 0; i < result.size(); ++i) {
if (i > 0) cout << " ";
cout << result[i];
}
cout << endl;
}
return 0;
}
算法正确性证明
该算法的正确性基于以下事实:
-
完备性:如果存在一种分配方案使得球队 x x x 可能夺冠,那么对应的流网络存在大小为总剩余比赛数的流。
- 构造方法:按照分配方案,每场比赛的胜场流向对应的球队节点。
-
可靠性:如果流网络存在大小为总剩余比赛数的流,那么可以构造出一种分配方案。
- 从流网络中提取:对于每条从比赛节点到球队节点的边,其流量表示该球队从这场比赛中获得的胜场数。
-
容量限制:球队节点到汇点的容量 m a x W i n − w i maxWin - w_i maxWin−wi 保证了没有球队的胜场会超过 m a x W i n maxWin maxWin 。
时间复杂度分析
对于每个测试用例:
- 有
n
n
n 支队伍需要检查,每检查一支队伍需要:
- 构建流网络: O ( n 2 ) O(n^2) O(n2)
- 运行 Dinic 算法: O ( V 2 E ) O(V^2E) O(V2E) 或 O ( E V ) O(E\sqrt{V}) O(EV) ,其中 V ≈ n ( n − 1 ) 2 + n + 2 V \approx \frac{n(n-1)}{2} + n + 2 V≈2n(n−1)+n+2 , E ≈ 3 × n ( n − 1 ) 2 E \approx 3 \times \frac{n(n-1)}{2} E≈3×2n(n−1)
最坏情况下 n = 25 n=25 n=25 , V ≈ 327 V \approx 327 V≈327 , E ≈ 900 E \approx 900 E≈900 , Dinic \texttt{Dinic} Dinic 算法可以快速运行。总复杂度在可接受范围内。
总结
本题展示了如何将一个实际体育竞赛问题转化为经典的最大流模型。关键点在于:
- 确定目标球队的最大可能胜场数。
- 将比赛分配问题转化为流网络中的流量分配。
- 使用 Dinic \texttt{Dinic} Dinic 等高效最大流算法判断可行性。
这种将实际问题抽象为图论模型的能力在算法竞赛和实际问题解决中都非常重要。
3141

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



