wqs二分学习笔记

wqs 二分

适用条件

对于一些满足以下要求的题目:

1.其有形似恰好 \(k\) 个的形式。

2.如果没有恰好 \(k\) 的形式是十分可做的。

3.设恰好选 \(k\) 个的答案为 \(f(k)\),那么由 \((k, f(k))\) 构成的图像为一个凸包。

ps:第三条一般打表找规律。

那么我们就可以使用 wqs 二分。

思路

假设对于每一个选 \(x\) 个最优答案为 \(f(x)\)

对于这样一个上凸包(下凸包是同理的):

我们假设要求 \(f(k)\)

如果我们忽略掉选 \(k\) 个的限制,我们发现我们肯定选到 \(y\) 坐标即 \(f(x)\) 最高的点 \((x, f(x))\),这时候的 \(x\) 不一定等于 \(k\)。但是我们只会把选 \(k\) 个的条件忽略掉做啊,那我们想想可以怎么样加一些限制使选到的点恰好是 \((k, f(k))\)

那我们可以对每个 \((x, f(x))\) 变成 \((x, f(x) + x * val)\) (\(val \in R\),即 \(val\) 可正可负可为零)。我们发现新图仍然是一个凸包,但是凸包最高点的位置变化了,不理解可以看这里。以图中的上凸包为例,如果 \(val\) 过小,凸包最高点会左移,过大会右移,如果刚刚好的话新凸包的最高点就恰好是 \((k, f(k) + k * val)\)。我们发现这个是具有单调性的。

于是我们可以对 \(val\) 进行二分,二分到的最高点如果恰好是 \((k, f(k) + k * val)\) 的话再将纵坐标减去 \(k * val\) 我们就把 \(f(k)\) 求出来了。

check()

就是忽略掉限制直接最优的选。但是对于每一个要选的物品(即规定要选 \(k\) 个的那个东西)的贡献都加上 \(val\),那么我们选 \(x\) 的贡献就刚好被加上了 \(x * val\)

最后返回最高的 \((x, f(x) + x * val)\) 即可。

关于二分的方向

设 check 选出的点为 \((x, f(x))\),答案要算 \((k, f(k))\)

假如说要用 wqs 选东西,如果优先选小的,那么应在 \(x \le k\) 时更新 \(ans\)

为什么?

这是因为凸包最高点可能有多个,既有可能共线。如果我们优先选小的,那么我们会取到多个最高点中 \(x\) 最小的点,这个点应该判 \(x \le k\) 才是合理的。假如我们在 \(x \ge k\) 时存答案,如果这个 \(val\) 的区间左端点为 \((l, f(k))\), 右端点为 \((r, f(x))\),并且存在 \(l \le k \le r\),此时优先选最小的点选到的应为 \((l, f(k))\),拿去判断 \(x \ge k\) 会被认为 \(x\) 靠左了,显然有问题。感觉讲的不是很清楚自己理解下吧。

如果优先选大的就反之。

题目

P2619 [国家集训队] Tree I

思路

假设选 \(k\) 个白边的答案为 \(f(k)\)

注意到 \((k, f(k))\) 构成的答案为下凸壳。于是直接 wqs 二分并每次 check() 直接 kruskal 即可。

需要注意的是这题是下凸壳,如果二分到的答案的 \(x\) 小于 \(k\) 应该将 \(val\) 减小。

代码

注意这道题会出现凸包 3 点共线的情况,所以假设先选白边的话应该像下面的代码一样写二分。


int n, m, k;

struct edge {
    int u, v, w, id;
    edge(int U = 0, int V = 0, int W = 0, int ID = 0) { u = U, v = V, w = W, id = ID; }
} ;
vector <edge> e;
bool operator <(edge a, edge b) { return a.w == b.w ? a.id < b.id : a.w < b.w; }

int fa[maxn];
void init() { for(int i = 1; i <= n; i++) fa[i] = i; }
int find(int x) { while(x ^ fa[x]) x = fa[x] = fa[fa[x]]; return x; }

pair <int, int> check(int val) {
    for(auto &tmp : e) if(tmp.id ^ 1) tmp.w += val;
    sort(e.begin(), e.end());
    int cnt = 0, res = 0;
    init();
    for(auto tmp : e) {
        int u = tmp.u, v = tmp.v;
        if(find(u) == find(v)) continue;
        fa[find(u)] = fa[find(v)];
        cnt += tmp.id ^ 1;
        res += tmp.w;
    }
    for(auto &tmp : e) if(tmp.id ^ 1) tmp.w -= val;
    return {cnt, res};
}

