wqs二分 & 斜率优化dp & 凸包
📌 WQS二分
洛谷 P2619
题意:
给定点、边权和边的颜色,求一棵最小权的生成树,要求恰好有 need 条白色边。
💡 思路分析
看到“恰好 k 条边”的限制,考虑 WQS 二分。使用 WQS 二分的前提是,函数 f(x) 表示用了 x 条白色边形成的 MST,其图像是凸的。
感性理解(不会严谨证明):
- 没有任何限制时,MST 的白色边数为
x。 - 我们想要加上一条白色边就必须在加上这条边后形成的环上去掉一条最大的黑色边,可以感觉到限制是越来越强的,你一条权值小的白色边换权值大的黑色边在后面操作如果增量小于前面的显然可以在前面先操作,所以增量只会越来越大。
- 所以
x, f(x)能构成下凸的凸包,可以用 WQS 二分。
🔍 WQS 二分过程
本质上是二分斜率 k,然后:
- 每条白边的权值变成
原权值 - k。 - 在这个新图上直接跑 MST。
- 这样得到的 MST 是在当前斜率下能形成的最优解。
为什么可以这样?
我们考虑函数:
f(x) = k * x + b
=> b = f(x) - k * x
所以我们可以认为 截距的意义就是f(x) - kx 就是一个新的权值下的 MST,MST 结果反映了这个斜率下白边数量。因为函数是凸的,斜率是递增的,所以可以二分找到刚好 need 条白边对应的斜率,求出 f(need)。
✅ 代码实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
struct node {
int s, t, c, col;
bool operator < (const node &b) const {
if (c == b.c) return col < b.col;
return c < b.c;
}
};
void solve() {
int V, E, need;
cin >> V >> E >> need;
vector<node> a(E + 1);
vector<int> f(V + 1);
for (int i = 1; i <= E; i++) {
cin >> a[i].s >> a[i].t >> a[i].c >> a[i].col;
}
for (int i = 0; i <= V; i++) f[i] = i;
auto find = [&](auto self, int x) -> int {
return f[x] == x ? x : f[x] = self(self, f[x]);
};
auto check = [&](int x) -> pair<int, int> {
vector<node> b(E + 1);
for (int i = 1; i <= E; i++) {
b[i] = a[i];
if (b[i].col == 0) b[i].c -= x;
}
for (int i = 0; i <= V; i++) f[i] = i;
sort(b.begin() + 1, b.end());
int ans = 0, cnt = 0;
for (int i = 1; i <= E; i++) {
int u = b[i].s, v = b[i].t;
int fu = find(find, u), fv = find(find, v);
if (fu == fv) continue;
f[fu] = fv;
cnt += (b[i].col == 0);
ans += b[i].c;
}
if (cnt >= need) return {cnt, ans};
else return {-1, ans};
};
int l = -100, r = 100, res = 1e18, p;
while (l <= r) {
int mid = (l + r) >> 1;
auto it = check(mid);
if (it.first != -1) {
p = mid;
res = it.second;
r = mid - 1;
} else {
l = mid + 1;
}
}
res += need * p;
cout << res << '\n';
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T = 1;
while (T--) solve();
return 0;
}
📌 斜率优化 DP
以 P2365、P5785 为例:
P2365 题意简述
定义 dp[i][j] 表示前 i 个数分成 j 组的最小代价。
转移方程为:
dp[i][j] = min(dp[i1][j - 1] + (sumt[i] + s * j) * (sumf[i] - sumf[i1]))
暴力时间复杂度为 $O(n^3)$。
💡 思路优化
我们可以发现每分一组,这个s一定会对后面产生s*(sumf[n]-sumf[i1])的代价并且后续不可能再影响这个操作,我们就可以考虑分组的费用提前计算省去第二维到底分了几个组,即把分组当成每次从j转移过来的一个额外费用。
这是一个费用提前计算的dp优化。于是将二维优化成一维,转化为:
dp[i] = min(dp[i1] + (sumf[n] - sumf[i1]) * s + sumt[i] * (sumf[i] - sumf[i1]))
时间复杂度降低到 $O(n^2)$,可以通过 P2365。
⏩ 再进一步斜率优化
将式子整理成:
dp[j] = (s + sumt[i]) * sumf[j] + dp[i] - sumf[n] * s - sumt[i] * sumf[i]
这是一个关于 sumf[j] 的一次函数形式。每次寻找在斜率 k = s + sumt[i] 下交点截距最小的点。想要找到截距最小其实就是找到第一个于斜率为k相切的图像上的交点,满足(a,b,c)ab斜率小于k,bc斜率大于k。下凸包可以解决这个问题。
我们就可以用下凸包来优化:
- 维护斜率递增的下凸包。
- 每次使用二分第一个斜率大于等于k的边。
- 凸包维护使用单调队列。
⚠️ 注意:此方法只适用于 x 单调递增的情况(在此题中满足)。x不单调需要李超线段树维护。
✅ 代码实现
#include<bits/stdc++.h>
#define int long long
using namespace std;
void solve() {
int n, s;
cin >> n >> s;
vector<int> t(n + 1), f(n + 1);
for (int i = 1; i <= n; i++) {
cin >> t[i] >> f[i];
}
vector<int> dp(n + 1, 1e18);
vector<int> sumf(n + 1), sumt(n + 1);
for (int i = 1; i <= n; i++) sumf[i] = sumf[i - 1] + f[i];
for (int i = 1; i <= n; i++) sumt[i] = sumt[i - 1] + t[i];
dp[0] = 0;
vector<int> arr;
arr.push_back(0);
auto check = [&](int x, int i) -> bool {
int y = arr[x + 1];
x = arr[x];
return (sumf[y] - sumf[x]) * (s + sumt[i]) <= (dp[y] - dp[x]);
};
for (int i = 1; i <= n; i++) {
int l = 0, r = arr.size() - 2, ans = r + 1;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid, i)) {
ans = mid;
r = mid - 1;
} else l = mid + 1;
}
ans = arr[ans];
dp[i] = dp[ans] + (sumf[n] - sumf[ans]) * s + sumt[i] * (sumf[i] - sumf[ans]);
int x = arr.size() - 1, y = arr.size() - 2;
x = arr[x], y = arr[y];
while (arr.size() > 1 && (__int128_t)(dp[i] - dp[x]) * (sumf[x] - sumf[y]) <= (__int128_t)(dp[x] - dp[y]) * (sumf[i] - sumf[x])) {
arr.pop_back();
x = arr.size() - 1, y = arr.size() - 2;
x = arr[x], y = arr[y];
}
arr.push_back(i);
}
cout << dp[n];
}

浙公网安备 33010602011771号