WQS二分(带权二分)

WQS二分的详细解析

1. WQS二分的定义与背景

WQS二分(王钦石二分)是一种在特定问题中常用于优化的二分算法,特别适用于有一定约束条件且涉及凸性(或凹性)的问题。其核心思想是,通过二分斜率来快速求解最优解。这类问题的一个显著特点是需要选择恰好 K K K个物品或元素,并且在选择时有约束条件。它常用于求解“最优化”类问题,尤其是在涉及到递增或递减的情况下,二分可以高效缩小解空间。

2. WQS二分的核心前提:凹凸性

使用WQS二分的一个前提条件是问题本身具有凹凸性。具体来说,问题的目标函数 F ( x ) F(x) F(x)需要具备“凸性”或“凹性”,这保证了函数的单调性,从而使得可以利用二分法来进行有效搜索。对于一个上凸函数(即 F ( x ) F(x) F(x)是凸的),其函数的斜率随着 x x x的增大而递减。

例子:

在这里插入图片描述

假设有一个上凸函数 F ( x ) F(x) F(x),它的斜率是单调递减的。这意味着当我们通过计算斜率时,斜率值在变化过程中总是呈现递减趋势,且我们可以利用二分法找到一个最优斜率点。这就构成了WQS二分的基本思路:通过二分来快速定位最优解。

在这里插入图片描述

3. WQS二分的感性理解

感性理解上,WQS二分可以用两个物品的差值来做类比。如果这两个物品之间的差值较大,那么其中一个物品的数量将变少,另一个物品的数量变多。通过比较这两个物品的差值,可以决定如何调整它们的数量,使得总体的差异最小化。

关键点:
  • 当差值较大时,我们需要根据物品的数量来调整差值,通常通过二分法进行调整,找到合适的分界点。

4. WQS二分的具体实现

4.1 部分1:凹凸性

WQS二分常用于有凸性或凹性函数的问题。对于选择问题(例如选择 K K K个物品),我们知道:

  • 第一次选择时,会选择当前可以选择的最大值。
  • 当选择 K K K个物品时,我们会选择最优的价值,并且随着物品的选择,限制条件会越来越严,无法选择一些原本可以选择的物品。

可以证明: F ( K ) − F ( K − 1 ) ≤ F ( K + 1 ) − F ( K ) F(K) - F(K-1) \leq F(K+1) - F(K) F(K)F(K1)F(K+1)F(K),从而证明了 F ( K ) F(K) F(K)是凸的。即 F ( K ) F(K) F(K)是一个凸函数,其曲线在图形上呈现“凹”的形态。

伪代码


int l = ?, r = ?, ans;
while(l <= r)
{
	int mid = l + r >> 1;
	if(check(mid) >= mid) l = mid + 1 / r = mid - 1, ans = ?;
	else r = mid - 1 / l = mid + 1;
}

4.2 部分2:求被切点

假设我们已经进行了二分,得到了一个斜率 k k k,我们接下来的任务是求出该斜率所对应的切点。假设函数的斜率为 k k k,那么我们可以通过线性方程 y = k x + b y = kx + b y=kx+b来表示每个目标物体的代价增量。

在这时,我们通过贪心算法、动态规划(DP)等方法,来根据当前的代价计算最优选择的物品数量。这里需要注意:

  • 在计算最优选择时,除了计算代价最小化之外,我们还需要记录选择的物品数量。

4.3 部分3:主体

WQS二分的主体部分实际上就是一个简单的二分过程。通过二分方法,我们能够有效地缩小问题的搜索范围,并找到最优解。二分的基本步骤如下:

  1. 设定一个斜率区间,通常从最小斜率到最大斜率。
  2. 通过二分法迭代求解斜率。
  3. 在每次迭代中,根据当前斜率,计算出最优的 F ( K ) F(K) F(K)

4.4 部分4:注意点

二分结束后,我们得到的是最优的斜率。这个最优斜率可以帮助我们计算出最优的 F ( K ) F(K) F(K)。但是需要注意的是,二分过程中,我们并没有直接找到切点 F ( K ) F(K) F(K),而是找到了一个对应的斜率。

