wqs二分 学习笔记

wqs二分

参考资料

【学习笔记】WQS二分详解及常见理解误区解释 -ikrvxt -CSDN
wqs二分 学习笔记 -Leap_Frog -Luogu
wqs二分详解 -Hypoc_ -CSDN

前言

2024.08.13

模拟赛遇到恰好选 \(m\)的限制的反悔贪心做模拟费用流的题,然而不会wqs二分,改不完题,于是赶快学习wqs二分,遂要写下此文。


关键词

恰好选 \(m\) 个,凸性,二分斜率。

引入

有这样一类问题:

\(n\) 个物品,你要从中选出恰好 \(m\) 个物品,选的时候带有一些限制,选了一个物品后能获得价值,要求价值和最大。

同时这一类问题的特点是,如果没有 \(m\) 的限制,也就是选任意多个,能容易求得,可以是用贪心或是 DP 之类的。

然而当有 \(m\) 的限制时,我们就不能快速求得。

此时可以考虑 wqs二分 优化。

用时有一个前提:

\(g(i)\) 表示恰好选 \(i\) 个的答案,若将 \((i,g(i))\) 画在平面上并连线,要满足它是一个凸包(上凸或下凸),即这些边的斜率有单调性。

表示成代数意义就是:\(g(i)-g(i-1)\ge g(i+1)-g(i)\)

实际意义就是:一开始选最优的,然后选最优和次优的,然后越来越不优,到后面就开始下降。

然而我们实际并不知道谁更优,只知道答案是凸性。

例如像这样:

image

满足凸性就可以 wqs二分了。

算法

二分一个斜率 \(k\),一会儿就知道有什么用了。

我们让这个斜率为 \(k\) 的直线去切凸包。

我们现在要知道切的点 \(x\)\(g(x)\) 是什么,这样就能不断调整 \(k\),直到我们的 \(k\) 切到我们要求的限制 \(m\),同时我们也能知道答案 \(g(m)\)

注意我们并不是用 \(x\) 去求 \(g(x)\)

我们看被切到的点满足什么性质。

发现斜率为 \(k\) 的直线经过它时的截距 \(b\) 最大。

就像这样:

image

而我们又有,\(y=g(x)=kx+b\)

\(b=g(x)-kx\)\(b\) 最大。

考虑 \(b\) 的意义:

在这里,我们的被切点 \(x\) 还没有确定。

所以对于任意 \(x\),我们取:使得 \(g(x)-kx\) 最大的 \(x\),这个 \(x\) 就是被切点。

由于 \(x\) 是任意的,所以 \(g(x)-kx\) 就相当于,我恰好选 \(x\) 个物品,每一个选的物品的价值都减少 \(k\)

也可以看做所有物品价值都减少 \(k\),然后选恰好 \(x\) 个物品。

而对于任意 \(x\),取 \(g(x)-kx\) 最大的 \(x\)。就相当于每个物品价值减少 \(k\),可以选任意个物品,使得价值和 \(b\) 最大。

发现这就是文章开头说的没有限制选的物品个数的情况,这可以快速求。

于是我们就能知道我们选了多少个物品,选的个数为 \(x\),同时根据 \(g(x)=b+kx\) 知道我们的答案。

实现

从代码顺序的角度复述一遍上面所说的算法。

我们二分一个斜率 \(k\)

此时把所有物品的价值减 \(k\),然后快速跑一遍:选任意个物品,价值和最大。

此时能得到选的物品个数 \(x\),与答案 \(b=g(x)-kx\)

然后根据 \(x\) 调整斜率 \(k\)

具体怎么调整?

根据 \(g(x)\) 的凸包的斜率单调性来调整即可。

二分,直到 \(x=m\),最后的答案就是 \(b+kx\)

负优化 Trick:

当然你发现,当 \(x\ne m\) 时,答案或许没有用,我们可以跑到最后 \(x=m\) 时,再次用这个 \(k\) 跑一次求答案,当然这或许多此一举了。


优化费用流模型

wqs二分 常用于优化费用流模型,即优化反悔贪心&模拟费用流(反悔贪心就是模拟了费用流的退流操作)。

为什么可以优化?

因为费用流的费用是关于流量的凸函数。


例题

P2619 国家集训队

P5633 最小度限制生成树

jzoj 8118 coffee

P4694 [PA2013] Raper

P6246 [IOI2000] 邮局 加强版 加强版

abc355 Baseball


例题题解

需要注意的是,以下有一些关于 wqs 二分实现上的细节。

P2619 Tree I