void ACehomoxue() {
    cin >> n >> m >> k;
    for(int i = 1, u, v, w, id; i <= m; i++) {
        cin >> u >> v >> w >> id;
        e.push_back(edge(u + 1, v + 1, w, id));
    }
    int l = -1000, r = 1000, ans = 0;
    while(l <= r) {
        int mid = l + (r - l) / 2;
        if(check(mid).first >= k) {
            ans = mid;
            l = mid + 1;
        } else {
            r = mid - 1;
        }
    }
    ans = check(ans).second - ans * k;
    cout << ans, el;
}

P4983 忘情

思路

注意到答案组成的图像是一个单调递减的下凸壳,考虑 wqs 二分。

考虑 check 怎么写。

中间那坨大的式子其实等价于 \(\sum ( (\sum x_i) + 1) ^ 2\),想到使用 dp 并用前缀和优化。设 \(dp_r\) 表示以 \(r\) 为结尾前缀和,显然有 \(dp_r = \max_{1 < l < r} { dp_{l - 1} + (sum_r - sum_{l - 1} + 1) ^ 2 }\),中间那坨拆开则有 \(dp_r = (sum_r + 1)^2 + \max_{1 < l < r} \{ -2 \times sum_{l - 1} + sum_l^2 - 2 \times sum_l \times sum_r \}\),加上 wqs 二分的 \(val\),有:

\[dp_r = (sum_r + 1)^2 + \max_{1 < l < r} \{ -2 \times sum_{l - 1} + sum_l^2 - 2 \times sum_l \times sum_r \} + val \]

发现是个斜率优化,直接李超水过去。

注意由于图像是一个单调递减的下凸壳,二分范围为 \([0, 10^{18}]\)

代码

由于在李超线段树中优先选小的常数大过不了,于是选择不优先选并且乱搞过了。数据真水。


int n, m, sum[maxn], x[maxn];

struct line {
	int k, b, cnt;
} pos[maxn];
inline int calc(int i, int p) { return pos[i].k * sum[p] + pos[i].b; } 

class Lichao_xds {
private:
	struct tree {
		int l, r, id;
	} t[maxn << 2];
public:
	void build(int i, int l, int r) {
		t[i].l = l, t[i].r = r, t[i].id = 0;
		if(l == r) AC;
		int mid = l + (r - l) / 2;
		build(i * 2, l, mid);
		build(i * 2 + 1, mid + 1, r);
	}
	void updata(int i, int id) {
		int mid = t[i].l + (t[i].r - t[i].l) / 2;
		if(calc(t[i].id, mid) > calc(id, mid)) swap(t[i].id, id);
		if(calc(t[i].id, t[i].l) > calc(id, t[i].l)) updata(i * 2, id);
		if(calc(t[i].id, t[i].r) > calc(id, t[i].r)) updata(i * 2 + 1, id);
	}
	inline int query(int i, int p) {
		if(t[i].l == t[i].r) return t[i].id;
		int mid = t[i].l + (t[i].r - t[i].l) / 2, res;
		if(p <= mid) res = query(i * 2, p);
		else res = query(i * 2 + 1, p);
		return calc(t[i].id, p) < calc(res, p) ? t[i].id : res;
	}
} ds;

int dp[maxn], f[maxn];
inline pair <int, int> check(int val) {
	memset(dp, 0, sizeof(dp));
	memset(f, 0, sizeof(f));
	memset(pos, 0, sizeof(pos));
	ds.build(1, 1, n);
	pos[0].k = 0, pos[0].b = 1e18;
	for(int i = 1; i <= n; i++) {
		if(i) {
			pos[i - 1].k = -2 * sum[i - 1];
			pos[i - 1].b = sum[i - 1] * sum[i - 1] - 2 * sum[i - 1] + dp[i - 1];
			pos[i - 1].cnt = f[i - 1];
			ds.updata(1, i - 1);
		}
		int mx = ds.query(1, i);
		dp[i] = (sum[i] + 1) * (sum[i] + 1) + val + calc(mx, i);
		f[i] = pos[mx].cnt + 1;
	}
	return {f[n], dp[n]};
}

void ACehomoxue() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        cin >> x[i];
        sum[i] = sum[i - 1] + x[i];
    }	
	int l = 0, r = 3 * 1e18, ans = 0;
	while(l <= r) {
		int mid = l + (r - l) / 2;
		if(check(mid).first <= m) {
			ans = mid;
			r = mid - 1;
		} else l = mid + 1;
	}
	cout << check(ans).second - ans * m, el;
}

P5896 [IOI 2016] aliens

思路

第一步转化很重要。注意到拍摄范围的正方形的对角线和网格重合,于是可以将一个 \((x, y)\) 的点转化为一个 \([\min(x, y), \max(x, y)]\) 的区间,一个左下端点为 \((l, l)\) 和右上端点为 \((r, r)\) 的正方形转化为一个 \([l, r]\) 的区间。

