数学 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\)。
例题与代码
给定无向图,边分黑边和白边,求恰好有 \(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\))都是为了防止答案错误。