wqs二分

我全是二分。

今天一个下午调模板题结果发现是 int 返回值打成了 bool!哈哈哈我是小丑。


这个东西应该算是一种思想而不是具体的算法,跟 CDQ 分治,整体二分这些性质上差不多。

wqs 二分主要是解决恰好选取 $k$ 个元素时的最小(最大)代价这样一类问题的。

它的思想主要是:引入一个惩罚/奖励函数(或常数)$f(k)$,表示在选取 $k$ 个元素时会额外获得的代价。需要注意的是,这个 $f(k) = k \times \Delta$,而其中的 $\Delta$ 便是 wqs 二分的精髓。

首先来一道例题

如果没有恰好 $need$ 条白边的限制,那么这道题就是最小生成树板题了。

如果有这个限制该怎么办?

正常跑最小生成树会有下面三种情况:

  1. 选取了少于 $need$ 条白边。
  2. 选取了等于 $need$ 条白边。
  3. 选取了多于 $need$ 条白边。

分析第一种和第三种情况,造成这种结果的原因是:某些白边的权值较大(或较小),贪心地选择权值最小的边会更少地(或更多地)使用白边。

于是我们人为地改变白边的权值,这个时候就会用到惩罚函数了:我们给每条白边的权值增加 $\Delta$ 再跑最小生成树,这个时候仍然会有上面三种情况,我们按以下方式处理:

  1. 选取了少于 $need$ 条白边,这个时候白边的权值仍然是较大的,我们需要将 $\Delta$ 调小。
  2. 选取了等于 $need$ 条白边,这个时候正好得到了答案,注意最后的结果应该减去惩罚 $f(need)$。
  3. 选取了多于 $need$ 条白边,这个时候白边的权值仍然是较小的,我们需要将 $\Delta$ 调大。

说明一下为什么这么做是对的:

感性地理解,我们对白边的权值整体做出了调整(相对大小不变),使得在跑最小生成树时能恰好使用出 $need$ 条白边,按照 Kruskal 的贪心策略这个时候一定是当前状态的最优解。

严谨的证明:

令函数 $g(k)$ 表示恰好选 $k$ 条白边时,最小生成树的权值。如果我们取 $g(1), g(2), g(3), \cdots g(m)$ 的值画在平面直角坐标系中,再将它们依次相连,就会发现这是一个下凸壳。

(具体解释并不方便,但是这道题中这一性质算是比较直观的了吧。不严谨的讲,假设最优的情况下我们会选 $x$ 条白边,那么小于 $x$ 条和 大于 $x$ 条的情况肯定都不会优于选 $x$ 条的情况,并且和 $x$ 离得越远那么就越劣,可以理解为条件更加严苛。)

直接跑最小生成树的话相当于选取一条斜率为 $0$ 的直线与这个下凸壳相切,切到的点就是最有情况下的方案。

但是我们并不想要切到最优决策,而是想要切到一个指定的点。思考一下为什么会切到这个点?因为它的 $y$ 坐标最小($g(x)$ 最小),我们选取斜率为 $0$ 的直线去切的时候才会切到它,现在我们改变这个直线的斜率,使它能够恰好切到我们想要的点。

或者也可以理解为:把这个下凸壳给抬高(或压低),并且越靠右抬得就越高(或低),这样就会人为地改变 $y$ 坐标最小的点了,于是我们能切到它。

实际上实现还有很多细节,比如会出现多点共线的情况,这个时候就需要令 check 函数更倾向于选白边(就是说边权相同的时候优先选白边),若当前选出的白边数量大于等于 $need$ 则更新答案并上调 $\Delta$,否则下调 $\Delta$。

这一操作的本质上是在用直线切凸壳的时候切到最靠右的点,实际上切最靠左的点也行。

放一下代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, m, need, l, r, mid, ans, sum, u, v, father[50005];
struct edge {
    int u, v, w, c;
    bool operator < (const edge& _) const {
        return w != _.w ? w < _.w : c < _.c;
    }
} e[100005];
int findset(int x) {return father[x] == x ? x : father[x] = findset(father[x]);}
int check(int c) {
    for(int i = 1; i <= m; ++i) {
        if(!e[i].c) e[i].w += c;
    }
    for(int i = 1; i <= n; ++i) father[i] = i;
    stable_sort(e + 1, e + 1 + m);
    sum = 0;
    int ret = 0;
    for(int i = 1; i <= m; ++i) {
        u = findset(e[i].u), v = findset(e[i].v);
        if(u != v) {
            sum += e[i].w;
            ret += !e[i].c;
            father[u] = v;
        }
    }
    for(int i = 1; i <= m; ++i) {
        if(!e[i].c) e[i].w -= c;
    }
    return ret;
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n >> m >> need;
    for(int i = 1; i <= m; ++i) {
        cin >> e[i].u >> e[i].v >> e[i].w >> e[i].c;
        ++e[i].u, ++e[i].v;
    }
    l = -100, r = 100;
    while(l <= r) mid = (l + r) >> 1, (check(mid) >= need) ? ((ans = sum - mid * need), l = mid + 1) : (r = mid - 1);
    cout << ans;
    return 0;
}
posted @ 2023-12-23 17:34  A_box_of_yogurt  阅读(3)  评论(0编辑  收藏  举报  来源
Document