数学 Trick 之:wqs 二分

能够解决的问题

  • 规定某些事物只能用 \(k\),求最值权值和。
  • 设只能用 \(x\) 个时的答案为 \(\text{g}(x)\),则 \(\text{g}(x)\) 有凸性。

思路

由于有凸性,则我们可以二分斜率,求出每次的切点,与给定的 \(k\) 比大小,直到切点为它。

对于如何根据斜率 \(mid\) 找切点:

注:

  • 请在草稿纸上画图配合理解。
  • 只解释上凸包,下凸同理。

我们用斜率为 \(mid\) 的直线来切上凸包 \(\text{g}(x)\),则我们可以构造函数 \(\text{f}(x) = \text{g}(x) - kx\),移项:\(\text{g}(x) = \text{f}(x) + kx\),即\(\text{f}(x)\) 为直线与凸包交于横坐标为 \(x\) 的点时的直线的截距,而要使相切,截距就应最大,所以 \(\text{f}(x)\) 最大时,\(x\) 即为切点。

由上一段的结论,我们要使 \(\text{f}(x)\) 最大,也就是让 \(\text{g}(x) - kx\) 最大。又由 \(\text{g}\) 的定义(在文章的第一部分),其为选 \(x\) 个的最值权值和,又因为后面减了个 \(kx\),所以 \(\text{f}(x)\) 可以变为:把所有待选点的权值减 \(k\) 后的最值权值和,设取到最值时选了 \(x\) 个事物,则切点横坐标为 \(x\)

例题与代码

[国家集训队] Tree I

给定无向图,边分黑边和白边,求恰好有 \(need\) 条白边的 MST。

凸性证明

设在图的最小生成树中有 \(k\) 条白边,则此时函数最小。而无论是加白边还是加黑边(减白边)都会使生成树权值增大,所以下凸。

做法

于是我们二分 \(mid\),每次把所有白边减 \(mid\) 然后求 MST,直到白边正好 \(need\) 个。

Code

#include <bits/stdc++.h>
using namespace std;

constexpr int maxn = 50010;

int n, m, need;
struct edge {
	int from, to, v, col;
	bool operator < (edge o) {
		if (v == o.v) return col < o.col;
		return v < o.v;
	}
} E[maxn << 1];
struct bingchaji {
	int fa[maxn], depp[maxn];
	void init() {
		for (int i = 1; i <= n; i++) {
			fa[i] = i;
		}
		for (int i = 1; i <= n; i++) {
			depp[i] = 1;
		}
		return ;
	}
	int find(int x) {
		if (fa[x] != x) fa[x] = find(fa[x]);
		return fa[x];
	}
	void merge(int l, int r) {
		l = find(l), r = find(r);
		if (depp[l] <= depp[r]) {
			fa[l] = r;
			depp[r] += (depp[l] == depp[r]);
		} else {
			fa[r] = l;
		}
		return ;
	}
} bcz;

bool check(int k) {
	for (int i = 1; i <= m; i++) {
		if (E[i].col == 0) E[i].v += k;
	}
	int nowwhite = 0;
	sort(E + 1, E + 1 + m);
	for (int i = 1; i <= m; i++) {
		if (E[i].col == 0) E[i].v -= k;
	}
	bcz.init();
	for (int i = 1; i <= m; i++) {
		if (bcz.find(E[i].from) != bcz.find(E[i].to)) {
			bcz.merge(E[i].from, E[i].to);
//			cout << i << ' ';
			if (!E[i].col) nowwhite++;
		}
	}
//	cout << endl;
	return nowwhite < need;
}

int getans(int k) {
	int ress = 0;
	for (int i = 1; i <= m; i++) {
		if (E[i].col == 0) E[i].v += k;
	}
	sort(E + 1, E + 1 + m);
	bcz.init();
	for (int i = 1; i <= m; i++) {
		if (bcz.find(E[i].from) != bcz.find(E[i].to)) {
			bcz.merge(E[i].from, E[i].to);
			ress += E[i].v;
		}
	}
	for (int i = 1; i <= m; i++) {
		if (E[i].col == 0) E[i].v -= k;
	}
	return ress;
}

signed main() {
	ios::sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	
	cin >> n >> m >> need;
	for (int i = 1; i <= m; i++) {
		cin >> E[i].from >> E[i].to >> E[i].v >> E[i].col;
		E[i].from++;
		E[i].to++;
	}
	int l = -100, r = 100, midd;
	while (l <= r) {
		midd = ((l + r) >> 1);
		if (check(midd)) {
			r = midd - 1;
		} else {
			l = midd + 1;
		}
	}
	cout << getans(r) - r * need << '\n';
	
	return 0;
}

注意事项:让我们用直线靠近凸壳时,可能有很多点间斜率相同,此时我们用直线靠近时有很多答案,所以代码中的 \(\text{operator} <\) 与答案计算(在外面减去 \(r \times midd\))都是为了防止答案错误。

posted @ 2025-07-29 16:26  porse114514  阅读(7)  评论(0)    收藏  举报