关于模拟网络流

参考文献

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\n",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\n",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\n",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\n",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\),都是些小技巧。

posted @ 2019-11-03 21:06  敌敌畏58  阅读(507)  评论(0编辑  收藏  举报