P12026 [USACO25OPEN] Compatible Pairs S 解题报告


P12026 [USACO25OPEN] Compatible Pairs S 解题报告

题目核心

我们要解决的问题是,在一大群奶牛中,根据它们的ID号,找出最多能组成多少对可以相互通信的奶牛。

通信规则

  1. 两头牛的ID号相加必须等于 A 或者 B
  2. 可以是不同ID的两头牛,也可以是两头ID相同的牛(只要ID和满足条件)。
  3. 每头牛只能加入一个通信对,不能“一心二用”。

思路分析:从配对到连线

直接计算配对方案会非常复杂,因为一头牛可能有多个潜在的配对选择。例如,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-IDB-ID
  • 因此,图上每个节点的度数(连接的边数)最多为2。

一个所有节点度数都不超过2的图,它的结构非常简单:它必然是由若干条独立的链(像一串珠子)和一些孤立的环组成的。

经过进一步分析(题解中提到的数学推导),可以证明,在这个问题中,除了 A=B 时可能出现长度为2的环,以及自环外,不会形成更复杂的环结构。所以,我们面对的图就是一堆链条和带自环的节点。

图示:整个网络分解为几条独立的链,其中一些节点可能有自环。

第三步:确定最优策略——“从两头向中间”

现在问题变成了:如何在一堆链条上最大化配对数?

我们来看一条链,比如 ID₁ - ID₂ - ID₃ - ID₄

  • ID₁ 的奶牛只能和 ID₂ 的奶牛配对,它们没有别的选择。
  • ID₄ 的奶牛也只能和 ID₃ 的奶牛配对。

这启发我们使用一个贪心策略优先处理链两端的节点

  1. 在链 ID₁ - ID₂ - ... 中,我们先尽最大可能配对 ID₁ID₂。配对数量是它们各自奶牛数量的最小值,即 min(奶牛数(ID₁), 奶牛数(ID₂))
  2. 配对后,其中一个ID的奶牛会被用完。这条链就相当于“断”了,问题规模缩小。比如 ID₁ 的奶牛用完了,链就变成了 ID₂ - ID₃ - ...,只不过 ID₂ 的奶牛数量减少了。
  3. 我们不断重复这个过程,从链的新端点继续向内配对。

这个“从两端向中间”的策略,就像是“剥洋葱”一样,一层一层地解决问题,可以保证得到全局最优解。

第四步:算法实现——拓扑排序

“从两端向中间处理”这个过程,在算法上可以用拓扑排序的思想来实现。拓扑排序的经典应用是处理有依赖关系的任务,而我们这里处理的是链的端点。

  1. 识别端点:链的端点,在图上就是度数为1的节点。一个节点如果只有一个邻居,它就是链的末端。
  2. 处理队列
    • 我们建立一个队列,首先把所有度数为1的节点(所有链的端点)放进去。
    • 然后,不断从队列里取出一个节点 u 进行处理。
  3. 配对过程
    • 取出节点 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;
}

总结

本题是一个巧妙的图论建模问题。解题的关键路径是:

  1. 抽象:将ID配对问题转化为图上节点连接问题。
  2. 洞察:发现图的结构是简单的链条集合,从而简化问题。
  3. 贪心:提出“从两端向中间”的最优配对策略。
  4. 实现:运用拓扑排序的思想,高效地执行贪心策略。

通过这四步,一个看似复杂的组合优化问题,就被我们分解成清晰的步骤,并用标准算法解决了。

posted @ 2025-07-12 14:41  surprise_ying  阅读(36)  评论(0)    收藏  举报