因此,二分结束后,虽然得到最优斜率,但我们仍然需要根据该斜率带入 K K K来计算出具体的 F ( K ) F(K) F(K)值。此时,最优的斜率可能会对应不同的切点,所以我们需要小心使用二分斜率。

5. 应用场景

WQS二分广泛应用于解决带有约束的最优化问题,尤其是在以下场景中非常有用:

  • 费用流问题: 在费用流模型中,我们可以通过WQS二分来优化流量增广过程中的费用,使得费用最小化。通过WQS二分优化,可以提高算法的效率,减少计算的复杂度。
  • 降维打击: 在动态规划(DP)问题中,WQS二分可以帮助我们压缩维度。特别是在选择问题中,WQS二分能有效减少需要记录的状态数量,从而减少计算开销。

6. 与费用流的关系

WQS二分和费用流算法有着紧密的关系。假设在费用流问题中,每次增广流量为 1 1 1,增广流的步骤如下:

  1. 找到一条从源点 s s s到汇点 t t t的最短路径,并进行增广。
  2. 在增广过程中,正向图的边容量会减少。

通过WQS二分,我们可以优化这种增广过程。假设我们增广 x x x次获得的流量为 f ( x ) f(x) f(x),那么 f ( x + 1 ) − f ( x ) f(x+1) - f(x) f(x+1)f(x)表示当前残量网络中的最短路径长度。根据前面的推理,残量网络的最短路径长度是单调递减的,因此,增广次数和流量之间的关系是一个凸函数(或凹函数)。

#include<bits/stdc++.h>

#define int long long
using namespace std;
const int N = 1e5 + 5,
    inf = 1e18;
inline int read() {
    int w = 1, s = 0;
    char c = getchar();
    for (; c < '0' || c > '9'; w *= (c == '-') ? -1 : 1, c = getchar());
    for (; c >= '0' && c <= '9'; s = 10 * s + (c - '0'), c = getchar());
    return s * w;
}

int n, m, a[N];
int dp[N], g[N]; //dp[i]表示前 i 个数分成若干段的最小值,g[i] 表示取到最小值分的段树 
int dq[N], l, r;
int calc(int j) { //求纵坐标 
    return dp[j] + a[j] * a[j] - 2 ll * a[j];
}
void check(int mid) {
    l = 1, r = 0;
    dp[0] = 0, g[0] = 0;
    dq[++r] = 0; //放 0 不是 1,因为可以自成 1 段。 
    for (int i = 1; i <= n; i++) {
        while (l < r && (calc(dq[l + 1]) - calc(dq[l])) < (2 ll * a[i] * (a[dq[l + 1]] - a[dq[l]]))) l++; //把开头斜率小于当前斜率的线段 pop 掉
        int j = dq[l];
        dp[i] = dp[j] + (a[i] - a[j] + 1 ll) * (a[i] - a[j] + 1 ll) + mid;
        g[i] = g[j] + 1 ll;
        while (l < r && (calc(i) - calc(dq[r])) * (a[dq[r]] - a[dq[r - 1]]) < (calc(dq[r]) - calc(dq[r - 1])) * (a[i] - a[dq[r]])) r--; //维护凸壳
        dq[++r] = i;
    }
}
signed main() {
    n = read(), m = read();
    for (int i = 1; i <= n; i++) a[i] = read(), a[i] += a[i - 1];
    int l = 0, r = inf, mid, ans = 0; //实际上斜率是负的,但是移项之后:b=f(x)-kx,所以就干脆把 k 取成正的,这样在check里是每一段+mid,而不是-mid 
    while (l <= r) {
        mid = (l + r) >> 1;
        check(mid);
        if (g[n] <= m) r = mid - 1, ans = mid;
        else l = mid + 1;
    }
    check(ans);
    printf("%lld\n", dp[n] - ans * m); //这里要减掉 mid(也就是最后的 ans) 带来的贡献 
    return 0;
}

7. 带权二分总结

