P12026 [USACO25OPEN] Compatible Pairs S 解题报告
P12026 [USACO25OPEN] Compatible Pairs S 解题报告
题目核心
我们要解决的问题是,在一大群奶牛中,根据它们的ID号,找出最多能组成多少对可以相互通信的奶牛。
通信规则:
- 两头牛的ID号相加必须等于
A
或者B
。 - 可以是不同ID的两头牛,也可以是两头ID相同的牛(只要ID和满足条件)。
- 每头牛只能加入一个通信对,不能“一心二用”。
思路分析:从配对到连线
直接计算配对方案会非常复杂,因为一头牛可能有多个潜在的配对选择。例如,ID为 d
的奶牛,可能既能和ID为 A-d
的奶牛配对,也能和ID为 B-d
的奶牛配对。我们需要一个全局最优的策略。
这种“关系”和“选择”问题,通常可以转换成图论模型来简化思考。
第一步:建立图模型
我们可以把每一种唯一的ID看作图上的一个节点(Node)。而每种ID有多少头奶牛,就是这个节点的“权重”或者说“储备量”。
如果两种ID的奶牛可以配对,我们就在代表这两种ID的节点之间连一条边(Edge)。
根据规则,边的连接方式如下:
- 对于一个ID为
d
的节点,如果存在一个ID为A-d
的节点,就在它们之间连一条边。 - 同样,如果存在一个ID为
B-d
的节点,也在它们之间连一条边。
特殊情况:自环
如果 2 * d = A
或者 2 * d = B
,意味着ID为 d
的奶牛可以和另一头ID也为 d
的奶牛配对。这在图上表现为一个节点自己连向自己的边,我们称之为“自环”。
这样,我们的问题就从“如何配对奶牛”变成了:
在一个带权重的图上,每条边代表一种配对方式。我们要从节点的“储备量”中消耗奶牛来“激活”这些边(形成配对),目标是最大化被“激活”的边的总数。
第二步:发现图的规律
我们构建出的这个图有什么特点呢?
- 每个节点(ID)最多只有两个潜在的配对伙伴:
A-ID
和B-ID
。 - 因此,图上每个节点的度数(连接的边数)最多为2。
一个所有节点度数都不超过2的图,它的结构非常简单:它必然是由若干条独立的链(像一串珠子)和一些孤立的环组成的。
经过进一步分析(题解中提到的数学推导),可以证明,在这个问题中,除了 A=B
时可能出现长度为2的环,以及自环外,不会形成更复杂的环结构。所以,我们面对的图就是一堆链条和带自环的节点。
图示:整个网络分解为几条独立的链,其中一些节点可能有自环。
第三步:确定最优策略——“从两头向中间”
现在问题变成了:如何在一堆链条上最大化配对数?
我们来看一条链,比如 ID₁ - ID₂ - ID₃ - ID₄
。
ID₁
的奶牛只能和ID₂
的奶牛配对,它们没有别的选择。ID₄
的奶牛也只能和ID₃
的奶牛配对。
这启发我们使用一个贪心策略:优先处理链两端的节点。
- 在链
ID₁ - ID₂ - ...
中,我们先尽最大可能配对ID₁
和ID₂
。配对数量是它们各自奶牛数量的最小值,即min(奶牛数(ID₁), 奶牛数(ID₂))
。 - 配对后,其中一个ID的奶牛会被用完。这条链就相当于“断”了,问题规模缩小。比如
ID₁
的奶牛用完了,链就变成了ID₂ - ID₃ - ...
,只不过ID₂
的奶牛数量减少了。 - 我们不断重复这个过程,从链的新端点继续向内配对。
这个“从两端向中间”的策略,就像是“剥洋葱”一样,一层一层地解决问题,可以保证得到全局最优解。
第四步:算法实现——拓扑排序
“从两端向中间处理”这个过程,在算法上可以用拓扑排序的思想来实现。拓扑排序的经典应用是处理有依赖关系的任务,而我们这里处理的是链的端点。
- 识别端点:链的端点,在图上就是度数为1的节点。一个节点如果只有一个邻居,它就是链的末端。
- 处理队列:
- 我们建立一个队列,首先把所有度数为1的节点(所有链的端点)放进去。
- 然后,不断从队列里取出一个节点
u
进行处理。
- 配对过程:
- 取出节点
u
,找到它的邻居v
。 - 情况A(自环):如果
v
就是u
自己,说明这个ID的奶牛可以内部配对。我们能组成奶牛数(u) / 2
对。 - 情况B(链条):如果
v
是另一个节点,我们就在它们之间配对。配对数是min(奶牛数(u), 奶牛数(v))
。然后更新它们的奶牛数量。 - 配对后,
u
的任务完成了。它的邻居v
因为失去了一个邻居u
,也可能变成了新的“端点”。所以,我们将v
加入队列,以便后续处理。
- 取出节点
通过这个过程,我们就能模拟“从两端向中间”的贪心策略,把所有链条上的配对都计算出来。最后剩下的,就是那些在链条中间,但配对完后还有剩余的奶牛,如果它们有自环,也可以进行内部配对(我们的算法在处理过程中已经包含了这种情况)。
代码解读
#include <bits/stdc++.h>
#define int long long // 使用 long long 防止数据溢出
using namespace std;
// ... 省略常量和结构体定义 ...
unordered_map<int, int> col; // 哈希表,用于通过ID值快速找到节点编号
void add(int u, int v) { // 加边函数
// ...
}
signed main() {
int n, A, B; cin >> n >> A >> B;
// 1. 读入数据并建立ID到节点编号的映射
for (int i = 1; i <= n; i ++) {
cin >> w[i] >> id[i]; // w[i]是奶牛数,id[i]是ID值
col[id[i]] = i; // 记录ID为id[i]的是第i个节点
}
// 2. 建图
for (int i = 1; i <= n; i ++) {
// 寻找A-partner
if (id[i] <= A && col.count(A - id[i])) { // col.count检查是否存在这个ID
add(i, col[A - id[i]]);
}
if (A == B) continue; // 如果A=B,B-partner和A-partner是同一种,跳过避免重复建边
// 寻找B-partner
if (id[i] <= B && col.count(B - id[i])) {
add(i, col[B - id[i]]);
}
}
// 3. 拓扑排序思想求解
queue<int> q;
// 将所有度数为1的节点(链的端点)加入初始队列
for (int i = 1; i <= n; i ++) {
if (d[i] == 1) q.push(i);
}
int ans = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
// 遍历u的所有邻居(其实对于从队列里出来的u,它只有一个“待处理”的邻居)
for (int i = hd[u]; i; i = e[i].nx) {
int v = e[i].to;
if (v == u) { // 情况A:自环
ans += w[u] / 2;
w[u] %= 2; // 剩下0或1头牛
} else if (w[v] > 0) { // 情况B:链条,且邻居v还有牛
int res = min(w[v], w[u]); // 计算能配对的数量
ans += res;
w[v] -= res; // 更新双方的奶牛数
w[u] -= res;
q.push(v); // u处理完了,v成为新的“端点”,加入队列
}
}
}
cout << ans;
return 0;
}
总结
本题是一个巧妙的图论建模问题。解题的关键路径是:
- 抽象:将ID配对问题转化为图上节点连接问题。
- 洞察:发现图的结构是简单的链条集合,从而简化问题。
- 贪心:提出“从两端向中间”的最优配对策略。
- 实现:运用拓扑排序的思想,高效地执行贪心策略。
通过这四步,一个看似复杂的组合优化问题,就被我们分解成清晰的步骤,并用标准算法解决了。