[总结] wqs 二分

参考资料:

洛谷日报-wqs二分 学习笔记

知乎-wqs 二分学习笔记

一个算法被确定后,思路、模型就好想出来了。

概念类

wqs 二分是集训队 王钦石 在2012年提到的一种二分方式。

关键字:恰好选 \(k\)凹凸性

恰好 \(k\) 个的限制

wqs 二分通过利用问题的性质,二分斜率把有恰好 \(k\) 个的限制转化为了容易解决的没有限制的问题

凹凸性

给定 [总结] wqs 二分_二分 个物品,我们需要在其中恰好选择 [总结] wqs 二分_i++_02 个,并且需要最大化收益。设对应的收益为 [总结] wqs 二分_i++_03 ,那么需要满足在最大化收益的前提下,每多选择一个物品,额外产生的收益是单调递减的,也就是 [总结] wqs 二分_斜率_04 。同时,如果我们对物品的选择数量没有限制,即 [总结] wqs 二分_i++_02 不存在,那么我们应当能够快速地计算出最大的收益,以及达到最大的收益需要选择的物品数量。

简单考虑就是每次都选最优的,最优的价值越来越小,这就是上凸的。

如果把选了 \(x\) 个数看作横坐标,其对应的价值看作纵坐标,那么凸包长这样:

[总结] wqs 二分_#include_06

wqs 二分就是利用了凸包斜率单调的性质,类似线性规划的求出斜率切在哪个点,直到切到横坐标为 \(k\) 的点且答案最大为止。

做法

假设当前二分的斜率为 \(c\),切在了点 \((k,g_k)\) 上,那么:

\[g_k=k\times c+b \]

化简一下可得:

\[b=g_k-k\times c \]

那么对于每一个横坐标 \(x\),设 \(f_x\) 为横坐标为 \(x\) 的点所对应的截距 \(b\),如果切在 \(x\) 那么 \(f_x\) 一定是最大的,或者说截距最大。

考虑 \(f_x\) 的本身含义:

\[f_x=k\times (val_i-c) \]

也就是说把每个物品的价值都减去斜率后的价值,通过转化后的物品价值可以求出 \(x\)\(f_x\),从而确定这个斜率 \(c\) 切在了哪个点

换句话说,wqs 二分通过二分偏移量 \(\Delta\) 来把问题转化成没有 \(k\) 的限制的问题,直到切到 \(k\) 为止。

\(wqs\) 二分的核心是证明原问题具有凹凸性

例题

P2619 [国家集训队]Tree I

给你一个无向带权连通图,每条边是黑色或白色。让你求一棵最小权的恰好有 \(need\) 条白色边的生成树。

数据保证有解。

如果是没有限制的最小生成树,那么直接跑一遍 \(kruskal\)

如果有了 \(need\) 的限制,就转化成了上面恰好选 \(k\) 个的模型。

令横坐标为选了多少条白色边,纵坐标为对应的最小生成树的权值,通过对白色边权值增加或减少偏移量 \(\Delta\) 来使得最小生成树选到 \(k\) 条白色边。

下面证明原问题具有凹凸性:

假如当前有一棵最小生成树,它的白色边数量为 \(x\)

尝试加一条边 \(x+1\),那么这条边的权值一定比以前选过的白色边的权值都大,否则可以把第 \(x+1\) 条白色边换到前面去。

加入这条边之后,需要剪掉生成树中环上的最长的黑边,可以发现,这个最长边-最短边是越来越大的。

证毕

下面考虑二分的主体过程,如果说当前选的数目是 \(< k\) 的,说明白色边权值过大,需要减去一些,反之亦然。

可以发现,wqs 二分是紧紧依赖题目本身性质的,二分斜率只是一种方法,而 wqs 二分是一种二分偏移量的思想。

细节

  • 如果两条边边权相同,应优先规定好选白色还是黑色。

考虑一种三点共线的情况。

[总结] wqs 二分_斜率_07

假如说 \(k\) 在红点的位置,但是在二分过程中切到了绿点,可能会影响正确性。

如果说答案保存的仅仅是斜率,那不会影响正确性,但是最后通过 \(check\) 函数通过斜率计算答案的时候横坐标一定要按照 \(k\),因为你不知道会切在红点上还是绿点上。

否则,我们需要统一规定一些东西来保证正确性:

对于这种情况,我们可以在求解子问题时,尽可能地多进行交易,求解出最大的那个 [总结] wqs 二分_i++_02 值。从本质上来说,红色的点与绿色的点之间实际上只是相差了若干笔收益为 [总结] wqs 二分_#include_09的交易而已,因此它们之间都是可以互相转换的。

