zoukankan      html  css  js  c++  java
  • wqs二分学习笔记

    怎么总是因为一场模拟赛来填坑啊 /kel

    Ubuntu 没有几何画板(悲)

    适用问题

    题目类型:给定 (n) 个物品,要求刚好选择 (m) 个,最大/小化权值。

    特点:如果没有限制,能够较简单地求出最优解

    使用前提:设取 (k) 个物品的最优决策是 (f(k)) ,那么函数 (y=f(x)) 必须具有凹凸性(即凹/凸函数,或者你愿意说图像是个凸包也可以)

    算法主体

    以下默认讨论的是上凸包。

    首先,来考虑对于一个固定的 (m) ,如何求出 (f(m)) .

    对于一个上凸包,有一个显然的性质(定义):随着 (x) 轴坐标增大,斜率单调递减。

    这样一个具有单调性的东西,容易想到二分。我们可以二分一个斜率 (k) ,然后找到斜率为 (k) 的直线和这个凸包相切的切点。就像这样:

    wqs1

    大眼观察得 随着 (k) 的减小,切点会向右移动。于是可以二分 (k) 直到切点横坐标为 (m) ,那么其纵坐标就是答案。

    现在的问题就是如何判定切点位置。

    考虑多条斜率为 (k) 的直线,如下图:

    wqs2

    显然,过切点的直线在所有直线中处于最上方,也就是说,直线在 (y) 轴上的截距最大。

    设截距为 (b) ,那么直线方程就是 (y=kx+b) . 显然,这等价于给每个物品的权值加上 (k) 之后,选了 (x) 个物品的总权值。因此,我们可以直接将所有物品的权值加上 (k) ,在没有个数限制的情形下计算得到最大的 (x) ,然后由于要求的是切点横坐标为 (m) 的情况,所以可以根据目前的 (x)(k) 进行调整,如果 (x)(m) 左边,那么斜率要减小;反之增大。(当然,这里讨论的是上凸包)

    (由于有前提:如果没有限制,能够较简单地求出最优解 ,所以这个 check 是很容易能够在较低的复杂度内实现的)

    思路还是很好理解的,结合图像更佳。但是口胡是不行的!我们要做题!代码说明一切

    Tree I

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

    Solution

    这道题满足了 “恰好 (k) 个” 的限制,且在没有限制的情况下可以轻松地用 Kruskal 求解,因此满足了使用 WQS 二分的前提。

    模板题,直接二分然后把所有白边加上这个权值就好了。

    有一个细节:如果你跟我一样,写的是整数二分,那么请注意,对于边权相等的情况,需要强制一个 “优先选黑/白边” 的条件,然后根据相应的情况在二分条件中写 >= 或者 <= .

    //白边优先的情况
    bool operator < ( const Edge &tmp ) const { return (val^tmp.val) ? val<tmp.val : typ<tmp.typ; }
    
    if ( tmp>=k ) l=mid+1,ans=mid;
    else r=mid-1;
    
    //黑边优先的情况
    bool operator < ( const Edge &tmp ) const { return (val^tmp.val) ? val<tmp.val : typ>tmp.typ; }
    
    if ( tmp<=k ) r=mid-1,ans=mid;
    else l=mid+1;
    

    原理的话就是尽可能多选黑/白边,以避免边权相等时出现奇怪的不可控情况,反正只要相等时有个顺序就行了。

    //Author: RingweEH
    const int N=5e4+10,M=1e5+10;
    struct Edge
    {
    	int fro,to,val,typ;
    	bool operator < ( const Edge &tmp ) const { return (val^tmp.val) ? val<tmp.val : typ<tmp.typ; }
    }e[M];
    int n,m,k,fa[N];
    
    int find( int x )
    {
    	return (x==fa[x]) ? x : fa[x]=find(fa[x]);
    }
    
    int check( int &funcx,int del )
    {
    	for ( int i=1; i<=m; i++ )
    		if ( e[i].typ==0 ) e[i].val+=del;
    	sort( e+1,e+1+m );
    	for ( int i=1; i<=n; i++ )
    		fa[i]=i;
    	int tot=0,cnt=0,sum=0;
    	for ( int i=1; i<=m; i++ )
    	{
    		int u=e[i].fro,v=e[i].to; u=find(u),v=find(v);
    		if ( u==v ) continue;
    		fa[u]=v; cnt++; tot+=(e[i].typ==0); sum+=e[i].val;
    		if ( cnt==(n-1) ) break;
    	}
    	for ( int i=1; i<=m; i++ )
    		if ( e[i].typ==0 ) e[i].val-=del;
    	funcx=sum; return tot;
    }
    
    int main()
    {
    	n=read(); m=read(); k=read();
    	for ( int i=1; i<=m; i++ )
    		e[i].fro=read()+1,e[i].to=read()+1,e[i].val=read(),e[i].typ=read();
    
    	int l=-100,r=100,ans=0; int res=0;
    	while ( l<=r ) 
    	{
    		int mid=(l+r)>>1,tmp=check( res,mid );
    		if ( tmp>=k ) l=mid+1,ans=mid;
    		else r=mid-1;
    	}
    
    	check(res,ans); res=res-ans*k;
    	printf( "%d
    ",res );
    
        return 0;
    }
    

    最小度限制生成树

    给定一个 (n)(m) 边带权无向图,求一棵点 (s) 正好连了 (k) 条边的最小生成树。

    Solution

    题面中满足了 “正好 (k) 个” 的条件,且没有限制的最小生成树很容易求解,前提充分。

    显然这里的物品就是和 (s) 相连的所有边了。二分 (k) 给这些边加上就行。

    但是注意数据范围:(1leq n le 5 imes 10^4,1leq m le 5 imes 10^5.) 如果是 (Omicron(mlog ^2 )) 显然非常的危。

    所以可以加一点点 小优化

    最开始把 (s) 连的边和其他边分开,排个序,然后跑最小生成树的时候归并排序即可。

    显然这样只需要合并一次,而且所有相连的边增加同一个值,顺序不变。

    代码需要注意一些细节和判断无解,如:

    • 排序的时候相同权值,和 (s) 相连优先
    • 如果没有改变权值也不能找出生成树,无解
    • 跑出答案之后再找一遍生成树,如果无解或者 (s) 的度数不等于 (k) 也是无解
    • 没给边权范围就离谱,但是我也不知道 int 能不能过去
    //Author: RingweEH
    const int N=5e4+10,M=5e5+10,INF=1e9;
    struct Edge
    {
    	int fro,to; ll val;
    	bool operator < ( const Edge &tmp ) const { return val<tmp.val; }
    }e1[M],e2[M],e[M];
    int n,m,s,k,tot1=0,tot2=0,fa[N]; 
    ll nowsum;
    
    bool has( Edge x )
    {
    	if ( x.fro==s ) return 1;
    	if ( x.to==s ) return 1;
    	return 0;
    }
    
    void Merge_Sort()
    {
    	int i=0,j=0,tot=0;
    	while ( (i<tot1) && (j<tot2) ) 
    	{
    		Edge t1=e1[i+1],t2=e2[j+1];
    		if ( (t1.val<t2.val) || ((t1.val==t2.val) && (has(t1))) ) e[++tot]=t1,i++;
    		else e[++tot]=t2,j++;
    	}
    	while ( i<tot1 ) e[++tot]=e1[++i];
    	while ( j<tot2 ) e[++tot]=e2[++j];
    }
    
    int find( int x )
    {
    	return (x==fa[x]) ? x : fa[x]=find(fa[x]);
    }
    
    int Kruskal()
    {
    	for ( int i=1; i<=n; i++ )
    		fa[i]=i;
    	int cnt=0,cnts=0; ll sum=0;
    	for ( int i=1; i<=m; i++ )
    	{
    		int u=e[i].fro,v=e[i].to; ll w=e[i].val; 
    		u=find(u); v=find(v);
    		if ( u==v ) continue;
    		fa[u]=v; sum+=w; cnt++;
    		if ( has(e[i])) cnts++;
    		if ( cnt==(n-1) ) break;
    	}
    	if ( cnt<(n-1) ) return -1;
    	nowsum=sum; return cnts;
    }
    
    int check( int x )
    {
    	for ( int i=1; i<=tot1; i++ )
    		e1[i].val+=x;
    	Merge_Sort();
    	int res=Kruskal();
    	for ( int i=1; i<=tot1; i++ )
    		e1[i].val-=x;
    	return res;
    }
    
    int main()
    {
    	n=read(); m=read(); s=read(); k=read();
    	for ( int i=1; i<=m; i++ )
    	{
    		int u=read(),v=read(),w=read();
    		if ( u==s ) { e1[++tot1].fro=u,e1[tot1].to=v; e1[tot1].val=w; }
    		else if ( v==s ) { e1[++tot1].fro=v,e1[tot1].to=u; e1[tot1].val=w; }
    		else { e2[++tot2].fro=u; e2[tot2].to=v; e2[tot2].val=w; }
    	}
    
    	sort( e1+1,e1+1+tot1 ); sort( e2+1,e2+1+tot2 );
    	if ( check(0)==-1 ) { printf( "Impossible
    " ); return 0; }
    	int l=-INF,r=INF,ans=-INF;
    	while ( l<=r )
    	{
    		int mid=(l+r)>>1;
    		if ( check(mid)>=k ) l=mid+1,ans=max(ans,mid);
    		else r=mid-1;
    	}
    
    	int now=check(ans);
    	if ( (now==-1) || (now^k) ) printf( "Impossible
    " );
    	else 
    	{
    		ll ans_sum=nowsum-ans*k;
    		printf( "%lld
    ",ans_sum );
    	}
        return 0;
    }
    

    April Fools' Problem (hard)

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

    Solution

    看到 (k) 个东西,就能想到 wqs二分了。

    显然,斜率单调不降,因此这题是个下凸包,把每个物品的权值减去 (mid) 即可。然后来考虑怎么写 checker .

    对于每一天,有三种选择:

    1. 跳过这一天
    2. 准备一道题(将可选项中加入一个 (a_i)
    3. 打印出现过的最小的一个 (a_i) (用 (b_i) 和之前的 (a_i) 配对)

    显然这个东西可以用优先队列维护。每个 (b_i) 有两种选择:

    1. 和某个新的 (a_i) 配对,取堆顶即可。
    2. 替换之前某个 (a_i) 所配的 (b_i) ,这个就直接类似反悔贪心一样搞,往堆里面加入一个 (b_i-del) 即可,这样当你访问到 (b_j) 的时候,(b_j-del-val=b_j-del-b_i+del=b_j-b_i) ,就相当于加入差值了。

    那么就做完了。写WQS第一次一遍AC,我不行

    奉送双倍经验:[PA2013]Raper

    //Author: RingweEH
    const int N=5e5+10;
    struct Node
    {
    	ll val; int typ;
    	Node ( ll _val=0,int _typ=0 ) { val=_val; typ=_typ; }
    	bool operator < ( const Node &tmp ) const { return val<tmp.val; }
    };
    int n,k;
    ll a[N],b[N],sav_sum=0;
    priority_queue<Node> q;
    
    int check( ll del )
    {
    	ll sum=0;
    	for ( int i=1; i<=n; i++ )
    	{
    		Node t(-a[i],0); q.push(t);
    		Node now=q.top();
    		ll tmp=b[i]-del-now.val;
    		if ( tmp<0 )
    		{
    			sum+=tmp; q.pop();
    			q.push( Node(b[i]-del,1) );
    		}
    	}
    	int cnt=0; sav_sum=sum;
    	while ( !q.empty() ) { cnt+=(q.top().typ==1); q.pop(); }
    	return cnt;
    }
    
    int main()
    {
    	n=read(); k=read();
    	for ( int i=1; i<=n; i++ )
    		a[i]=read();
    	for ( int i=1; i<=n; i++ )
    		b[i]=read();
    
    	ll l=0,r=3e9,ans=0;
    	while ( l<=r )
    	{
    		ll mid=(l+r)>>1; int now=check(mid);
    		if ( now<=k ) l=mid+1,ans=mid;
    		else r=mid-1;
    	}
    
    	check(ans);
    	printf( "%lld
    ",sav_sum+ans*k );
    
        return 0;
    }
    

    忘情

    给定一个式子,表示序列的值:

    [frac{left((sumlimits_{i=1}^{n}x_i×ar x)+ar x ight)^2}{ar x^2} ]

    给定一个长度为 (n) 的序列,要求分成 (m) 段且每段的值之和最小,求最小值。

    (mleq nleq 1e5,1leq x_ileq 1000) .

    Solution

    这式子纯粹是来恶心人的qwq

    In fact , 上下除以 (ar x) 就会变成:

    [left(sumlimits_{i=1}^{n}x_i+1 ight)^2 ]

    这样就清新多了。而且显然平方里面的东西可以前缀和预处理出来。

    那么现在就是 DP 一眼题:

    [f[i]=min{f[j]+(S[i]-S[j]+1)^2} ]

    但是可惜的是,这是个 (Omicron(n^2)) 的式子……考虑优化。

    然后发现,这个式子几乎跟 这道斜优板子 一模一样!(不会斜优请自行前往)

    来推个式子:

    [f[i]=f[j]+S[i]^2-2S[i]S[j]+S[j]^2+2S[i]-2S[j]+1=>y=kx+b\\ -f[j]-S[j]^2+2S[j]=-2S[i]cdot S[j]+(S[i]^2-f[i]+2S[i]+1)\\ f[j]+S[j]^2-2S[j]=2S[i]S[j]+(f[i]-S[i]^2-2S[i]-1) ]

    斜率 (k=2S[i])(x=S[j])(b=f[i]-S[i]^2-2S[i]+1)(y=f[j]+S[j]^2-2S[j]) .

    显然,斜率单增,因此最优决策点单增,可以决策单调性再优化,直接一个单调队列维护就好了。

    然后这个 WQS二分 也是个下凸包的板子。

    板子套板子.jpg

    我有问题 我一开始写成上凸包了 然后又没开 long long

    //Author: RingweEH
    const int N=1e5+10;
    int n,m,cnt_block[N],q[N];
    ll f[N],S[N];
    ll X( ll num ) { return S[num]; }
    ll Y( ll num ) { return f[num]+S[num]*S[num]-2*S[num]; }
    db slope( ll t1,ll t2 ) { return (db)(Y(t2)-Y(t1))/(X(t2)-X(t1)); }
    
    int check( ll del )
    {
    	memset( f,0x3f,sizeof(f) ); memset( cnt_block,0,sizeof(cnt_block) );
    	int head=1,tail=0; q[++tail]=f[0]=0;
    	for ( int i=1; i<=n; i++ )
    	{
    		while ( head<tail && slope(q[head],q[head+1])<2*S[i] ) head++;
    		f[i]=f[q[head]]+(S[i]-S[q[head]]+1)*(S[i]-S[q[head]]+1)+del; 
    		cnt_block[i]=cnt_block[q[head]]+1;
    		while ( head<tail && slope(q[tail-1],q[tail])>slope(q[tail-1],i) ) tail--;
    		q[++tail]=i;
    	}
    	return cnt_block[n];
    }
    
    int main()
    {
    	n=read(); m=read();
    	for ( int i=1; i<=n; i++ )
    		S[i]=read();
    
    	for ( int i=2; i<=n; i++ )
    		S[i]+=S[i-1];
    	ll l=0,r=1e16,ans=0;
    	while ( l<=r )
    	{
    		ll mid=(l+r)>>1;
    		if ( check(mid)<=m ) r=mid-1,ans=f[n]-mid*m;
    		else l=mid+1;
    	}
    
    	printf( "%lld
    ",ans );
        return 0;
    }
    

    林克卡特树

    给定一棵 (n) 点带权树,去掉其中 (k) 条边,再加上 (k) 条边权为 (0) 的边。可以任意选择两点 (p,q) ,求 (p,q) 树上路径的边权和的最大值。求这个值。

    (1leq nleq 3e5,0leq kleq 3e5,k<n,|v_i|leq 1e6)

    Solution

    将一棵树删去 (k) 条边,会出现 (k+1) 个连通块,而新的边边权为 (0) ,对答案没有贡献,也就是说,我们只需要求出每个连通块内的直径即可。

    放回原树上,其实我们并不用真的删去 (k) 条边,而是转化成选择了 (k+1) 条不相邻的链/点(也就是删完之后每个连通块的直径)。

    这个问题显然可以用 DP 解决。设 (f[u][i]) 表示子树 (u) 中选择了 (i) 条链的最大价值。

    然而这样好像并不利于转移 考虑一些特殊性质。

    注意到最后的链是不相交的,因此每个点的度数至多为 (2) . 不妨再增设一维状态:(f[0/1/2][u][i]) 表示点 (u) 的度数。设当前子树为 (v) ,考虑三种情况:

    1. (u) 度数为 (0) . (f[0][u][i]=max(f[0][u][j]+max(f[0/1/2][v][i-j])))
    2. (u) 度数为 (1),说明点 (u) 处是一条链的端点,(f[1][u][i]=max(f[1][u][j]+f[0][v][i-j],f[0][u][j]+f[1][v][i-j]+w(u,v)))
    3. (u) 度数为 (2) ,说明点 (u) 处被一条链经过,(f[2][u][i]=max(f[2][u][j]+f[0][v][i-j],f[1][u][j]+f[1][v][i-j+1]+w(u,v)))

    这样就完成了 DP 部分。显然,这样的时间复杂度是 (Omicron(nk)) ,因为还要枚举一个 (j) .

    那么现在就可以往上套 WQS二分 了。此时的函数值 (f(x)=max(f[0/1/2][rt][x])) .

    注意到,每增加一条链,有两种方式:

    1. 再找一条链
    2. 拆开一条链

    显然,每一次操作都是选择当前的最优解,新的操作不优于上一个,因此函数图像是上凸包。

    那么在 DP 外套上一个 WQS二分,去掉关于每个点选了几条链的次数限制,时间复杂度 (Omicron(n)) ,总时间复杂度 (Omicron(nlog k)) ,可以通过本题。

    洛谷 O2 第一页了w 然而 LOJ 上已经排不上号了

    实现的时候有一些细节:

    1. 合并两条链的时候要补上多减了一次的代价
    2. 最后要对“只选一个点”的情况取 (max) .
    3. DFS 开头注意清空,对于 (f[0][u]) 的情况是 (0) ,但是另外两个是负权。
    4. 记录选的链数挺麻烦的,建议直接写结构体重载
    //Author: RingweEH
    const int N=3e5+10,INF=1e9;
    struct Edge
    {
    	int to,nxt; ll val;
    }e[N<<1];
    struct Node
    {
    	ll x; int cnt;
    	Node ( ll _x=0,int _cnt=0 ) : x(_x),cnt(_cnt) {}
    	Node operator + ( const Node &tmp ) const { return Node(x+tmp.x,cnt+tmp.cnt); }
    	bool operator < ( const Node &tmp ) const { return ( x<tmp.x || (x==tmp.x && cnt<tmp.cnt) ); }
    	void clear() { x=-INF; cnt=-INF; }
    }f[N][3];
    int n,k,tot=0,head[N];
    
    void add( int u,int v,ll w )
    {
    	e[++tot].to=v; e[tot].nxt=head[u]; head[u]=tot; e[tot].val=w;
    }
    
    void dfs( int u,int fa,ll del )
    {
    	f[u][0]=Node(0,0); f[u][1].clear(); f[u][2].clear();
    	for ( int i=head[u]; i; i=e[i].nxt )
    	{
    		int v=e[i].to;
    		if ( v==fa ) continue;
    		dfs( v,u,del );
    		f[u][2]=max( f[u][2]+f[v][0],f[u][1]+f[v][1]+Node(e[i].val+del,-1) );
    		f[u][1]=max( f[u][0]+f[v][1]+Node(e[i].val,0),f[u][1]+f[v][0] );
    		f[u][0]=f[u][0]+f[v][0];
    	}
    	f[u][1]=max( f[u][1],f[u][0]+Node(-del,1) );
    	f[u][0]=max( f[u][0],max(f[u][1],f[u][2]) );
    }
    
    
    int main()
    {
    	n=read(); k=read(); k++;
    	for ( int i=1; i<n; i++ )
    	{
    		int u=read(),v=read(); ll w=read();
    		add( u,v,w ); add( v,u,w );
    	}
    	
    	ll l=-INF,r=INF,ans=0;
    	while ( l<=r )
    	{
    		ll mid=(l+r)>>1; dfs( 1,0,mid );
    		if ( f[1][0].cnt>=k ) l=mid+1,ans=f[1][0].x+k*mid;
    		else r=mid-1;
    	}
    	
    	printf( "%lld
    ",ans );
    	return 0;
    }
    
    

    What's More

    事实上,WQS二分是可以优化费用流的!没想到吧

    考虑这样一个经典问题:

    给定一个长度为 (n) 的序列 (a) ,要求超出恰好 (k) 个不相交的连续子序列,使得和最大。

    复杂度要求:(Omicron(nlog n))(n,k) 同级。

    事实上,这是个费用流模型。

    对于序列中每个点,拆分成两个点 (i,i') ,连一条 (i o i') ,流量为 1,费用 (a_i) 的边。

    对于每个 (i) ,连 (S o i) ,流量为 1,费用为 0 .

    对于每个 (i') ,连 (i' o T) ,流量为 1,费用为 0 .

    对于相邻点 (i,i+1) ,连 (i' o i+1) ,流量为 1,费用为 0.

    显然这样每次沿着最大费用路径单路增广一次,就相当于选择了原问题的一个最大连续子序列。

    增广 (k) 次就是答案,由于有反向边,所以不会出现区间相交的情况。

    然后就有两种方法:

    第一,数据结构优化。

    把模型放到原问题上,每次增广就是求全局的最大连续子序列和,然后取反。

    那么可以用线段树维护这个操作,复杂度 (Omicron(klog n)) .

    第二,考虑特殊性质。

    由于每次单路增广的是最长路,那么增广之后的网络显然是残余网络,每次得到的费用会比上一次少。

    也就是说,增广 (x) 次后的流量 (f(x)) 是个上凸包。

    事实上 (f(x)) 就是选了 (x) 个不相交的连续子序列的最大和。

    那么到这里就和上面的WQS二分重合了。

    后记

    其实难度主要是想到用这个东西,和 checker 里面的东西吧,正经 WQS二分并不难写。

    WQS学习文章/图源:wqs二分详解

    费用流部分来源于: wqs二分/dp凸优化

    大部分题目来源:wqs二分学习笔记

  • 相关阅读:
    Java中final、finally、finalize的区别
    GC垃圾回收机制详解
    spring ioc Di
    获取不同语言版本的任务状态
    转:系统架构师-基础到企业应用架构
    转:SharePoint【Site Definition 系列】
    转:SharePoint【ECMAScript对象模型系列】
    转:SharePoint【Ribbon系列】
    SharaPoint Farm Administrator密码变换及管理员转换
    转:Programming with Features(操作Feature)
  • 原文地址:https://www.cnblogs.com/UntitledCpp/p/WQSBinarySearch.html
Copyright © 2011-2022 走看看