WQS二分是一种非常强大的二分优化方法,广泛应用于带有限制的最优化问题。其关键在于问题具有凹凸性,通过二分斜率来找到最优解,进而优化其他相关计算。通过将二分与动态规划或贪心算法结合,WQS二分能够高效求解一些复杂问题,特别是在费用流和选择问题中的应用表现尤为突出。

上面所提到的两个例题涉及了多种算法技术,特别是二分优化(Binary Search on Answer,简称WQS)和动态规划。我们将逐步进行详细解析,解答其中的数学背景、代码结构、算法设计以及如何理解各个部分。

8. 例题精讲

例题 1:最小生成树问题

P2619 [国家集训队] Tree I

题目简介
给定一个带权无向图,其中每条边有黑色或白色。我们的目标是从图中选择恰好有 k k k 条白色边的生成树,使得生成树的权值最小。每条边的权值范围在 [ − 100 , 100 ] [-100, 100] [100,100]

算法思想
这个问题的核心在于如何找到一棵最小生成树,并且限制其中的白色边数量。

  1. 先假设全黑边构成生成树
    我们可以先用 Kruskal 算法构造一个全黑边的生成树,并且假设图中有一条“无限权重”的黑边,这样就能保证连通性。
    Kruskal算法的核心思想是按照边权从小到大排序,逐条添加到生成树中,直到树中有 n − 1 n - 1 n1 条边(对于 n n n 个节点)。如果边的两端已经连通,就跳过这条边。

  2. 逐步替换为白边
    生成树构造完成后,逐步用最小的白色边替换掉当前生成树中的黑色边。通过不断增加白边,并移除最大的黑色边,保证每次替换后的生成树仍然是一个合法的生成树。

  3. 二分法优化
    为了高效地找到最优的白色边数量,我们使用二分法来确定额外的白色边权值。二分法的核心思想是:通过设定一个额外的权值 c c c,我们就能动态调整白色边的选取,从而逐步优化生成树的权值。

算法实现

int n, m, need, cntw = 1, fa[maxn];
int find(int u) { return fa[u] == u ? u : fa[u] = find(fa[u]); }

bool add(int i) {
    int u = find(e[i].u), v = find(e[i].v);
    if (u == v) return false;
    else return fa[u] = v, true;
}

int main() {
    is >> n >> m >> need;
    FOR(i, 1, m) {
        is >> e[i].u >> e[i].v >> e[i].w >> e[i].c;
        ++e[i].u, ++e[i].v; // 注意原题点从 0 开始标号
        if (!e[i].c) swap(e[cntw++], e[i]); // 将白点集中在数组前部
    }
    sort(e + 1, e + cntw), sort(e + cntw, e + m + 1);
    int l = -100, r = 101, ans = 0;
    while (l + 1 < r) {
        int mid = (l + r) >> 1;
        FOR(i, 1, n) fa[i] = i;
        int chosen = 0, val = 0;
        int i = 1, j = cntw;
        while (i < cntw && j <= m) {
            if (e[i].w + mid <= e[j].w) { // 同权优先选白点,相当于保证最优解同时物品数最大
                if (add(i)) val += e[i].w + mid, ++chosen;
                ++i;
            } else val += e[j].w * add(j), ++j;
        }
        for (; i < cntw; ++i) if (add(i)) val += (e[i].w + mid), ++chosen;
        for (; j <= m; ++j) val += add(j) * e[j].w;
        if (chosen >= need) ans = val - mid * need, l = mid; // 可能搜到解
        else r = mid; // 不可能是解
    }
    os << ans << '\n';
    return 0;
}
  • 主要步骤
    • 初始化边集,并将白色边集中在数组前部,黑色边放在后面。
    • 使用二分法优化选取白色边的权值,并通过 Kruskal 算法选择生成树的边。
    • 每次二分选择后,检查当前生成树是否符合 k k k 条白色边的条件。

例题 2:期望问题

CF739E Gosha is hunting

题目简介
给定 n n n 个宝可梦, a a a 个 Poke Ball 和 b b b 个 Ultra Ball。每个宝可梦可以使用不同类型的球来捕获,Poke Ball 的捕获概率为 p i p_i pi,Ultra Ball 的捕获概率为 q i q_i qi。我们的目标是求最优策略下,期望抓到的宝可梦个数。