我们再回顾一下 wqs 二分的过程,首先这道题目要解决的就是限定白边要恰好选 \(need\) 条,我们猜想(或感性理解)答案是关于白边边权的偏移量的凸函数,于是我们可以用 wqs 二分。

也就是二分一个偏移量,然后将白边边权都加上偏移量,然后求最小生成树,此时就能同时得到选择的白边数量和此时的权值和,然后再根据白边数量调整偏移量。

怎么调整呢,感性理解就是,加上的偏移量越小选的白边越多(因为是最小生成树).

这里,我们考虑用 Kruskal 求最小生成树,每次把白边加上偏移量,然后再排序。

实现细节:

  • 首先偏移量为整数,然后范围为 \([-100,100]\),这恰好对应边权的范围。

  • 注意到可能会出现这种情况:偏移量为 \(x\) 时选的白边数 \(<need\),而偏移量为 \(x-1\) 时,选的白边数 \(>need\),这该怎么办呢?

    还有就是当排序时相等的白边和黑边优先选哪一条?

    对于上面的问题,我们考虑为什么会出现这种情况,那就是当某条将要选的白边和将要选的黑边的权值相等时,我们不一定先选黑边也不一定先选白边。最后使得即使正确的偏移量也无法得到正确的白边数量。这种情况在凸函数的图像上就表现为出现了一段与 \(x\) 轴平行的线段,而答案在这条线段上。

    怎么解决呢?我们可以优先选白边,也就是在排序时,遇到相等的白边和黑边时,优先把白边放到前面。设函数 \(check(Mid)\) 返回偏移量为 \(Mid\) 时选的白边数,然后我们在二分时将 \(check(Mid)\ge need\) 判定为合法条件即可(注意这道题保证了数据有解,若不保证则要先判断有无解再二分)。

  • 我们可以把白边和黑边放在两个数组里,对于白边数组我们就加偏移量,然后用两个指针跑就可以将复杂度从 \(O(m\log m \log 200)\) 降为 \(O(m\log 200)\),注意依然要优先选择白边。

AC 代码(两个 \(\log\) 的每次再排序做法):

int n,m;
const int N=1e5+5;
struct arr{
	int u,v,opt,co;
	int dco;
	void getread(){
		read(u),read(v),read(dco),read(opt);
	}
}a[N];
int fa[N];
int getfa(int x){
	if(fa[x]==x)return x;
	return fa[x]=getfa(fa[x]);
}
int sum;
int check(int x){
	int cnt=0;
	fo(i,1,m)if(!a[i].opt)a[i].co=a[i].dco-x;
	else a[i].co=a[i].dco;
	sort(a+1,a+1+m,[](auto x,auto y){
		if(x.co==y.co)return x.opt<y.opt;
		else return x.co<y.co;
	});
	sum=0;
	fo(i,0,n-1)fa[i]=i;
	fo(i,1,m){
		int u=getfa(a[i].u),v=getfa(a[i].v);
		if(u!=v){
			fa[u]=v;
			sum+=a[i].co;
			if(a[i].opt==0)cnt++;
		}		
	}
	return cnt;
}
int ned;
signed main(){
//	usefile("1");
	read(n),read(m),read(ned);
	fo(i,1,m){
		a[i].getread();
	}	
	int l=-101,r=101,ans=0;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid)>=ned)ans=mid,r=mid-1;
		else l=mid+1;
	}
	check(ans);
	write(sum+ans*ned);
	return 0;
}

P5633 最小限度生成树

类似于上面这道题,然而由于可能无解,所以我们不能直接跑二分而需要先判断。

具体怎么判断有无解:

  • 首先 \(s\) 的边的个数要 \(\ge k\)

  • 原图连通。

  • \(s\) 的边断掉,然后原图分成了很多连通块,连上 \(s\) 的至多 \(k\) 条边后原图连通,即连通块个数 \(\le k+1\)(包括 \(s\) 点的连通块)。

AC 代码:

