AT-jsc2019-qual-e Card Collector 题解

\(\text{AT-jsc2019-qual-e Card Collector 题解}\)

题目本身难度没有那么大,但很具有启发性。

这种无从着手的题先考虑图论建模。如果你的网络流掌握地不错,那么应该能一眼看出来一个费用流模型。对于每一行和每一列看成一个点,从源点 \(S\) 向行和列分别连边,再分别向每个物品连边,从每个物品向汇点 \(T\) 连边,跑费用流。

傻子也知道这样肯定过不去,考虑优化。能否模拟费用流?观察这张图的性质。按照一般的方法我们按照物品的价值从大到小往右部点里加,判断能否和当前的左部点进行匹配。这样做的正确性是考虑调整法,如果有较大的未被匹配而在当前(只考虑不比它小的情况下)能被匹配,那么如果不选它而选更小的节点一定能通过不断的调整使得选到它。现在的问题是考虑如何判断在当前的情形下只加入当前点是否合法。判断有没有完美匹配使用 Hall 定理。那么原先的图的子集显然都是合法的。考虑加入一个新的右部点。判断加入后的所有子集还是有点吃操作了,因此发掘一下这个图有什么性质。比较好的性质是一个右部点会且仅会向两个左部点连边,换句话说对于现有的一个集合 \(S\),若新加入的一个右部点 \(y\)\(S\) 有交集,那么至多会有一条边连向 \(S\) 的外部,这样带来的贡献是左部点和右部点各一个,与原问题是等价的,不会放松限制。那么我们可以在判断一条边的时候只判断当前 \(x\) 所在集合与 \(y\) 所在集合连起来后左部点与右部点的个数关系,而不考虑连起来 \(x\)\(y\) 的这个新集合 \(S\) 的其它子集,这个原因是把刚才的性质反过来理解,因为删掉一个右部点最多会删掉一个左部点,这样一个一个删掉点后限制不会比原来宽松,因此这样做是正确的。

这个做法能否扩展到更高维度,比如三维、四维的情形呢?很遗憾是不行的,原因是在删除一个右部点的时候除了和 \(S\) 集合保持连通的那条边,还可能有两个对应的左部点被删除,这样一来限制更为宽松,不能保证子集的合法性了。

具体实现的时候可以直接记录每个左部点集合此时已经匹配了多少右部点,用并查集合并即可。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6 + 5;
int pn, n, m;
int fa[N], fk[N], ans;
int fnd(int x) {
	return x == fa[x] ? x : fa[x] = fnd(fa[x]);
}
struct Node {
	int x, y, w;
	bool operator < (const Node &a) const {
		return w > a.w;
	}
};
vector<Node>v;

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> pn >> n >> m;
	for (int i = 1; i <= pn; i++) {
		int x, y, w;
		cin >> x >> y >> w;
		v.push_back({x, y + n, w});
	}
	sort(v.begin(), v.end());
	iota(fa + 1, fa + N, 1);
	fill(fk + 1, fk + N, 1);
	for (auto i : v) {
		int x = fnd(i.x), y = fnd(i.y), w = i.w;
		if (x == y) {
			if (fk[x]) --fk[x], ans += w;
		}
		else if (fk[x] + fk[y]) {
			fa[y] = x;
			fk[x] += fk[y] - 1;
			ans += w;
		}
	}
	cout << ans << '\n';
	return 0;
}

如果你觉得模拟费用流以及 Hall 定理还是太吃操作了,那么有没有更简单的方法呢?有的兄弟有的。考虑刚才做法的核心性质是每个物品只和两个坐标连边,那么考虑图论建模的另外一种思想是把一些东西看成点,一次操作看成边,那么在这里显然是把每个坐标看成点,一次选物品看成一条边,这样一来我们把选物品问题转化为了给边定向的问题,限制是每个点最多只能有一条出边,那么取到最大时就是一棵基环树。这样一来只需要拿并查集维护集合内环的个数判断选择的合法性了。

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2e6 + 5;
int pn, n, m;
int fa[N], fk[N], ans;
int fnd(int x) {
	return x == fa[x] ? x : fa[x] = fnd(fa[x]);
}
struct Node {
	int x, y, w;
	bool operator < (const Node &a) const {
		return w > a.w;
	}
};
vector<Node>v;

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> pn >> n >> m;
	for (int i = 1; i <= pn; i++) {
		int x, y, w;
		cin >> x >> y >> w;
		v.push_back({x, y + n, w});
	}
	sort(v.begin(), v.end());
	iota(fa + 1, fa + N, 1);
	for (auto i : v) {
		int x = fnd(i.x), y = fnd(i.y), w = i.w;
		if (x == y) {
			if (!fk[x]) fk[x] = 1, ans += w;
		}
		else if (!fk[x] || !fk[y]) {
			fa[y] = x;
			fk[x] |= fk[y];
			ans += w;
		}
	}
	cout << ans << '\n';
	return 0;
}

因此实际上两种做法本质上是一致的,要求本质上都是 Hall 定理,即在每个时刻每个子集内坐标的个数不小于物品的个数,只不过方法二抽象成了图论模型因此显得更为直观,而仔细想想发现法一的性质也和基环树是一致的:删掉一条边至多会减少一个节点或不减少节点,只是思考的方向不同而已,并非本质不同。

posted @ 2025-11-20 09:24  长安19路  阅读(12)  评论(0)    收藏  举报