【学习笔记】wqs 二分
happy_zero · · 算法·理论
感觉是一个很神秘的东西。
例题
P2619 [国家集训队] Tree I
- 从
m 条边中选n-1 条 - 要求选恰好
k 条白边,且边集是原图生成树 - 求边权和的最小值
这题不算是 dp,但还是记
我是这么理解的:设原图最小生成树白边数量为
将
对于下凸壳的题,不难想到用直线去扫。wqs 二分就是枚举直线的斜率
先给结论:对应的
其实证明比较简单:设这条直线切凸壳与点
和之前不一样的是,这次多了一个
tips:这里不要像我一样搞错逻辑,是
k 推出x 和g_x ,而不是用已知x 求g_x !
求出
对于二分的上下界,这里的
但接下来才是 wqs 二分的精髓!
Q:首先,万一遇到下面这种情况,相邻直线斜率相同,切不到某一个点怎么办(又或者同时切到线段两端的点)?
A:这涉及到一个很重要的细节:对于权值相同的边,优先选白边。由于一定有解,取不到的点是因为
于是做出如上规定,尽量选白边,但相应地,同时也是 wqs 必须处理的一步:求出来的
其实还有一种限制:尽量选黑边,在
注意这是保证有解的情况,无解的情况后面有,处理一样,但判断会比较复杂。
Q:那最后减去的是
A:无论
Q:斜率
由于横纵坐标及斜率都是整数,根据上面,如果切不到,也一定能找到其和相邻节点的斜率,也可以找到答案。
其实程序中还有个小优化:可以把白边黑边存起来分别排序,由于白边减去 我懒地写了。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
const int M = 105;
struct node { int u, v, w, co; } e[N];
bool cmp(node x, node y) {//权值相同优先选白边
if (x.w != y.w) return x.w < y.w;
else return x.co < y.co;
}
int n, m, k, sum, fa[N];
int find(int x) {
if (fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
bool chk(int x) { // kruskal 最小生成树
for (int i = 1; i <= m; i++)
if (!e[i].co) e[i].w -= x;
sort(e + 1, e + 1 + m, cmp);
for (int i = 1; i <= n; i++) fa[i] = i;
int s = 0, cnt = 0; sum = 0;
for (int i = 1; i <= m; i++) {
int tx = find(e[i].u), ty = find(e[i].v);
if (tx == ty) continue; fa[tx] = ty;
s += !e[i].co, cnt++, sum += e[i].w;
if (cnt == n - 1) break;
} for (int i = 1; i <= m; i++)
if (!e[i].co) e[i].w += x;
return s >= k;
}
signed main() {
cin >> n >> m >> k;
for (int i = 1; i <= m; e[i].u++, e[i].v++, i++)//注意题目中节点从 0 开始
cin >> e[i].u >> e[i].v >> e[i].w >> e[i].co;
int l = -M, r = M, ans = 0;
while (l <= r) {
int mid = l + r >> 1;
if (chk(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
} chk(ans); cout << ans * k + sum << '\n';
//注意最后还得再调用一下求出 sum(否则可能被不不合法的 mid 覆盖)
return 0;
}
P5633 最小度限制生成树
这题主要部分跟上一题差不多,主要在于对无解的处理。有两种方法:
第一种是基于这一题来看的。首先要判断
写的有点丑陋:
int co[N], ts;
bool vis[N], viss[N];
//vis 维护联通块,viss 维护点
vector <int> p[N];
void dfs(int k) {
vis[k] = true, co[k] = ts;
for (auto i : p[k])
if (!vis[i]) dfs(i);
}
bool chkans() {
for (int i = 1; i <= m; i++)
if (!e[i].f) {
p[e[i].u].push_back(e[i].v);
p[e[i].v].push_back(e[i].u);
}
for (int i = 1; i <= n; i++)
if (i != id && !vis[i]) ts++, dfs(i);
memset(vis, 0, sizeof(vis)); int cnt1 = 0, cnt2 = 0;
for (int i = 1; i <= m; i++)
if (e[i].f) {
if (e[i].u != id) swap(e[i].u, e[i].v);
if (!vis[co[e[i].v]]) vis[co[e[i].v]] = true, cnt1++;
if (!viss[e[i].v]) viss[e[i].v] = true, cnt2++;
} return cnt1 <= k && cnt1 == ts && cnt2 >= k;
}
第二种是利用 wqs 二分的性质。注意到合法的
这题会卡一点 krusual 快排,需要写上面提到的归并(不能偷懒了,悲)。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 5e5 + 5;
const int M = 3e4 + 5;
inline int read() {
int w = 1, q = 0; char ch = ' ';
while (ch != '-' && (ch < '0' || ch > '9')) ch = getchar();
if (ch == '-') w = -1, ch = getchar();
while (ch >= '0' && ch <= '9') q = q * 10 + ch - '0', ch = getchar();
return w * q;
}
struct node { int u, v, w; bool f;} e1[N], e2[N];
int n, m, m1, m2, id, k, sum, s, fa[N];
inline bool cmp(node x, node y) {
if (x.w != y.w) return x.w < y.w;
else return x.f > y.f;
}
inline int find(int x) {
if (fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
inline bool chk(int x) {
for (int i = 1; i <= m1; i++) e1[i].w -= x;
for (int i = 1; i <= n; i++) fa[i] = i;
int cnt = 0, n1 = 1, n2 = 1; s = 0, sum = 0;
for (int i = 1; i <= m; i++) {
int tx, ty; node t; bool f = 0;
if (n2 > m2 || n1 <= m1 && cmp(e1[n1], e2[n2]))
t = {e1[n1].u, e1[n1].v, e1[n1].w}, f = 1, n1++;
else t = {e2[n2].u, e2[n2].v, e2[n2].w}, n2++;
tx = find(t.u), ty = find(t.v);
if (tx == ty) continue; fa[tx] = ty;
s += f, cnt++, sum += t.w;
if (cnt == n - 1) break;
} for (int i = 1; i <= m1; i++) e1[i].w += x;
return s >= k;
}
inline bool chkans() {
int l, r; chk(-M), l = s; chk(M), r = s;
return l <= k && k <= r;
}
signed main() {
n = read(), m = read(), id = read(), k = read();
for (int i = 1; i <= m; i++) {
int u, v, w; cin >> u >> v >> w;
if (u == id || v == id) e1[++m1] = {u, v, w, 1};
else e2[++m2] = {u, v, w, 0};
} sort(e1 + 1, e1 + 1 + m1, cmp);
sort(e2 + 1, e2 + 1 + m2, cmp);
if (!chkans()) return cout << "Impossible", 0;
int l = -M, r = M, ans = -M; bool f = 0;
while (l <= r) {
int mid = l + r >> 1;
if (chk(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
} chk(ans); cout << ans * k + sum; return 0;
}
P4983 忘情
wqs 二分优化 dp。
式子太丑了,化简一下
先证明单调递减,用划分成一段的代价和两段进行相减(为了方便,下文是将区间
因为
同时感性理解(没错,又是它)一下式子的最大值在划分段越少的情况下越小,所以其是满足凸性的。
于是又可以用之前的套路,将
这题的边界处理也要注意,由于我们二分出
不讲斜率优化,但这题我写挂了好久。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e5 + 5;
const int INF = 1e16;
int n, m, s[N], f[N], cnt[N], X[N], Y[N], q[N];
double cal(int x, int y) { return (Y[y] - Y[x]) * 1.0 / (X[y] - X[x]); }
bool chk(int x) {
int l = 1, r = 1;
for (int i = 1; i <= n; i++) {
int k = 2 * (s[i] + 1);
while (r > l && (cal(q[l], q[l + 1]) < k || \
cal(q[l], q[l + 1]) == k && cnt[q[l + 1]] > cnt[q[l]])) l++;
//相等优先选择次数多的
f[i] = (s[i] - s[q[l]] + 1) * (s[i] - s[q[l]] + 1) + f[q[l]] - x;
cnt[i] = cnt[q[l]] + 1; X[i] = s[i], Y[i] = s[i] * s[i] + f[i];
while (r > l && (cal(q[r - 1], q[r]) > cal(q[r], i) || \
cal(q[r - 1], q[r]) == cal(q[r], i) && cnt[q[r]] < cnt[i])) r--; q[++r] = i;
} return cnt[n] >= m;
}
signed main() {
cin >> n >> m;
for (int i = 1, x; i <= n; i++)
cin >> x, s[i] = s[i - 1] + x;
int l = -INF, r = 0, ans = 0;
while (l <= r) {
int mid = l + r >> 1;
if (chk(mid)) ans = mid, r = mid - 1;
else l = mid + 1;
} chk(ans); cout << f[n] + ans * m << '\n';
return 0;
}
习题
CF321E Ciel and Gondolas 基本板子,也可以用四边形不等式。
CF739E Gosha is hunting wqs 二分优化 dp,要用到小数二分。
P5308 [COCI2018-2019#4] Akvizna wqs 二分+斜率优化
巨佬的 blog
【学习笔记】WQS二分详解及常见理解误区解释