我们注意到这个转换之后答案是一个单调递减的凸包,套一个 wqs 二分之后我们就发现和 P6047 很像了。一样的去掉没有用的区间一样的斜率优化,一样的李超。

代码

注意在 dp 的时候要减去重复的区间

#define int ll
#define ll long long

int n, m, k;

struct line {
    int k, b;
} pos[maxn];

struct pt {
    int l, r;
    friend inline bool operator <(pt a, pt b) {
        return a.l == b.l ? a.r > b.r : a.l < b.l;
    }
} a[maxn];
vector <pt> vec;

int dp[maxn], f[maxn];
inline int calc(int p, int id) { return pos[id].k * p + pos[id].b; }
bool cmp(int id1, int id2, int p) { return calc(p, id1) != calc(p, id2) ? calc(p, id1) < calc(p, id2) : f[id1 - 1] > f[id2 - 1]; }
struct Lichao_xds {
private:
    struct tree {
        int l, r, id;
    } t[maxn << 2];
public:
    void build(int i, int l, int r) {
        t[i].l = l, t[i].r = r, t[i].id = 1;
        if(l == r) AC;
        int mid = l + (r - l) / 2;
        build(i * 2, l, mid);
        build(i * 2 + 1, mid + 1, r);
    }
    void updata(int i, int id) {
        int mid = t[i].l + (t[i].r - t[i].l) / 2;
        if(cmp(id, t[i].id, mid)) swap(id, t[i].id);
        if(cmp(id, t[i].id, t[i].l)) updata(i * 2, id);
        if(cmp(id, t[i].id, t[i].r)) updata(i * 2 + 1, id);
    }
    inline int query(int i, int p) {
        if(t[i].l == t[i].r) return t[i].id;
        int mid = t[i].l + (t[i].r - t[i].l) / 2, res;
        if(p <= mid) res = query(i * 2, p);
        else res = query(i * 2 + 1, p);
        return cmp(res, t[i].id, p) ? res : t[i].id;
    }
} ds;

pair <int, int> check(int val) {
    memset(dp, 0, sizeof(dp));
    memset(f, 0, sizeof(f));
    ds.build(1, 1, m + 1);
    a[0].r = 0;
    for(int i = 1, x; i <= n; i++) {
        pos[i].k = -2 * a[i].l;
        pos[i].b = dp[i - 1] + a[i].l * a[i].l - max(0ll, a[i - 1].r - a[i].l + 1) * max(0ll, a[i - 1].r - a[i].l + 1);
        if(i ^ 1) ds.updata(1, i);
        x = ds.query(1, a[i].r + 1);
        dp[i] = calc(a[i].r + 1, x) + (a[i].r + 1) * (a[i].r + 1) + val;
        f[i] = f[x - 1] + 1;
    }
    return {f[n], dp[n]};
}

void ACehomoxue() {
    cin >> n >> m >> k;
    for(int i = 1, x, y; i <= n; i++) {
        cin >> x >> y;
        a[i].l = min(x, y) + 1;
        a[i].r = max(x, y) + 1;
        vec.push_back(a[i]);
    }
    memset(a, 0, sizeof(a));
    sort(vec.begin(), vec.end());
    n = 0;
    for(auto tmp : vec) {
        if(!n) { a[++n] = tmp; continue; }
        if(a[n].l <= tmp.l && a[n].r >= tmp.r) continue;
        else a[++n] = tmp;
    }
    k = min(n, k);
    int l = 0, r = (m + 1) * (m + 1) + 6, ans = 0;
    while(l <= r) {
        int mid = l + (r - l) / 2; 
        if(check(mid).first >= k) {
            ans = mid;
            l = mid + 1;
        } else r = mid - 1;
    }
    cout << check(ans).second - ans * k, el;
}   

P6246 [IOI 2000] 邮局 加强版 加强版

思路

毒瘤。紫的双倍经验绿的双倍经验

首先想到,邮局越多越好,于是答案应该是一个单减的下凸包(一般这种越多越好或越少越好的都是单调的凸包),于是用 wqs 二分弄掉限制。

假设 \(i\)\(j\)\(k\) 表示编号区间 \([i, k]\) 的村庄钦定 \(j\) 上建一个邮局并为区间最近的邮局。虽然这样做,可能会有不合法的情况,即区间内有的村庄的最近邮局不在 \(j\),但显然不合法的情况是比合法的情况劣的,所以不用考虑直接这么做就行。

这样维护三个值是 \(n^3\) 的。

我们可以拆开。\(dp1_i\) 表示 \(i\) 作为 \(k\) 点时的(即是一个钦定区间的结尾的情况),\(dp2_i\) 表示 \(i\) 作为 \(j\) 点(即是一个钦定区间中钦定的建邮局的点)。

先看 \(dp1_i\),显然:

