[总结] wqs 二分
参考资料:
一个算法被确定后,思路、模型就好想出来了。
概念类
wqs 二分是集训队 王钦石 在2012年提到的一种二分方式。
关键字:恰好选 \(k\) 个 ,凹凸性
恰好 \(k\) 个的限制
wqs 二分通过利用问题的性质,二分斜率把有恰好 \(k\) 个的限制转化为了容易解决的没有限制的问题。
凹凸性
给定 个物品,我们需要在其中恰好选择 个,并且需要最大化收益。设对应的收益为 ,那么需要满足在最大化收益的前提下,每多选择一个物品,额外产生的收益是单调递减的,也就是 。同时,如果我们对物品的选择数量没有限制,即 不存在,那么我们应当能够快速地计算出最大的收益,以及达到最大的收益需要选择的物品数量。
简单考虑就是每次都选最优的,最优的价值越来越小,这就是上凸的。
如果把选了 \(x\) 个数看作横坐标,其对应的价值看作纵坐标,那么凸包长这样:
wqs 二分就是利用了凸包斜率单调的性质,类似线性规划的求出斜率切在哪个点,直到切到横坐标为 \(k\) 的点且答案最大为止。
做法
假设当前二分的斜率为 \(c\),切在了点 \((k,g_k)\) 上,那么:
化简一下可得:
那么对于每一个横坐标 \(x\),设 \(f_x\) 为横坐标为 \(x\) 的点所对应的截距 \(b\),如果切在 \(x\) 那么 \(f_x\) 一定是最大的,或者说截距最大。
考虑 \(f_x\) 的本身含义:
也就是说把每个物品的价值都减去斜率后的价值,通过转化后的物品价值可以求出 \(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 二分是一种二分偏移量的思想。
细节
- 如果两条边边权相同,应优先规定好选白色还是黑色。
考虑一种三点共线的情况。
假如说 \(k\) 在红点的位置,但是在二分过程中切到了绿点,可能会影响正确性。
如果说答案保存的仅仅是斜率,那不会影响正确性,但是最后通过 \(check\) 函数通过斜率计算答案的时候横坐标一定要按照 \(k\) 来,因为你不知道会切在红点上还是绿点上。
否则,我们需要统一规定一些东西来保证正确性:
对于这种情况,我们可以在求解子问题时,尽可能地多进行交易,求解出最大的那个 值。从本质上来说,红色的点与绿色的点之间实际上只是相差了若干笔收益为 的交易而已,因此它们之间都是可以互相转换的。
多选或不选价值为 \(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;
}