多选或不选价值为 \(0\) 的物品即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 5e4 + 10 , maxm = 1e5 + 10;
int fa[maxn];
struct node{
	int u,v,w,col;
	bool operator <(const node &x)const{
		if(w!=x.w)return w<x.w;
		return col<x.col;
	}
}a[maxm];
int n,m,k;
int find(int x){
	while(x!=fa[x])x=fa[x]=fa[fa[x]];
	return x;
}
#define mp make_pair
pair<int,int> check(int x){
	for(int i=1;i<=n;i++)fa[i]=i;
	for(int i=1;i<=m;i++)if(a[i].col==0)a[i].w+=x;
	sort(a+1,a+1+m);
	int cnt=0,sz=0,sum=0;
	for(int i=1;i<=m;i++){
		int u=a[i].u,v=a[i].v;
		u=find(u),v=find(v);
		if(u!=v){
			fa[u]=v;cnt++;sz+=(!a[i].col);
			sum+=a[i].w;
		}
	}
	for(int i=1;i<=m;i++)if(a[i].col==0)a[i].w-=x;
	return mp(sz,sum-x*k);
}
#define read() read<int>()
int main(){
	n=read();m=read();k=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read(),col=read();
		u++;v++;
		a[i]=(node){u,v,w,col};
	}
	int L=-100,R=100,ans=0;
	while(L<=R){
		int mid=(L+R)>>1;
		int x=check(mid).first;
		if(x>=k)L=mid+1,ans=mid;
		else if(x<k)R=mid-1;
	}
	printf("%d\n",check(ans).second);
	return 0;
}

P5633 最小度限制生成树

给你一个有 \(n\) 个节点,\(m\) 条边的带权无向图,你需要求得一个生成树,使边权总和最小,且满足编号为 \(s\) 的节点正好连了 \(k\) 条边。

证明类似上一道,这里不再给出。

需要注意的是不用每次都 \(sort\) 一遍,开始时 \(sort\) 一遍然后归并排序即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int maxn = 5e4 + 10 , maxm = 5e5 + 10;
const int INF = 0x3f3f3f3f;
struct node{
	int u,v,w;
	bool operator <(const node &x)const{
		return w<x.w;
	}
}q1[maxm],q2[maxm];
int n,m,s,k;
int cnt1,cnt2,fa[maxn];
inline int find(int x){
	while(x!=fa[x])x=fa[x]=fa[fa[x]];
	return x;
}
#define mp make_pair
pair<int,int> check(int x){
	int L1=1,L2=1;
	int cnt=0,sum=0;
	for(int i=1;i<=n;i++)fa[i]=i;
	while(L1<=cnt1 && L2<=cnt2){
		if(q1[L1].w+x<=q2[L2].w){
			node t=q1[L1];
			int u=t.u,v=t.v,w=t.w+x;
			u=find(u);v=find(v);
			if(u!=v)fa[u]=v,cnt++,sum+=w;
			L1++;
		}
		else {
			node t=q2[L2];
			int u=t.u,v=t.v,w=t.w;
			u=find(u);v=find(v);
			if(u!=v)fa[u]=v,sum+=w;
			L2++;
		}
	}
	while(L1<=cnt1){
		node t=q1[L1];
		int u=t.u,v=t.v,w=t.w+x;
		u=find(u);v=find(v);
		if(u!=v)fa[u]=v,cnt++,sum+=w;
		L1++;
	}
	while(L2<=cnt2){
		node t=q2[L2];
		int u=t.u,v=t.v,w=t.w;
		u=find(u);v=find(v);
		if(u!=v)fa[u]=v,sum+=w;
		L2++;
	}
	return mp(cnt,sum-k*x);
}
#define read() read<int>()
int main(){
	n=read();m=read();s=read();k=read();
	for(int i=1;i<=m;i++){
		int u=read(),v=read(),w=read();
		node t=(node){u,v,w};
		if(u==s || v==s)q1[++cnt1]=t;
		else q2[++cnt2]=t;
	}
	sort(q1+1,q1+1+cnt1);sort(q2+1,q2+1+cnt2);
	int L=-INF,R=INF;
	if(check(L).first<k || check(R).first>k){puts("Impossible");return 0;}
	int ans=0;
	while(L<=R){
		int mid=(L+R)>>1;
		if(check(mid).first>=k)ans=mid,L=mid+1;
		else R=mid-1;
	}
	printf("%d\n",check(ans).second);
	return 0;
}

CF802O April Fools' Problem (hard)

\(n\) 道题, 第 \(i\) 天可以花费 \(a_i\) 准备一道题, 花费 \(b_i\) 打印一道题, 每天最多准备一道, 最多打印一道, 准备的题可以留到以后打印, 求最少花费使得准备并打印 \(k\) 道题。

wqs 二分 + 基础反悔贪心

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#include <queue>
#define LL long long
const int maxn = 5e5 + 10;
LL a[maxn],b[maxn];
int n,k;
#define read() read<LL>()
#define Pair pair<LL,int>
#define mp make_pair
priority_queue<Pair,vector<Pair>,greater<Pair> > q;
pair<int,LL> check(LL x){
	while(q.size())q.pop();
	int cnt=0;LL sum=0;
	for(int i=1;i<=n;i++){
		q.push(mp(a[i],0));
		LL tmp=b[i]-x+q.top().first;
		if(tmp<0){
			sum+=tmp;q.pop();
			q.push(mp(-b[i]+x,1));
		}
	}
	while(q.size())cnt+=q.top().second,q.pop();
	return mp(cnt,sum+1LL*k*x);
}
int main(){
	n=read<int>();k=read<int>();
	for(int i=1;i<=n;i++)a[i]=read();
	for(int i=1;i<=n;i++)b[i]=read();
	LL L=0,R=2e9;
	int ans=0;
	while(L<=R){
		LL mid=(L+R)>>1;
		if(check(mid).first>=k)ans=mid,R=mid-1;
		else L=mid+1;
	}
	printf("%lld\n",check(ans).second);
	return 0;
}