zoukankan      html  css  js  c++  java
  • 关于模拟网络流

    参考文献

    https://blog.csdn.net/litble/article/details/88410435
    https://www.mina.moe/archives/11762

    模拟最大流

    题意

    其实是CF 724 E

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    这其实是师兄改编了题目QMQ,真实范围应该是(n<=10000)

    思路

    这道题目很明显可以用网络流来做:

    在这里插入图片描述

    但是范围直接T了。

    然后我们可以用贪心

    然后我们发现这个图貌似有神奇的性质,利用最大流=最小割,我们可以枚举最小割。

    最小割的性质就是每个点要么位于(st)集合或者(ed)集合,所以我们就可以先枚举出一种情况:
    在这里插入图片描述

    我们发现这个中间的边也要处理,所以对于(st,ed)我们还要处理一下,但是我们要如何处理最小割呢?

    我们设(f[i][j])表示的是到了第(i)个点,有(j)个归到(st)集合。(归到(st)集合就是割掉(ed)

    那么不计(C)的影响的话,不难列出:
    (f[i][j]=min(f[i-1][j-1]+b[i],f[i-1][j]+a[i]))

    但是如何处理(C)呢,不难发现对于(i,j(i<j))如果(i)选了(ed)(j)选了(st),那么就会有个(C)

    这个该归到(st)还是(ed)呢,很明显为了DP没有后效性,我们就归到(ed)吧。

    那么DP转移方程变成了:(f[i][j]=min(f[i-1][j-1]+b[i],f[i-1][j]+a[i]+j*C))

    就可以转移了,时间复杂度(O(n^2)),当然,这个又叫模拟网络流。

    #include<cstdio>
    #include<cstring>
    #define  N  2100
    using  namespace  std;
    typedef  long  long  LL;
    inline  LL  mymin(LL  x,LL  y){return  x<y?x:y;}
    LL  f[2][N],m;
    int  a[N],b[N],n;
    int  main()
    {
    //	freopen("c.in","r",stdin);
    //	freopen("c.out","w",stdout);
    	scanf("%d%lld",&n,&m);
    	for(int  i=1;i<=n;i++)scanf("%d",&a[i]);
    	for(int  i=1;i<=n;i++)scanf("%d",&b[i]);
    	int  pre=0,now=1;
    	for(int  i=1;i<=n;i++)
    	{
    		pre^=now^=pre^=now;
    		f[now][0]=f[pre][0]+a[i];
    		for(int  j=1;j<i;j++)f[now][j]=mymin(f[pre][j-1]+b[i],f[pre][j]+a[i]+j*m);
    		f[now][i]=f[pre][i-1]+b[i];
    	}
    	LL  ans=(LL)9999999999999999;
    	for(int  i=0;i<=n;i++)ans=mymin(ans,f[now][i]);
    	printf("%lld
    ",ans);
    	return  0;
    }
    

    我的思路

    我是采用贪心,这道题目(n)(2000),还开(2s),那么我们不难想到一种贪心思路,就是使得所有卖完粮食后剩余粮食大于(C)的位置的粮食尽量的平衡。

    也就是尽可能的榨干这个(C),使得后面每个位置都可以堆满(C)

    至于平衡的值,我们采用二分查找,那么时间复杂度(O(n^2log值域))

    而且还有优化,(1s)内过掉。

    #include<cstdio>
    #include<cstring>
    #include<queue>
    #define  N  3100
    using  namespace  std;
    typedef  long  long  LL;
    template  <class  T>
    inline  T  mymin(T  x,T  y){return  x<y?x:y;}
    int  a[N],b[N];
    priority_queue<int>q;//储存粮食信息,当然是大于C的才给放
    int  sta[N],top;//表示可以移动的粮食 
    int  list[N],tail;
    int  n,m;
    LL  zans;
    bool  check(int  x,int  k/*原本的数字*/)
    {
    	for(int  i=1;i<=top;i++)
    	{
    		if(sta[i]<=x)return  false;
    		k+=mymin(sta[i]-x,m);//可以给你多少的数字 
    		if(k>=x)return  true;
    	}
    	return  false;
    }
    void  work(int  x,int  y)
    {
    	top=0;
    	while(!q.empty())sta[++top]=q.top(),q.pop();
    	int  k=x-y;//可能是负数
    	if(top)//有大于C的粮食位置
    	{
    		int  l=k+1,r=mymin((LL)sta[1],(LL)k+(LL)top*(LL)m),mid,ans=k/*就是不变*/;//表示范围,而且能防止m过小时时间过大
    		while(l<=r)
    		{
    			mid=(l+r)/2;
    			if(check(mid,k)==true)ans=mid,l=mid+1;
    			else  r=mid-1;
    		}
    		for(int  i=1;i<=top;i++)
    		{
    			if(k==ans)break;
    			int  zjj=mymin(mymin(ans-k,m),sta[i]-ans);sta[i]-=zjj;k+=zjj;
    		}
    	}
    	if(k<0)
    	{
    		while(k<0  &&  tail)k+=list[tail--];
    	}
    	zans+=mymin(y,k+y);
    	if(k>0)
    	{
    		if(k<=m)list[++tail]=k;
    		else  q.push(k);
    	}
    	for(int  i=1;i<=top;i++)
    	{
    		if(sta[i]<=m)list[++tail]=sta[i];
    		else  q.push(sta[i]);
    	}
    }
    int  main()
    {
    //	freopen("c.in","r",stdin);
    //	freopen("c.out","w",stdout);
    	scanf("%d%d",&n,&m);
    	for(int  i=1;i<=n;i++)scanf("%d",&a[i]);
    	for(int  i=1;i<=n;i++)scanf("%d",&b[i]);
    	for(int  i=1;i<=n;i++)
    	{
    		work(a[i],b[i]);
    	}
    	printf("%lld
    ",zans);
    	return  0;
    }
    

    小结

    一般模拟最大流都是模拟最小割。

    模拟费用流

    普通模版

    题意

    题目链接

    思路

    我们很容易可以想出费用流的构图。

    但是这里我们要用贪心加堆模拟费用流。

    那么可以发现,这道题目表示的是(n)只兔子,有(m)个洞,洞有容量,允许左右走,要求每只兔子必须进洞(因为菜的数量和送菜员一样)。

    (x)为坐标,(s)为洞的附加权值。

    对于一只兔子(a)选了前面的一个洞(b),那么产生的费用为(x_a-x_b+s_b),而洞的最小值,可以从洞的小根堆直接取。

    那么如果后面的一个洞(c)抢了(a),那么就可以多出(s_c+x_c-x_a-(x_a-x_b+s_b)=s_c+x_c-2x_a+x_b-s_b),那么我们只需要添加一个兔子,权值为(-2x_a+x_b-s_b),然后我们对于每个洞,都可以取兔子的堆顶,如果贡献(<0),那么就可以选了,而如果选了我们构造出来的兔子,就相当于退流,不选洞(b)

    当然是不可能有兔子抢(b)的,因为这样结果不会是最优的。

    但是对于洞(b)选了兔子(a),那么造成的价值应该是(v_a+s_b+x_b)(v_a)(a)在堆中的值),那么如果后面的兔子(c)要抢(b)的话,那么增加贡献就是(-v_a-2*x_b),那么我们需要在洞堆添加这个数字。

    你又会问了,那一个兔子的洞被抢了,而洞的兔子又被抢了,不就剩了一对兔子和洞了吗,但是如果你仔细的观察式子就会发现,其实这样子抢的话,这一对的价值是会算上的(其实就是把换的增值去掉了)。

    而如果洞(b)选了兔子(a),而(c)洞抢了(a)呢?那么很明显设一个兔子权值为(-s_b-x_b)就行了,而且这个贪心的正确性在于一个兔子一定进了一个洞,就也就是不管后面怎么抢选他的洞,他都可以回原来的洞。

    不过其实如果兔子不一定要求进洞的话,不给他搞洞其实也是可以的,我们考虑为什么可能会错呢?就是我们担心洞找兔子后,添加的洞和兔子的后悔操作同时被触发,但是其实是不会的(而且如果兔子不是必须进洞,你也可以认为兔子进了自己脚下的洞,就是不选)。

    因为后面的兔子和洞匹配怕不是更优QMQ。

    但是讲了这么多兔子一定要进洞怎么搞,我们只要设无限远的地方有一些洞就行了。

    同时多个洞就可以用(pair)记录有多少个洞。

    而且因为兔子就(n)个,所以堆的个数就是(n)个,(O(nlogn))

    代码来自https://blog.csdn.net/litble/article/details/88410435。

    #include<bits/stdc++.h>
    using namespace std;
    #define RI register int
    int read() {
    	int q=0;char ch=' ';
    	while(ch<'0'||ch>'9') ch=getchar();
    	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    	return q;
    }
    typedef long long LL;
    typedef pair<LL,LL> PR;
    const LL inf=1e15;
    const int N=100005;
    int n,m;LL ans,sumc;
    struct node{LL x,w,c;}t[N<<1];
    bool cmp(node A,node B) {return A.x<B.x;}
    priority_queue<PR,vector<PR>,greater<PR> > q1,q2;
    
    void work() {
    	q1.push((PR){inf,n});
    	for(RI i=1;i<=n+m;++i) {
    		if(t[i].w==-1) {
    			PR kl=q1.top();LL kv=kl.first;q1.pop();
    			ans+=kv+t[i].x,--kl.second;
    			if(kl.second) q1.push(kl);
    			q2.push((PR){-kv-2*t[i].x,1});//后面可以换洞
    		}
    		else {
    			LL num=t[i].c,sumsum=0;
    			while(num&&(!q2.empty())) {
    				PR kl=q2.top();LL kv=kl.first;
    				if(kv+t[i].w+t[i].x>=0) break;
    				q2.pop();LL sum=min(kl.second,num);
    				ans+=(kv+t[i].w+t[i].x)*sum,kl.second-=sum,num-=sum;
    				if(kl.second) q2.push(kl);
    				sumsum+=sum,q1.push((PR){-kv-2*t[i].x,sum});//换兔子
    			}
    			if(sumsum) q2.push((PR){-t[i].x-t[i].w,sumsum});//换洞
    			if(num) q1.push((PR){t[i].w-t[i].x,num});
    		}
    	}
    }
    int main()
    {
    	n=read(),m=read();
    	for(RI i=1;i<=n;++i) t[i].x=read(),t[i].w=-1,t[i].c=0;
    	for(RI i=1;i<=m;++i)
    		t[i+n].x=read(),t[i+n].w=read(),t[i+n].c=read(),sumc+=t[i+n].c;
    	if(sumc<(LL)n) {puts("-1");return 0;}
    	sort(t+1,t+1+n+m,cmp),work();
    	printf("%lld
    ",ans);
    	return 0;
    }
    
    

    树上模拟

    题目链接

    这道题目的话,我们需要思考一个事情,就是坐标到底要怎么搞?

    那么我们可以给每个点设(dis_x),表示第(x)个点到根的长度。

    那么坐标就不是:(dis_x-dis_y),而是(dis_x+dis_y-2*dis_{lca(x,y)})了。

    所以我们考虑在树上从下往上搞。

    堆用左偏树。

    对于(x),他找到子树(y),那么他就可以把自己目前的子树和他对比,即两方的兔子和洞合并,由于贪心局部最优,所以这个子树内的后悔操作已经处理好了。

    而两方的兔子洞合并,肯定是各取堆顶是最好的啦,而且有个很显然的事情,就是对于后悔出来的洞和堆,后悔后的兔子和洞肯定也是一个(x)一个(y),不然从贪心策略讲,(d)变小了,所以减的值变小,那么如果是来自同个集合((x)(y))的配对,肯定会比原来更大,那么就肯定不会出现这种情况,所以我们只需要对于(x)的兔子,直接合并(y)的洞,反之也是。

    而且(lca)肯定是(x),所以先对于(x,y)处理一下洞和兔子,然后再合并,找下一棵子树就行了。

    不过这里是洞要求必须进,怎么办,那么我们就把他的权值减上一个(-inf),要求最优的就一定要进洞吗,然后结果把(-inf)去掉。

    你好不好奇为什么这里兔子可以直接添加成一个点,你可以认为是他们进了脚下的洞。

    而且对于树上。

    rtX[x]=merge(rtX[x],newnode(-v2+2*d,sum));
    rtY[y]=merge(rtY[y],newnode(-v1+2*d,sum));
    

    这两句话也是一定不会同时触发的,为什么?

    因为如果有一个洞和一个兔子分别和这两个洞和兔子配对,那么一定都经过了(x),重复,那还不如那两个匹配,这两个原本的匹配在一起(即使最坏也是和原情况相等)。

    那么关键是相等怎么判断呢?

    (x,y)配对后,(a)(x)(b)(y)

    我们不难发现,相等的情况有两个:

    1. (a)(lca(x,y))的祖先,(b)(lca(x,y))子树内的节点。
    2. (a,b)都为子树内的节点。

    在这里插入图片描述

    那么我们就看(b)(b)抢了(y)后,在(a)选时,相当于问你,是((x,y),(a,b))这样更优,还是((b,y),(a,x)-dis_x+2d),而(2d>-dis_x+2d>0),所以贪心会自动选择((x,y),(a,b)),所以我们不用担心。

    代码来自https://blog.csdn.net/litble/article/details/88410435

    #include<bits/stdc++.h>
    using namespace std;
    #define RI register int
    int read() {
    	int q=0;char ch=' ';
    	while(ch<'0'||ch>'9') ch=getchar();
    	while(ch>='0'&&ch<='9') q=q*10+ch-'0',ch=getchar();
    	return q;
    }
    typedef long long LL;
    typedef pair<LL,int> PR;
    const int N=250005;
    const LL inf=1e12;
    int n,tot,SZ;LL ans;
    int h[N],ne[N<<1],to[N<<1],X[N],Y[N],rtX[N],rtY[N];LL dis[N],w[N<<1];
    struct node{int ls,rs,d;PR v;}tr[N*30];
    //申请超大结构体要花很多时间,建议拆成单个数组。
    
    int merge(int a,int b) {
    	if(!a||!b) return a|b;
    	if(tr[a].v>tr[b].v) swap(a,b);
    	tr[a].rs=merge(tr[a].rs,b);
    	if(tr[tr[a].rs].d>tr[tr[a].ls].d) swap(tr[a].ls,tr[a].rs);
    	tr[a].d=tr[tr[a].rs].d+1;return a;
    }
    int newnode(LL val,int sum)
    	{++SZ,tr[SZ].v=(PR){val,sum},tr[SZ].d=1;return SZ;}
    
    void add(int x,int y,LL z) {to[++tot]=y,ne[tot]=h[x],h[x]=tot,w[tot]=z;}
    void mer(int x,int y,LL d) {
    	while(rtX[x]&&rtY[y]) {
    		LL v1=tr[rtX[x]].v.first,v2=tr[rtY[y]].v.first;
    		if(v1+v2-2*d>=0) break;
    		int sum=min(tr[rtX[x]].v.second,tr[rtY[y]].v.second);
    		ans+=(v1+v2-2*d)*sum;
    		tr[rtX[x]].v.second-=sum,tr[rtY[y]].v.second-=sum;
    		if(!tr[rtX[x]].v.second) rtX[x]=merge(tr[rtX[x]].ls,tr[rtX[x]].rs);
    		if(!tr[rtY[y]].v.second) rtY[y]=merge(tr[rtY[y]].ls,tr[rtY[y]].rs);
    		rtX[x]=merge(rtX[x],newnode(-v2+2*d,sum));
    		rtY[y]=merge(rtY[y],newnode(-v1+2*d,sum));
    	}
    }
    void dfs(int x,int las) {
    	if(X[x]) rtX[x]=newnode(dis[x],X[x]);
    	if(Y[x]) rtY[x]=newnode(dis[x]-inf,Y[x]),ans+=1LL*Y[x]*inf;
    	for(RI i=h[x];i;i=ne[i]) {
    		int y=to[i];if(y==las) continue;
    		dis[y]=dis[x]+w[i],dfs(y,x);
    		mer(x,y,dis[x]),mer(y,x,dis[x]);
    		rtX[x]=merge(rtX[x],rtX[y]),rtY[x]=merge(rtY[x],rtY[y]);
    	}
    }
    int main()
    {
    	int x,y,z;
    	n=read();
    	for(RI i=1;i<n;++i)
    		x=read(),y=read(),z=read(),add(x,y,z),add(y,x,z);
    	for(RI i=1;i<=n;++i) {
    		X[i]=read(),Y[i]=read();
    		int kl=min(X[i],Y[i]);
    		X[i]-=kl,Y[i]-=kl;
    	}
    	dfs(1,0);
    	printf("%lld
    ",ans);
    	return 0;
    }
    

    时间复杂度的证明

    非树证明

    这里我们设计到了如果对于洞和兔子都有分身,那么复杂度是多少?

    我个人认为是(O(nlogn))带一些常数的,分析是基于算法的分析。

    对于兔子。

    他找到了一个洞,如果他目前的容量小于这个洞的话,那么这个洞被拆成两个部分,然后(break),而对于小于等于,则是把这个洞和兔子匹配,然后生成了一个新的洞,表示抢兔子,而抢洞的则是在最后添加。

    如果发现权值大于(0),则直接拿这些兔子做一个点插入堆。

    观察整个过程,貌似对于一个兔子产生的点的个数级是(O(1))的。

    而对于洞,也可以这样分析。

    所以就是(O(nlogn))

    树上

    这里设计到了合并。

    就不是一个个合并了。

    我们看一下对于兔子和洞的合并,如果从两个堆中取了兔子和动,并且塞了后悔操作回去,这个后悔操作会不会在本次合并中再次被采用呢?

    如果其中一个后悔操作是和原堆中的节点(x)合并,那么为什么在配对的时候堆顶不是(x)

    所以我们只需要考虑为什么不是一个后悔操作对应一个后悔操作(而且肯定不是原来的一对后悔操作,肯定是不同配对的后悔操作):

    在这里插入图片描述
    那么为什么(a,x)在配对的时候堆顶会是(b,y)呢?这点需要考虑一下QMQ。

    所以每次只会匹配兔子和洞堆的大小的(min)

    而且,对于每次堆顶合并,最多会生成三个点。(多了一个点)而真正会参与后面操作的则是两个点,所以我们多出来的点就是合并次数。

    那么这个(min)要怎么搞呢?

    对于两个点数为(x,y(x<=y))的集合,那么他们兔子堆和洞堆中的可以操作的点是(O(n))的。


    那么就是(a+b=x,c+d=y),求(min(a,c)+min(b,d))的最大值。

    (min(a,c)+min(x-a,y-c))

    (a<=c,x-a<=y-c)时,那么就是(x)

    (a<=c,x-a>=y-c)时,那么合并次数(<=x)

    (a>=c,x-a<=y-c)时,那么合并次数(<=x)

    所以就是(x)


    由于集合合并只会记录最小的集合,那么最坏情况就是两个集合大小相等。

    那么就是一个完全二叉树的情况。

    而这种情况就是多产生了(nlogn)个节点,而还要计算左偏树,所以就是(O(nlog^2n))

    不知道是不是对的QAQ。

    小结

    模拟费用流一般是模拟退流操作

    而且对于兔子和洞选择的必要性可以添加点,如某些兔子可以不选,那么就添加一个权值为(0)的洞,选了这些洞就是不选,而如果必须选,就添加(inf),洞必须进,就把权值(-inf),都是些小技巧。

  • 相关阅读:
    机器学习理论决策树理论第二卷
    机器学习理论决策树算法第一卷
    win7 下安装 Ubuntu16.04以及Centos6.5 双系统小结
    mapreduce学习工程之五---map端join连接
    org.apache.hadoop.ipc.Client: Retrying connect to server异常的解决
    ubuntu server命令换源
    linux下的软链接和硬链接
    linux下 mysql完全卸载
    Java学习路线
    git练习题
  • 原文地址:https://www.cnblogs.com/zhangjianjunab/p/11789135.html
Copyright © 2011-2022 走看看