\[dp1_i = \min_j \{sum_i - sum_j - (i - j) \times a_j + dp2_j\} + val \]

整理,得:

\[dp1_i = sum_i + \min \{-a_j \times i + j \times a_j + dp2_j\} + val \]

\(x = i\)\(k = -a_j\)\(b = j \times a_j + dp2_j\),斜率优化可做。

在看 \(dp2_i\),有:

\[dp2_i = \min \{(i - j) \times a_i - sum_i + sum_j + dp1_j\} \]

整理,得:

\[dp2_i = i \times a_i - sum_i + \min \{-j * a_i + sum_j + dp1_j\} \]

\(x = a_i\)\(k = -j\)\(b = sum_j + dp1_j\),斜率优化可做。

同时开两个李超线段树维护即可。

记得再开一个数组记录建了几个邮局。\(f1_i\) 对应 \(dp1_i\)\(f2_i\) 对应 \(dp2_i\)。直接跟着 \(dp\) 数组转移即可,\(f1_i = f2_j + 1\)\(f2_i = f1_j\)

记得离散化。

代码

优美的指针。 注意卡常。

#define int ll
#define ll long long

int n, m, a[maxn], sum[maxn], tt[maxn];

struct line {
    int k, b;
} pos1[maxn], pos2[maxn];

int dp1[maxn], dp2[maxn], f1[maxn], f2[maxn];

inline int calc(line id, int p) { return id.k * p + id.b; } 

class Lichao_xds {
public:
    line *pos;
    int *dp, *f, *rfl;
    inline int calc(int id, int p) { return pos[id].k * rfl[p] + pos[id].b; }
    inline int cnt(int id) { return f[id]; }
    inline bool cmp(int id1, int id2, int p) { return calc(id1, p) == calc(id2, p) ? f[id1] < f[id2] : calc(id1, p) < calc(id2, p); }
    struct tree {
        int l, r, id;
    } t[maxn << 2];
    void build(int i, int l, int r, int c) {
        t[i].l = l, t[i].r = r, t[i].id = c;
        if(l == r) AC;
        int mid = l + (r - l) / 2;
        build(i * 2, l, mid, c);
        build(i * 2 + 1, mid + 1, r, c);
    }
    void set(int *DP, int *F, line *POS, int c, int *RFL) {
        pos = POS;
        dp = DP;
        f = F;
        rfl = RFL;
        build(1, 1, n, c);
    }
    void updata(int i, int id) {
        int mid = t[i].l + (t[i].r - t[i].l) / 2;
        if(cmp(id, t[i].id, mid)) swap(t[i].id, id);
        if(cmp(id, t[i].id, t[i].l)) updata(i * 2, id);
        if(cmp(id, t[i].id, t[i].r)) updata(i * 2 + 1, id);
    }
    inline int query(int i, int p) {
        if(t[i].l == p && t[i].r == p) return t[i].id;
        int mid = t[i].l + (t[i].r - t[i].l) / 2, res;
        if(p <= mid) res = query(i * 2, p);
        else res = query(i * 2 + 1, p);
        return cmp(res, t[i].id, p) ? res : t[i].id;
    }
} ds1, ds2;

inline pair <int, int> check(int val) {
    for(int i = 0; i <= n; i++) dp1[i] = dp2[i] = f1[i] = f2[i] = 0;
    ds1.set(dp1, f2, pos2, 1, tt);
    ds2.set(dp2, f1, pos1, 0, a);
    for(int i = 1, j; i <= n; i++) {
        j = ds2.query(1, i);
        dp2[i] = i * a[i] - sum[i] + ds2.calc(j, i);
        f2[i] = ds2.cnt(j);
        ds1.pos[i].k = -a[i], ds1.pos[i].b = i * a[i] - sum[i] + dp2[i];
        ds1.updata(1, i);
        j = ds1.query(1, i);
        dp1[i] = sum[i] + ds1.calc(j, i) + val;
        f1[i] = ds1.cnt(j) + 1;
        ds2.pos[i].k = -i, ds2.pos[i].b = sum[i] + dp1[i];
        ds2.updata(1, i);
    }
    // debug(dp1[n]); debug(f1[n]);
    return {f1[n], dp1[n]};
}

void ACehomoxue() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) {
        tt[i] = i;
        cin >> a[i];
        sum[i] = sum[i - 1] + a[i]; 
    }    
    int l = 0, r = 2 * 1e6, ans = 0;
    while(l <= r) {
        int mid = l + (r - l) / 2;
        if(check(mid).first <= m) {
            ans = mid;
            r = mid - 1;
        } else l = mid + 1;
    }
    cout << check(ans).second - m * ans, el;
}
posted @ 2026-04-14 17:34  ACehomoxue  阅读(23)  评论(0)    收藏  举报