int n,m,s,K;
const int N=5e5+5;
struct arr{
	int u,v,w;
	int opt;
	int dw;
	void Read(){
		read(u),read(v),read(dw);
	}
}a[N];
int fa[N];
int getfa(int x){
	if(fa[x]==x)return x;
	return fa[x]=getfa(fa[x]);
}
int sum;
int check(int x){
	int cnt=0;
	fo(i,1,m){
		if(a[i].opt)a[i].w=a[i].dw+x;
		else a[i].w=a[i].dw;
	}
	sort(a+1,a+1+m,[](auto x,auto y){
		if(x.w==y.w)return x.opt>y.opt;
		return x.w<y.w;
	});
	sum=0;
	fo(i,1,n)fa[i]=i;
	fo(i,1,m){
		int u=getfa(a[i].u),v=getfa(a[i].v);
		if(u!=v){
			fa[u]=v;
			sum+=a[i].w;
			cnt+=a[i].opt;
		}
	}
	return cnt;
}
signed main(){
// 	usefile("1");
	read(n),read(m),read(s),read(K);
	int Cnt=0;
	int Flag=0;
	fo(i,1,n)fa[i]=i;
	fo(i,1,m){
		a[i].Read();
		if(a[i].u==s||a[i].v==s)a[i].opt=1,++Cnt;
		else{
			int u=getfa(a[i].u),v=getfa(a[i].v);
			if(u!=v)fa[u]=v;
		}
	}
	fo(i,1,n)if(getfa(i)==i)Flag++;
	if(Flag<=K+1)Flag=1;
	else Flag=0;
	int L=-3e4-1,R=3e4+1,ans=0;
	while(L<=R){
		int mid=(L+R)>>1;
		if(check(mid)>=K)ans=mid,L=mid+1;
		else R=mid-1;
	}
	if(check(ans)>=K&&Cnt>=K&&Flag){
		check(ans);
		write(sum-ans*K);
	}
	else write("Impossible");
	return 0;
}

jzoj 8118 coffee

题目大意

\(n\) 天的咖啡,第 \(i\) 天的咖啡价格 \(a_i\)

\(m\) 张优惠券,每张优惠券可以在第 \(r_i\) 及之前的天使用,可以使一杯咖啡减少 \(w_i\)

咖啡价格可以为负数,每天最多买一杯咖啡,每杯咖啡最多使用一张优惠券。

问买恰好 \(k\) 杯咖啡的最小价格。

数据范围:\(10^5\) 级别。

分析 & 解法

考虑没有 \(k\) 的限制时怎么做,这是经典的兔子进洞模型,考虑反悔贪心(模拟费用流)。

那么就是当某一杯咖啡使用优惠券后价格为负时就选它。

先插入 \(n\)\(r=n,w=0\) 的优惠券,这对答案没有影响。

接下来将优惠券按 \(r_i\) 排序。

枚举优惠券,把在 \(r_i\) 之前的咖啡插入堆里。

每次取出堆顶的 \(x\),看是否为负,是则贡献为 \(x-w_i\)

但是有个问题,就是后面可能还有 \(w_j\)\(x\) 配对更优,但是这个 \(w_i\) 已经把 \(x\) 的位置占掉了。

于是考虑反悔贪心的操作,即模拟费用流退流的操作。

我们在配对 \(x\)\(w_i\) 后,往堆里再插入一杯价值为 \(w_i\) 的咖啡,这样后面再有 \(w_j\) 配对 \(x\) 时,它就要跟 \(w_i\) 配对,此时的贡献和为:

\[(x-w_i)+(w_i-w_j)=x-w_j \]

于是这是正确的。

\(k\) 的限制怎么办?

发现答案随 \(k\) 的增大具有凸性,考虑 wqs二分。

看看时间复杂度,有一个二分斜率的 \(\log\),还有一个堆的 \(\log\),总复杂度 \(O((m+n)\log^2 n)\)

Raper

依然是反悔贪心,还是要注意 Tree I 中的细节问题。

邮局 加强版 加强版

这道题是四边形不等式得到决策单调性 DP,然后用 wqs 二分去掉个数的限制。

注意细节问题:

我们对于多个合法的决策点,我们优先选择下标小的,即在二分队列里我们用 \(<\) 判断优而不是用 \(\le\) 判断优,这样就能使对于同样的答案,我们选择的个数尽可能小,然后在比较时采用 \(check(mid)\le need\) 的做法。

而且我们发现多余的邮局可以舍去,所以不用判断二分是否有解的问题,同时运用 \(check(mid)\le need\) 刚好满足这个性质。

另外,输出答案应为 \(f_n-m*ans\) 而不是 \(f_n-cnt_n*ans\)

这里就涉及到 wqs 二分的本质,我们二分的是斜率,然而二分的这个斜率可能切了很多个点,这个斜率得到的是截距,而我们的答案是截距加偏移量乘要求选的个数,因此我们得到的 \(cnt_n\) 不等于 \(m\) 时答案取后者就不对了。

也可以说,这几个点都可以被这个斜率的直线所切,截距都相等,然而我们求的答案与 \(m\) 即横坐标有关,而与 \(cnt\) 无关即横坐标不为 \(cnt\)

posted @ 2024-08-13 22:10  dengchengyu  阅读(19)  评论(0编辑  收藏  举报