算法思想
本题是一个经典的动态规划(DP)问题,可以用 DP 来记录在给定条件下期望抓到的宝可梦个数。

  1. 定义动态规划状态
    f i , j , k f_{i,j,k} fi,j,k 表示前 i i i 个宝可梦,使用 j j j 个 Poke Ball 和 k k k 个 Ultra Ball 时的期望抓到的宝可梦个数。

  2. 状态转移
    对于每个宝可梦,可以有以下几种选择:

    • 使用 Poke Ball 捕捉: f i , j , k = f i − 1 , j − 1 , k + p i f_{i,j,k} = f_{i-1,j-1,k} + p_i fi,j,k=fi1,j1,k+pi
    • 使用 Ultra Ball 捕捉: f i , j , k = f i − 1 , j , k − 1 + q i f_{i,j,k} = f_{i-1,j,k-1} + q_i fi,j,k=fi1,j,k1+qi
    • 使用两种球捕捉: f i , j , k = f i − 1 , j − 1 , k − 1 + p i + q i − p i × q i f_{i,j,k} = f_{i-1,j-1,k-1} + p_i + q_i - p_i \times q_i fi,j,k=fi1,j1,k1+pi+qipi×qi
  3. 二分法优化
    由于每次增加一个 Ultra Ball 时,增加的期望值是递减的,因此我们可以用二分法来优化期望值的计算,使得算法在处理大规模数据时更加高效。

算法实现

using db = double;
const int maxn = 2005;
int n, a, b;
db p[maxn], q[maxn];

struct DP {
    db dp;
    int cnt;
    DP(db _dp = 0, int _cnt = 0) : dp(_dp), cnt(_cnt) {}
    il bool operator>(const DP &rhs) const {
        return (myabs(dp - rhs.dp) < 1e-7) ? (cnt < rhs.cnt) : (dp > rhs.dp);
    }
    il DP operator+(const DP &rhs) const {
        return DP(dp + rhs.dp, cnt + rhs.cnt);
    }
} f[maxn][maxn];

int main() {
    ios::sync_with_stdio(false);
    cin >> n >> a >> b;
    FOR(i, 1, n) cin >> p[i];
    FOR(i, 1, n) cin >> q[i];
    db l = 0, r = 1, ans = 0;
    for (int kase = 1; kase <= 30; ++kase) {
        db mid = (l + r) / 2;
        memset(f, 0, sizeof f);
        FOR(i, 1, n) FOR(j, 0, a) {
            chkmax(f[i][j], f[i - 1][j] + DP(0, 0));
            chkmax(f[i][j], f[i - 1][j] + DP(q[i] - mid, 1));
            if (j) {
                chkmax(f[i][j], f[i - 1][j - 1] + DP(p[i], 0));
                chkmax(f[i][j], f[i - 1][j - 1] + DP(p[i] + q[i] - p[i] * q[i] - mid, 1));
            }
        }
        DP mx;
        FOR(j, 0, a) chkmax(mx, f[n][j]);
        if (mx.cnt <= b) ans = mx.dp + mid * b, r = mid;
        else l = mid;
    }
    cout << ans << endl;
    return 0;
}
  • 主要步骤
    • 初始化动态规划表格 f f f,并进行状态转移。
    • 使用二分法调整参数,寻找最优解。每次二分选择后,检查当前选用的球数量是否符合条件。

总结

  1. 例题 1:通过 Kruskal 算法构造生成树,并使用二分法优化白色边的选取。通过分析最小生成树权值的凸性,选择适当的参数进行二分搜索,从而提高效率。

  2. 例题 2:通过动态规划记录期望值,并使用二分法对 Ultra Ball 数量进行优化。通过对期望值的递减性质进行分析,使用 WQS 二分优化,从而提高解法的时间复杂度。

这两个例题都利用了二分法优化(WQS)来求解复杂的优化问题,使得算法能够高效地处理大规模数据。在这些问题中,关键在于找到合适的优化策略,理解其数学背景和算法复杂度。

posted @ 2025-07-31 23:30  晓律  阅读(13)  评论(0)    收藏  举报  来源