2025.7.11 DP
wqs 二分前置
ref:https://www.luogu.com.cn/article/hbx1okqa
wqs 二分,也叫凸单调性优化,是一类与函数凹凸性有关的 DP 优化.
假如我们有一个 \(f(x)\),已知其具有凸性(即导函数单调,数学上一般叫凹函数或凸函数),但是它的最值因为某些限制很难求. 此时我们可以引入一个参数更多的函数 \(G(x,k)=f(x)-kx\),如果这个函数在 \(k\) 确定时关于 \(x\) 的极值好算,那么我们就可以间接求出 \(k\) 不同时 \(f(x)\) 的取值. 这时候我们其实并没有考虑 $ x $ 的限制,但是根据凸性,\(x\) 此时也具有单调性,则现在有 $ f(x)=kx+G(x,k) $ 是关于 \(k\) 的单调函数,二分 \(k\) 即可求得 \(f(x)\) 最值.
为什么 \(G(x,k)\) 相比于 \(f(x)\) 会好求?在 OI 中,我们可以理解为加入的 \(k\) 使得我们暂时忽略了某个 \(x\) 的限制,但是 \(x\) 却有关于 \(k\) 单调的性质,而且我们算 \(G(x,k)\) 时能很容易地把此时的 \(x\) 求出来,那么我们就直接把 \(k\) 当未知数进行二分,根据 \(x\) 判断即可. 实际上不必过分关注凸性.
另外 \(G(x,k)\) 的极值点可能不唯一确定,也就是函数图像的极值呈平行于 $ x $ 轴的一条线段,这时候显然应该取两端点中的一个更优. 一般来说求最小值取左端点,求最大值取右端点,但是具体情况还需要具体分析.
综上,当你确定 $ f(x) $ 的凸性,或者直接确定 $ x $ 关于另一个 $ k $ 单调且 \(G(x,k)\) 好求等等性质时可以使用 wqs 二分进行优化.
wqs 二分涉及的思想是主元法,限制很强. 事实上它理应具有很强的可拓展性,即不同的 \(G(x,k)=f(x)+A(x)\) 可能适用于更多的情况.
P2619 [国家集训队] Tree I
恰好 \(need\) 条白边令人摸不着头脑,直接做非常困难. 计 \(x\) 条白边生成树的边权和为 \(f(x)\),也就是我们需要求 \(f(need)\) 的最小值. 考虑主元,暂时忽略 \(x\) 的限制. 由于我们要求的是最小生成树,于是给每条白边的边权加上 \(k\)(可正可负),随着 \(k\) 增大,显然 \(x\) 会减小,反之亦然. 所以我们考虑求构造出的新函数 \(G(x,k)=f(x)+kx\) 的最小值,也就是白边边权加上 \(k\) 后的最小生成树,这是容易的. 而我们根据此时 \(x\) 的值,二分调整 \(k\) 的大小即可确定所求 \(x=need\) 时的 \(f(x)\) 最小值.
一个细节是边权相等时应该优先选白边,因为 kruskal 边全从小到大排序,先选了黑边可能导致误判 \(cnt\) 和 \(k\) 的关系,找不到正解.
P4383 [八省联考 2018] 林克卡特树
Hint:把题意转换为选 \(k+1\) 条互不相交的链,观察凸性.
切掉 \(k\) 条边视为把链断开,必然存在使这些链首尾相连的 \(k\) 条边的连边方案,即转换为选择 \(k+1\) 条不交的链.
对于这个问题,先考虑一个朴素的树上背包. 根据树上背包的常见思路,容易设出 \(f_{i,j}\) 表示以 \(i\) 为子树选出 \(j\) 条不交链的最大权值,但是这样无法转移,\(i\) 各个儿子的子树各种链的形态和节点 \(i\) 的关系无法通过分讨来确定. 考虑新增一维表示点 \(i\) 的度数,由于各个链不交所以度数仅为 \(0/1/2\). 现在考虑转移,就是正常树上背包的转移:
复杂度 \(O(nk^2)\),可以获得 35pts,卡常似乎可以获得 60pts. 考虑优化,这真的能优化吗?按照普通树上背包的思路,这已经是极限了.
但是如果你不把它当成一个背包问题呢?
一个重要的猜想:考虑少选一些链,也就是合并一些链,这意味着多选一些边,那么答案应该会变大. 根据上面的暴力尝试打表 \(k\in[1,100]\) 的最大答案,发现这是正确的,也就是最大答案关于 \(k\) 具有凸性.
于是考虑 wqs 二分. 不考虑链的限制,直接统计最大答案,顺便统计链的个数. 这时候 DP 的复杂度已经退化为 \(O(n)\). 考虑限制链的个数,二分一个 \(mid\),令每有一条链权值就减去 \(mid\),只需要在 DP 中略作调整就可以做到. 这时根据凸性,链的个数 \(c\) 会减少,但是 \(ans+mid\times c\) 始终是 \(c\) 条链的最大答案. 于是二分逐步使链的个数 \(c\rightarrow k\),就能得到 \(k\) 条链的最大答案.
为了处理极值点不唯一的情况,需要钦定选链的条数尽可能多,否则会丢失正解.
代码细节很多,需要精细实现.
代码实现
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 3e5 + 10;
int n, k;
ll ans, sum, cnt, l, r, mid;
int tot, head[maxn];
struct Edge{int nxt, v; ll w;} e[maxn << 1];
inline void add(int u, int v, ll w) {e[++tot].nxt = head[u], head[u] = tot; e[tot].v = v, e[tot].w = w; return;}
struct Data{
ll v; int c;
Data operator + (Data y) {Data x; x.v = v + y.v, x.c = c + y.c; return x;}
bool operator < (Data y) {return v == y.v ? c < y.c : v < y.v;}
}f[maxn][3];
Data make_Data(ll v, int c) {Data x; x.v = v, x.c = c; return x;}
Data max_Data(Data x, Data y) {return (x < y) ? y : x;}
void dfs(int u, int fa) {
for(int i = head[u], v; i; i = e[i].nxt) {
v = e[i].v; ll w = e[i].w; if(v == fa) continue; dfs(v, u);
f[u][2] = max_Data(f[u][2] + f[v][0], f[u][1] + f[v][1] + make_Data(w - mid, 1));
f[u][1] = max_Data(f[u][1] + f[v][0], f[u][0] + f[v][1] + make_Data(w, 0));
f[u][0] = f[u][0] + f[v][0];
} f[u][0] = max_Data(f[u][0], max_Data(f[u][1] + make_Data(-mid, 1), f[u][2]));
}
bool check() {
for(int i = 1; i <= n; i++) f[i][0] = make_Data(0, 0), f[i][1] = make_Data(0, 0), f[i][2] = make_Data(-mid, 1);
dfs(1, 0), sum = f[1][0].v, cnt = f[1][0].c;
return cnt >= k;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> k; k++;
for(int i = 1, u, v, w; i < n; i++) cin >> u >> v >> w, add(u, v, w), add(v, u, w), r += ((w > 0) ? w : -w);
l = -r;
while(l <= r) {
mid = l + r >> 1;
if(check()) {l = mid + 1, ans = sum + k * mid;}
else r = mid - 1;
} cout << ans;
return 0;
}

浙公网安备 33010602011771号