概述
WQS二分(Wasserstrom-Spjudovich Binary Search),又称 Aliens Trick,是解决如下问题的高效技巧:
在DP中,要求恰好选择 个物品,使得总价值最大。如果对”选择物品数”没有限制时可以直接贪心,但加上恰好 个的限制时如何处理?
核心思想:将”恰好选 个”的约束转化为”每选一个物品惩罚 “,通过二分 找到最优解的 值。
算法思想
设原问题的解为 ,即选恰好 个物品的最大价值。
定义 (无限制选物品数,但每选一个扣减 )。
关键性质
- 是关于 的凸函数(或凹函数,取决于问题)
- 是 的对偶函数
- 通过二分 ,可以找到对应恰好 个物品的解
典型形式
当 增加时,选择的物品数减少。通过二分找到使得选择数为 的 。
代码框架
// 假设 dp(state) 返回 (maxValue, count) 的最大值贪心解
pair<long long, int> solveWithPenalty(double lambda) {
// 返回:最大价值(减去惩罚后),选择的物品数
long long val = 0;
int cnt = 0;
// 根据问题,贪心地选择或DP
for (each option) {
if (benefit - lambda * cost > 0) {
val += benefit - lambda * cost;
cnt += cost;
}
}
return {val, cnt};
}
double wqsBinarySearch(int targetK) {
double lo = MIN_LAMBDA, hi = MAX_LAMBDA;
for (int iter = 0; iter < 60; iter++) { // 二分迭代确保精度
double mid = (lo + hi) / 2;
auto [val, cnt] = solveWithPenalty(mid);
if (cnt > targetK) {
lo = mid; // 选多了,需要增大惩罚
} else {
hi = mid; // 选少了,需要减小惩罚
}
}
return (lo + hi) / 2;
}经典应用
1. 序列分组(Partition Array)
问题:将数组分成 段,每段和的最大值最小。
转化:惩罚每增加一段。
// 最大化 sum - λ * segments
// 二分 λ,找到恰好 k 段的解
int n, k;
vector<long long> a(n+1);
// check: 在惩罚 λ 下,最多能分多少段
int check(double λ) {
int segments = 0;
long long sum = 0;
for (int i = 1; i <= n; i++) {
if (sum + a[i] > λ) {
segments++;
sum = a[i];
} else {
sum += a[i];
}
}
return segments;
}2. 编辑距离变种
问题:给定字符串,每次操作可以删除一个字符,要求恰好删除 个字符使得剩余字符串字典序最小。
转化:每删除一个字符惩罚 ,二分找到最优。
3. 背包DP优化
问题:选恰好 件物品的背包 DP,要求价值最大。
// dp[i] = 选择 i 件物品的最大价值
// 惩罚 λ 后变成:dp'[i] = max(dp[i] - λ * i)
// 这是一个单调队列优化的DP凸性证明
设 是所有可行解的集合, 是解 的价值, 是解 选择的物品数。
定义 。
引理: 是凸函数。
证明:对于任意 和 :
因此 是凸函数。
例题:K 件物品的价值最大化
题目(CF 319C 简化):有 件物品,每件有价值 。选恰好 件物品,总价值最大。
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const int MAXN = 100005;
int n, k;
ll v[MAXN];
ll dp[MAXN]; // dp[i] = 选 i 件物品的最大价值
// 带惩罚 λ 的贪心选择
pair<ll, int> greedyWithPenalty(ll λ) {
ll val = 0;
int cnt = 0;
// 按单位价值排序,选择 v_i - λ > 0 的物品
vector<pair<ll, int>> idx;
for (int i = 1; i <= n; i++) {
idx.push_back({v[i], i});
}
sort(idx.begin(), idx.end(), [](auto& a, auto& b) {
return a.first > b.first;
});
for (auto& p : idx) {
ll benefit = p.first;
if (benefit > λ && cnt < k) {
val += benefit - λ;
cnt++;
}
}
return {val, cnt};
}注意事项
- 精度问题:使用 long long 时要注意精度损失,二分迭代60次通常足够
- 边界处理: 的上下界需要根据问题确定
- 不唯一解:如果存在多个最优解,需要额外处理
与其他优化方法的关系
| 方法 | 适用场景 | 复杂度 |
|---|---|---|
| WQS二分 | 限制选择数量的凸优化 | |
| 单调队列 | 满足四边形不等式的DP | |
| 斜率优化 | DP代价函数是线性函数 | |
| 凸包维护 | 凸代价函数的DP |
参考
Last updated: 2026-04-06