zoukankan      html  css  js  c++  java
  • 网络流学习笔记——难题

    这里是网络流难题集合。

    VI.[NOI2009]植物大战僵尸

    一眼看出拓扑排序。因为对于每个点\(i\),只有所有保护着\(i\)和在\(i\)右边的植物全挂掉之后,植物\(i\)才能够被攻击。这样只要建出图来,在上面拓扑排序,对每个排序到的点统计权值和即可。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,head[610],val[610],cnt,in[610],res;
    struct node{
    	int to,next;
    }edge[400100];
    void ae(int u,int v){
    	edge[cnt].next=head[u],edge[cnt].to=v,head[u]=cnt++;
    }
    queue<int>q;
    int main(){
    	scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
    	for(int i=0,t1,t2,t3;i<n*m;i++){
    		scanf("%d%d",&val[i],&t1);
    		while(t1--){
    			scanf("%d%d",&t2,&t3);
    			ae(i,t2*m+t3),in[t2*m+t3]++;
    		}
    	}
    	for(int i=0;i<n;i++)for(int j=1;j<m;j++)ae(i*m+j,i*m+j-1),in[i*m+j-1]++;
    	for(int i=0;i<n*m;i++)if(!in[i])q.push(i);
    	while(!q.empty()){
    		int x=q.front();q.pop();
    		res+=val[x];
    		for(int i=head[x];i!=-1;i=edge[i].next){
    			in[edge[i].to]--;
    			if(!in[edge[i].to])q.push(edge[i].to);
    		}
    	}
    	printf("%d\n",res);
    } 
    

    但是,如果你把它用本题的样例跑一下的话,你会发现,结果跑出来是\(15\)而不是答案\(25\)!!!

    为什么呢?

    看一下样例。我们发现里面存在负权点

    负权点就意味着,贪心地吃掉每一个能吃到的植物并不是最优的。我们仍需要权衡是放弃这个点还是吃掉它。

    咋办呢?

    这时候,就是网络流的登场。

    我们引出闭合子图概念。

    闭合子图是这样一个\(G(V,E)\),使得:

    如果点\(x \in V\),那么对于所有的边\((x,y)\),都有\((x,y)\in E\)\(y\in V\)

    换句话说,如果一个点\(x\)在子图里,那么从\(x\)出发爆搜,所有到达得了的点和边都在这个子图里。

    这时候,我们回过来看一下这道题,就会发现,如果我们建反边,即一个点向保护着该点的所有点连边,那么,一个正确的解法,必定是一张闭合子图。(不然就有点被吃了,但是保护着它的点中还有活着的,违背了题意)。

    显然,我们要求一个最大权闭合子图(字面意思)。

    如何建图呢?我们首先仍然要跑拓扑排序,只保留拓扑排序能够排序到的节点。剩余的部分出现了环,是不能被吃掉的。

    然后,开始建图。

    1.对于原图中的边\((x,y)\),在新图中连一条边\((x,y,INF)\)

    2.对于原图中的点\(x\),如果有\(val_x > 0\),则连一条边\((S,x,val_x)\);如果有\(val_x < 0\),则连一条边\((x,T,-val_x)\);如果有\(val_x = 0\),两条边中随便选一条连。

    最后答案即为(所有正权点的权值和-最小割)。

    证明:

    令集合\(\mathbb{S}\)为最小割意义下\(S\)所能到达的所有点,即为最终我们要攻击的所有点。因为原图中的所有边边权都是\(INF\),我们只能割断新加入的边。

    则如果一个点\(x \in \mathbb{S}\),那么对于所有\(x\)能到达的点\(y\),都有\(y \in \mathbb{S}\),因为原图中的边不会被截断。显然,如果一个点\(x\)已经在\(\mathbb{S}\)中,那么边\((S,x)\)一定不会被割断(不割的流量比割了小)。

    则我们证明了一个割方案下的\(\mathbb{S}\)一定是一张闭合子图。

    那为什么最小割就对应着最大权呢?

    因为如果一个正点被割了,就意味着我们不选这个点,要从正权点的权值和中减除\(val_x\);一个负权点被割了,就意为着\(x\in \mathbb{S}\),因此才要在汇点处把它割掉。所以我们要将正权点的权值和中加上\(val_x\),即减掉\(-val_x\),就是\((x,T)\)的边权。

    所以最小割,就是放弃最少的正点,选择最少的负点。

    然后就OK了。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,head[610],val[610],cnt,in[610],dep[610],cur[610],res,S,T,sum;
    struct node{
    	int to,next,val;
    }edge[400100];
    void ae(int u,int v,int w){
    	edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    	edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    }
    bool vis[610];
    queue<int>q;
    inline bool bfs(){
    	memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
    	while(!q.empty()){
    		register int x=q.front();q.pop();
    		if(!vis[x])continue;
    		for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    	}
    	return dep[T]>0;
    }
    bool reach;
    inline int dfs(int x,int flow){
    	if(x==T){
    		res+=flow;
    		reach=true;
    		return flow;
    	}
    	int used=0;
    	for(register int &i=cur[x];i!=-1;i=edge[i].next){
    		if(!edge[i].val||dep[edge[i].to]!=dep[x]+1||!vis[edge[i].to])continue;
    		register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    		if(ff){
    			edge[i].val-=ff;
    			edge[i^1].val+=ff;
    			used+=ff;
    			if(used==flow)break;
    		}
    	}
    	return used;
    }
    inline void Dinic(){
    	while(bfs()){
    		reach=true;
    		while(reach)reach=false,dfs(S,0x3f3f3f3f);
    	}	
    }
    int main(){
    	scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=n*m,T=n*m+1,vis[S]=vis[T]=true;
    	for(int i=0,t1,t2,t3;i<n*m;i++){
    		scanf("%d%d",&val[i],&t1);
    		while(t1--){
    			scanf("%d%d",&t2,&t3);
    			ae(t2*m+t3,i,0x3f3f3f3f),in[t2*m+t3]++;
    		}
    	}
    	for(int i=0;i<n;i++)for(int j=1;j<m;j++)ae(i*m+j-1,i*m+j,0x3f3f3f3f),in[i*m+j-1]++;
    	for(int i=0;i<n*m;i++)if(!in[i])q.push(i);
    	while(!q.empty()){
    		int x=q.front();q.pop();
    		vis[x]=true;
    		for(int i=head[x];i!=-1;i=edge[i].next){
    			if(edge[i].val)continue;
    			in[edge[i].to]--;
    			if(!in[edge[i].to])q.push(edge[i].to);
    		}
    		if(val[x]>=0)ae(S,x,val[x]),sum+=val[x];
    		else ae(x,T,-val[x]);
    	}
    	Dinic();
    	printf("%d\n",sum-res);
    } 
    

    XXII.最长k可重区间集问题

    这个建图比较神仙orz...

    一上来默认需要离散化。设离散化后共有\(lim\)个位置。然后呢?

    这里我们这样建图:

    1.对于每个位置\(i\),连一条边\((i,i+1,k,0)\)

    2.连边\((S,1,k,0)\)\((lim,T,k,0)\)

    3.对于每一条从\(l\)\(r\),长度为\(len\)的线段,连一条边\((l,r,1,len)\)

    答案即为最大费用。

    为什么呢?

    让我们看看一张典型的图:

    水流从\(S\)出发,流到了\(1\)

    \(1\)处,每有一股水流离开主干道,就能获得对应的收益。但是,直到这股水流重新归队,这一点流量是回不来的

    例如,如果有一股水流流入了路径\((1,3)\),那么,流经\(2\)的流量就会少\(1\)。但是,这股水流对\(3\)的流量并无影响,毕竟是开线段,在端点处没有影响。

    因此,我们就能看出这种建图的正确性。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,k,l[1010],r[1010],len[1010],cnt,head[1010],id[1010],fr[1010],dis[1010],lim,S,T,cost;
    vector<int>v;
    struct node{
    	int to,next,val,cost;
    }edge[101000];
    void ae(int u,int v,int w,int c){
    	edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    	edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    }
    queue<int>q;
    bool in[1100];
    bool SPFA(){
    	memset(dis,-1,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
    	while(!q.empty()){
    		int x=q.front();q.pop(),in[x]=false;
    		for(int i=head[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val)continue;
    			if(dis[edge[i].to]<dis[x]+edge[i].cost){
    				dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
    				if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
    			}
    		}
    	}
    	if(dis[T]==-1)return false;
    	int x=T,mn=0x3f3f3f3f;
    	while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
    	cost+=mn*dis[T],x=T;
    	while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
    	return true;
    }
    int main(){
    	scanf("%d%d",&n,&k),memset(head,-1,sizeof(head));
    	for(int i=1;i<=n;i++)scanf("%d%d",&l[i],&r[i]),len[i]=r[i]-l[i],v.push_back(l[i]),v.push_back(r[i]);
    	sort(v.begin(),v.end()),v.resize(unique(v.begin(),v.end())-v.begin()),lim=v.size(),S=lim+1,T=lim+2;
    	for(int i=1;i<lim;i++)ae(i,i+1,k,0);
    	ae(S,1,k,0),ae(lim,T,k,0);
    	for(int i=1;i<=n;i++){
    		if(l[i]>r[i])swap(l[i],r[i]);
    		l[i]=lower_bound(v.begin(),v.end(),l[i])-v.begin()+1;
    		r[i]=lower_bound(v.begin(),v.end(),r[i])-v.begin()+1;
    		ae(l[i],r[i],1,len[i]);
    	}
    	while(SPFA());
    	printf("%d\n",cost); 
    	return 0;
    }
    

    XXX.[CQOI2016]不同的最小割

    这里介绍一种新的神奇玩意儿:最小割树

    首先,这题不是让你跑\(n^2\)个网络流,绝对不是。

    我们观察得,一组最小割必定将原图分割成两个连通块,而在一棵树上删去一条边也会将这棵树分成两个连通块。既然最短路有最短路径树,我们是否能建出一棵最小割树来呢?

    我们先任选两个点,假设是\(1\)\(n\),当作源点和汇点。然后,我们跑出最小割,在另一张新图上连边\((1,n,cut)\)

    然后,原图肯定被分割成两部分。我们在两个部分内递归着跑最小割,直到某部分内只剩一个节点。

    如果看不懂,没关系,我们上图!

    初始时,所有节点都在同一个集合里。

    第一步,我们选择\(S=1\)\(T=6\)。跑得最小割为边\((1,6),(1,5),(3,5)\),值为\(10\)。之后它分为两个集合\((1,3,4)\)\((2,5,6)\)

    第二步,我们在集合\((1,3,4)\)内跑网络流。选择\(S=1,T=4\)。跑出最小割为\(6\),我们选择割边\((3,6)\)。集合\((1,3,4)\)被分成集合\((1,3)\)与集合\((4)\)

    第三步,我们在集合\((1,3)\)中选择\((S=1,T=3)\),跑出最小割为\(6\),割边\((1,3),(3,5)\)

    之后我们在集合\((2,5,6)\)中做相同操作。

    最终得到这样的图:

    那这最小割树有什么性质呢?

    1.它一定是一棵树。

    不要笑,我们还没有证明它是一颗树呢!万一在某一步时,我们跑出了一个最小割,却发现所有割边都在集合外怎么办?

    这种情况不可能发生。假设我们删去了该集合外的所有边,但这个集合一定仍然联通,我们一定还要至少删去集合内部的一条边,将这个集合真正地分成两个集合。

    2.原图中任意两点\(x,y\)的最小割,是最小割树中对应节点之间路径上的最小权值。

    因为路径上任意一条边,将它割断一定是这两点的一组割。则最小割即为两点间最小的权值。

    则本题的答案就呼之欲出了:就是最小割树上不同权值的种数。

    另外,注意是无向边,在建图时正向反向边都有权值。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,head[1010],cnt,dep[1010],cur[1010],ord[1010],S,T,res,pos[1010];
    struct node{
    	int to,next,val,ini;
    }edge[400100];
    void ae(int u,int v,int w){
    //	printf("%d %d %d\n",u,v,w);
    	edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=edge[cnt].ini=w,head[u]=cnt++;
    	edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=edge[cnt].ini=w,head[v]=cnt++;
    }
    queue<int>q;
    inline bool bfs(){
    	memset(dep,0,sizeof(dep));
    	q.push(S),dep[S]=1;
    	while(!q.empty()){
    		register int x=q.front();q.pop();
    		for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    	}
    	return dep[T]>0;
    }
    bool reach;
    inline int dfs(int x,int flow){
    	if(x==T){
    		res+=flow;
    		reach=true;
    		return flow;
    	}
    	int used=0;
    	for(register int &i=cur[x];i!=-1;i=edge[i].next){
    		if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
    		register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    		if(ff){
    			edge[i].val-=ff;
    			edge[i^1].val+=ff;
    			used+=ff;
    			if(used==flow)break;
    		}
    	}
    	return used;
    }
    inline void Dinic(){
    	while(bfs()){
    		reach=true;
    		while(reach)reach=false,dfs(S,0x3f3f3f3f);
    	}	
    }
    inline void initialize(){
    	for(int i=0;i<cnt;i++)edge[i].val=edge[i].ini; 
    }
    set<int>s;
    bool cmp(int x,int y){
    	return dep[x]<dep[y];
    }
    void work(int l,int r){
    	if(r<=l)return;
    //	printf("%d %d\n",l,r);
    	S=ord[l],T=ord[r];
    	res=0;
    	Dinic(),s.insert(res),initialize();
    	sort(ord+l,ord+r+1,cmp);
    //	for(int i=l;i<=r;i++)printf("%d ",dep[ord[i]]);puts("");
    	int mid=upper_bound(ord+l,ord+r+1,0,cmp)-ord;
    //	printf("%d\n",mid);
    	work(l,mid-1),work(mid,r);
    }
    int main(){
    	scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
    	for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),ae(x,y,z);
    	for(int i=1;i<=n;i++)ord[i]=i;
    	work(1,n);
    	printf("%d\n",s.size());
    //	for(set<int>::iterator it=s.begin();it!=s.end();it++)printf("%d\n",*it);
    	return 0;
    }
    

    XXXVI.[NOI2012]美食节

    我要举报,这里有人虐菜

    真·菜

    一眼看去,这很明显是[SCOI2007]修车的增强版。按照那题的思路,我果断敲了一发:

    #pragma GCC optimize(3)
    #include<bits/stdc++.h>
    using namespace std;
    int n,m,head[80100],cnt,dis[80100],fr[80100],id[80100],S,T,cost,p[50],s;
    struct node{
    	int to,next,val,cost;
    }edge[10001000];
    inline void ae(int u,int v,int w,int c){
    	edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    	edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    }
    queue<int>q;
    bool in[80100];
    inline bool SPFA(){
    	memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
    	while(!q.empty()){
    		register int x=q.front();q.pop(),in[x]=false;
    //		printf("%d\n",x);
    		for(register int i=head[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val)continue;
    			if(dis[edge[i].to]>dis[x]+edge[i].cost){
    				dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
    				if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
    			}
    		}
    	}
    	if(dis[T]==dis[0])return false;
    	register int x=T,mn=0x3f3f3f3f;
    	while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
    	cost+=dis[T]*mn,x=T;
    	while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
    	return true;
    }	
    int main(){
    	scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
    	for(register int i=1;i<=n;i++)scanf("%d",&p[i]),s+=p[i];
    	S=s*m+n+1,T=s*m+n+2;
    	for(register int i=1;i<=n;i++)ae(S,s*m+i,p[i],0);
    	for(register int i=1;i<=n;i++)for(register int j=1,x;j<=m;j++){
    		scanf("%d",&x);
    		for(register int k=1;k<=s;k++)ae(s*m+i,s*(j-1)+k,1,x*k);
    	}
    	for(register int i=1;i<=m;i++)for(register int j=1;j<=s;j++)ae((i-1)*s+j,T,1,0);
    	while(SPFA());
    	printf("%d\n",cost);
    	return 0;
    }
    

    结果光荣地T掉了。吸臭氧都没用。

    看了题解

    首先,如果在残量网络上添加新边的话,是可以在之前残量的基础之上不加修改地继续跑的。因此,我们就想着不一次性把所有点全加完,万一有些点从头到尾都没有被用到过怎么办?

    因为这道题的建模是分层的,第一层是源点,第二层全是菜(比如我),第三层全是虐菜的,最后一层是汇点,而SPFA费用流的特殊性,就在于它一次只能找到一条增广路。因此,每跑一边SPFA,就意为着将一个菜和一个虐菜的绑在了一起。

    考虑一个模型。首先,对于每道菜,我们不如让最快的人去虐它(尽管这是错的,但我们先不管它)。这样,只有当一个人虐完了一道菜,他才有可能去虐下一道菜(好像有些不对劲)。

    这样,当我们找出一条增广路后,增广路所涉及到的那个虐菜的就可以解锁下一道菜。当然咯,其它菜也有可能被虐的更好(丧 心 病 狂),因此我们要让所有菜都有一个被虐的机会(那菜还怎么活)。

    总结一下:

    1.在初始时,每一个人的首个虐菜位都是开放的:即,所有的菜连到所有第一时刻的人。同时,所有第一时刻的人连到汇点,源点连到所有菜。

    2.当跑出一条增广路后,这个人开放下一个虐菜位,所有的菜连到这个虐菜位,同时这个虐菜位连到汇点。

    当什么时候再也找不到新的增广路后,算法结束。

    代码:

    #pragma GCC optimize(3)
    #include<bits/stdc++.h>
    using namespace std;
    int n,m,head[80100],cnt,dis[80100],fr[80100],id[80100],S,T,cost,p[50],s,tim[50][110];
    struct node{
    	int to,next,val,cost;
    }edge[10001000];
    inline void ae(int u,int v,int w,int c){
    	edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    	edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    }
    queue<int>q;
    bool in[80100];
    inline bool SPFA(){
    	memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
    	while(!q.empty()){
    		register int x=q.front();q.pop(),in[x]=false;
    //		printf("%d\n",x);
    		for(register int i=head[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val)continue;
    			if(dis[edge[i].to]>dis[x]+edge[i].cost){
    				dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
    				if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
    			}
    		}
    	}
    	if(dis[T]==0x3f3f3f3f)return false;
    	register int x=T,mn=0x3f3f3f3f;
    	while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
    	cost+=dis[T]*mn,x=T;
    	while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
    	return true;
    }	
    int main(){
    	scanf("%d%d",&n,&m),memset(head,-1,sizeof(head));
    	for(register int i=0;i<n;i++)scanf("%d",&p[i]),s+=p[i];
    	S=s*m+n+1,T=s*m+n+2;
    	for(register int i=0;i<n;i++)ae(S,s*m+i,p[i],0);
    	for(register int i=0;i<n;i++)for(register int j=0;j<m;j++)scanf("%d",&tim[i][j]),ae(s*m+i,s*j,1,tim[i][j]);
    	for(register int i=0;i<m;i++)ae(i*s,T,1,0);
    	while(SPFA()){
    		int x=fr[T]+1;
    		ae(x,T,1,0);
    		for(int i=0;i<n;i++)ae(s*m+i,x,1,tim[i][x/s]*(x%s+1));
    	}
    	printf("%d\n",cost);
    	return 0;
    }
    

    XXXIX.[CQOI2012]交换棋子

    魔鬼题orz……

    首先这题我一点进去就懵了:这么个鬼畜的交换,怎么建模?我是一点思路也没有我太菜了

    题解的做法就很骇人了:

    首先,我们把一个点拆成\(3\)个点\(left,mid,right\)

    What?三个点?为什么?

    首先,我们观察一条移动路径就会发现,路径两端的格子都只移动了一次,但是路径中间的格子都移动了两次!再加上移入棋子和移出棋子的区别,只拆两个点肯定是不可以的不信您可以试试

    决定了拆点,我们就会发现这个方法可以非常轻松地解决这两个问题。

    移入棋子,我们统统都从\(left\)入;移出棋子,我们统统都从\(right\)出(和普通的拆点一样);统计答案,我们从\(mid\)统计。

    这样子,我们只需要限制边\((left,mid)\)\((mid,right)\)的流量,就能够达到区分路径两端和路径中间格子的目的(路径两边的格子,两条边的流量都被占去了;路径中间的格子,只有一条边的流量被占去了)。

    设始图为\(s\),终图为\(t\),格子最多交换次数为\(val\),那么:

    如果\(s_{i,j}='0'\),连边\((S,mid_{i,j},1,0)\),表示从点\((i,j)\)有一枚\('0'\)棋子出发了,目标是星辰大海

    如果\(t_{i,j}='0'\),连边\((mid_{i,j},T,1,0)\),表示点\((i,j)\)可以是某颗\('0'\)棋子的终点。

    如果有\(s_{i,j}=t_{i,j}\),则连边\((left_{i,j},mid_{i,j},val/2,0),(mid_{i,j},right_{i,j},val/2,0)\)。因为格子\((i,j)\)收支平衡了,所以在一组合法的方案中,这个格子流入的棋子肯定同流出的棋子相同,流量直接下取整(可能多余的那一点流量直接扔掉)。

    否则,如果\(s_{i,j}='0'\),则说明这个点移出的棋子比移入的棋子要多\(1\),故连\((left_{i,j},mid_{i,j},val/2,0),(mid_{i,j},right_{i,j},(val+1)/2,0)\)

    否则,即\(t_{i,j}='0'\),连边\((left_{i,j},mid_{i,j},(val+1)/2,0),(mid_{i,j},right_{i,j},val/2,0)\)

    然后,对于每一个八连通的点对,连边\((right,left,INF,1)\)。这条边可以被调用无限多次(但是点不可以),并且每调用一次就相当于带来一点费用。

    则答案即为最小费用。

    (如果始图中\('0'\)的数量和终图中\('0'\)的数量不等,则无解;如果跑出来最大流与始图中\('0'\)的数量不等,也无解)

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    #define left(x,y) (x)*m+(y)
    #define mid(x,y) (x)*m+(y)+n*m
    #define right(x,y) (x)*m+(y)+2*n*m
    int n,m,dx[8]={-1,0,1,1,1,0,-1,-1},dy[8]={1,1,1,0,-1,-1,-1,0},s1,s2;
    namespace MCMF{
    	const int N=2000,M=2000000;
    	int head[N],cnt,dis[N],fr[N],id[N],S,T,cost,flow;
    	struct node{
    		int to,next,val,cost;
    	}edge[M];
    	void ae(int u,int v,int w,int c){
    	//	printf("%d %d %d %d\n",u,v,w,c);
    		edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	bool in[N];
    	bool SPFA(){
    		memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
    		while(!q.empty()){
    			int x=q.front();q.pop(),in[x]=false;
    	//		printf("%d\n",x);
    			for(int i=head[x];i!=-1;i=edge[i].next){
    				if(!edge[i].val)continue;
    				if(dis[edge[i].to]>dis[x]+edge[i].cost){
    					dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
    					if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
    				}
    			}
    		}
    		if(dis[T]==0x3f3f3f3f)return false;
    		int x=T,mn=0x3f3f3f3f;
    		while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
    		flow+=mn,cost+=dis[T]*mn,x=T;
    		while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
    		return true;
    	}
    }
    using namespace MCMF;
    char s[30][30],t[30][30],c[30][30];
    bool ok(int x,int y){
    	return (x>=0&&x<n&&y>=0&&y<m);
    }
    int main(){
    	scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=3*n*m+1,T=3*n*m+2;
    	for(int i=0;i<n;i++)scanf("%s",s[i]);
    	for(int i=0;i<n;i++)scanf("%s",t[i]);
    	for(int i=0;i<n;i++)scanf("%s",c[i]);
    	for(int i=0;i<n;i++)for(int j=0;j<m;j++){
    		s1+=(s[i][j]=='0'),s2+=(t[i][j]=='0');
    		if(s[i][j]=='0')ae(S,mid(i,j),1,0);
    		if(t[i][j]=='0')ae(mid(i,j),T,1,0);
    		if(s[i][j]==t[i][j])ae(left(i,j),mid(i,j),(c[i][j]-'0')/2,0),ae(mid(i,j),right(i,j),(c[i][j]-'0')/2,0);
    		else{
    			if(s[i][j]=='0')ae(left(i,j),mid(i,j),(c[i][j]-'0')/2,0),ae(mid(i,j),right(i,j),(c[i][j]-'0'+1)/2,0);
    			if(t[i][j]=='0')ae(left(i,j),mid(i,j),(c[i][j]-'0'+1)/2,0),ae(mid(i,j),right(i,j),(c[i][j]-'0')/2,0);	
    		}
    		for(int k=0;k<8;k++)if(ok(i+dx[k],j+dy[k]))ae(right(i,j),left(i+dx[k],j+dy[k]),0x3f3f3f3f,1);
    	}
    	if(s1!=s2){puts("-1");return 0;}
    	while(SPFA());
    	if(flow==s1)printf("%d\n",cost);
    	else puts("-1");
    	return 0;
    }
    

    LII.[ZJOI2010]贪吃的老鼠

    神题,我写了题解

    LIX.[SDOI2014]LIS

    多测不清空,爆零两行泪

    因为一个\(queue\)没有清空我调了一下午QaQ……

    首先,这道题与IV.最长不下降子序列问题极像,也是一样的分层建图。第一问就是直接跑最小割。但是接下来我们要输出字典序最小的最小割,怎么办呢?

    我一开始想了非常暴力的方法:枚举一条边,断开它,看(新最小割+这条边的容量)等不等于(原本的最小割)。当然,复杂度过大,只有\(60\)连臭氧都救不了

    然后看题解,发现这道题就是求最小割的可行边

    最小割的可行边\((u,v)\)是这样的边,在某个残量网络中:

    \(\text{不存在任何一条增广路径可以从u到达v。}\)

    我们可以仍然按照\(c\)递增的顺序枚举边。如果这条边不是可行边,直接跳过;否则,考虑删去这条边,并抹去它的一切影响。

    抹去影响的方法就是:

    不妨设在原本的残量网络中,\(u\in \mathbb{S}\)\(v\in\mathbb{T}\)。那么,以\(u\)\(S'\)\(S\)\(T'\),跑最大流,就相当于\(u\)把流量还给了\(S\);以\(T\)\(S''\)\(v\)\(T''\),跑最大流,就相当于\(T\)把流量还给了\(u\)。之后,断去边\((u,v)\),即彻底地把这条边从图上删去,并使得这张图仍然是一条合法的残量网络。这种手法被称作退流

    代码:

    #pragma GCC optimize(3)
    #include<bits/stdc++.h>
    using namespace std;
    #define int long long
    const int INF=0x3f3f3f3f3f3f3f3f;
    int T_T,n,a[1010],b[1010],c[1010],f[1010],mx,ord[1010],id[1010];
    namespace MaxFlow{
    	const int N=2000,M=2000000;
    	int head[N],cur[N],dep[N],cnt,S,T,res;
    	struct node{
    		int to,next,val;
    	}edge[M];
    	inline void ae(int u,int v,int w){
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	inline bool bfs(){
    		memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
    		while(!q.empty()){
    			register int x=q.front();q.pop();
    			for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    		}
    		return dep[T]>0;
    	}
    	bool reach;
    	inline int dfs(int x,int flow){
    		if(x==T){
    			res+=flow;
    			reach=true;
    			return flow;
    		}
    		int used=0;
    		for(register int &i=cur[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
    			register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    			if(ff){
    				edge[i].val-=ff;
    				edge[i^1].val+=ff;
    				used+=ff;
    				if(used==flow)break;
    			}
    		}
    		return used;
    	}
    	inline void Dinic(){
    		while(bfs()){
    			reach=true;
    			while(reach)reach=false,dfs(S,INF);
    		}
    	}
    }
    using namespace MaxFlow;
    inline bool cmp(int x,int y){
    	return c[x]<c[y];
    }
    bool vis[2000];
    inline bool che(int u,int v){
    	while(!q.empty())q.pop();
    	memset(vis,false,sizeof(vis));
    	q.push(u),vis[u]=true;
    	while(!q.empty()){
    		int x=q.front();q.pop();
    		if(x==v)return true;
    		for(int i=head[x];i!=-1;i=edge[i].next)if(!vis[edge[i].to]&&edge[i].val)q.push(edge[i].to),vis[edge[i].to]=true;
    	}
    	return false;
    }
    vector<int>v; 
    signed main(){
    	scanf("%lld",&T_T);
    	while(T_T--){
    		scanf("%lld",&n),S=2*n+1,T=2*n+2,mx=0,v.clear();
    		for(register int i=1;i<=n;i++)scanf("%lld",&a[i]),ord[i]=i;
    		for(register int i=1;i<=n;i++)scanf("%lld",&b[i]);
    		for(register int i=1;i<=n;i++)scanf("%lld",&c[i]);
    		for(register int i=1;i<=n;i++){
    			f[i]=1;
    			for(register int j=1;j<i;j++)if(a[j]<a[i])f[i]=max(f[i],f[j]+1);
    			mx=max(mx,f[i]);
    		}
    		memset(head,-1,sizeof(head)),cnt=res=0;
    		for(register int i=1;i<=n;i++){
    			id[i]=cnt,ae(i,i+n,b[i]);
    			if(f[i]==1)ae(S,i,INF);
    			if(f[i]==mx)ae(i+n,T,INF);
    			for(register int j=1;j<i;j++)if(f[j]+1==f[i]&&a[j]<a[i])ae(j+n,i,INF);
    		}
    		Dinic();
    		printf("%lld ",res);
    		sort(ord+1,ord+n+1,cmp);
    		for(register int i=1;i<=n;i++){
    			if(che(ord[i],ord[i]+n))continue;
    			v.push_back(ord[i]);
    			S=ord[i],T=2*n+1,Dinic();
    			S=2*n+2,T=ord[i]+n,Dinic();
    			edge[id[ord[i]]].val=edge[id[ord[i]]^1].val=0;
    		}
    		sort(v.begin(),v.end());
    		printf("%d\n",v.size());
    		for(int i=0;i<v.size();i++)printf("%lld ",v[i]);puts("");
    	}
    	return 0;
    }
    

    LXI.[AHOI2009]最小割

    这里就是我们在LIX.[SDOI2014]LIS里面提到的最小割的可行边与必须边的应用。

    复习一下,一条边是最小割的可行边,当且仅当

    \(\text{不存在任何一条增广路径可以从u到达v。}\)

    而一条边是最小割的必须边,当且仅当

    \(\text{存在一条增广路径可以从S到达u,并存在一条增广路径可以从v到达T。}\)

    但是,这道题中,如果你像LIX一样,爆搜判连通的话,会取得30分的好成绩;如果你聪明点,会预处理出点对间相互到达的关系的话,能够拿到70分。

    这就启发我们必须要优化判连通的复杂度。

    这个时候,我们就可以用\(tarjan\)算法求出点双连通分量(\(SCC\)),判连通。

    如果你真就这么写了个\(tarjan\)交上去,会取得20分的好成绩。你满眼都会是红红火火。

    为什么呢?

    哦,我们忘记了一条重要限制:

    \(\color{red}\colorbox{Cadetblue}{任何一条边要想是必须边或者可行边,必须流量跑满。}\)

    然后就过了。

    代码:

    #pragma GCC optimize(3)
    #include<bits/stdc++.h>
    using namespace std;
    int n,m,dfn[4010],low[4010],col[4010],C,tot;
    namespace MF_ISAP{
    	const int N=5000,M=200000;
    	int head[N],cur[N],dep[N],gap[N],cnt,S,T,res;
    	struct node{
    		int to,next,val;
    	}edge[M];
    	void ae(int u,int v,int w){
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	inline void bfs(){
    		memset(dep,-1,sizeof(dep)),memset(gap,0,sizeof(gap)),q.push(T),dep[T]=0;
    		while(!q.empty()){
    			register int x=q.front();q.pop();
    			gap[dep[x]]++;
    			for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(dep[edge[i].to]==-1)dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    		}
    	}
    	inline int dfs(int x,int flow){
    		if(x==T){
    			res+=flow;
    			return flow;
    		}
    		int used=0;
    		for(register int &i=cur[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val||dep[edge[i].to]+1!=dep[x])continue;
    			register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    			if(ff){
    				edge[i].val-=ff;
    				edge[i^1].val+=ff;
    				used+=ff;
    			}
    			if(used==flow)return used;
    		}
    		gap[dep[x]]--;
    		if(!gap[dep[x]])dep[S]=n+1;
    		dep[x]++;
    		gap[dep[x]]++;
    		return used;
    	}
    	inline void ISAP(){
    		bfs();
    		while(dep[S]<n)memcpy(cur,head,sizeof(head)),dfs(S,0x3f3f3f3f);
    	}
    }
    using namespace MF_ISAP;
    struct EDGE{
    	int u,v,w,id;
    }e[60010];
    stack<int>s;
    void tarjan(int x){
    	s.push(x);
    	dfn[x]=low[x]=++tot;
    	for(int i=head[x];i!=-1;i=edge[i].next){
    		if(!edge[i].val)continue;
    		if(!dfn[edge[i].to])tarjan(edge[i].to),low[x]=min(low[x],low[edge[i].to]);
    		else if(!col[edge[i].to])low[x]=min(low[x],dfn[edge[i].to]);
    	}
    	if(low[x]!=dfn[x])return;
    	C++;
    	while(s.top()!=x)col[s.top()]=C,s.pop();
    	col[s.top()]=C,s.pop();
    }
    int main(){
    	scanf("%d%d%d%d",&n,&m,&S,&T),memset(head,-1,sizeof(head));
    	for(int i=1;i<=m;i++)scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w),e[i].id=cnt,ae(e[i].u,e[i].v,e[i].w);
    	ISAP();
    	for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
    	for(int i=1;i<=m;i++){
    		if(edge[e[i].id].val||col[e[i].u]==col[e[i].v]){puts("0 0");continue;}
    		printf("1 ");
    		puts(col[S]==col[e[i].u]&&col[e[i].v]==col[T]?"1":"0");
    	}
    	return 0;
    }
    

    LXVII.CF343E Pumping Stations

    这是一套最小割树的神题。

    我居然想着用费用流瞎搞

    首先,最小割树肯定要建。然后呢?

    考虑树中权值最小的一条边\((x,y,z)\)。则这条边一定会被统计入最终答案中至少一次,因为只要有排列中相邻的两个数一个在左侧,一个在右侧,这条边就会是最小割。而这种情况必定发生至少一次。

    我们考虑分治。对于左侧,我们设至少有两个节点\(x_1,x_2\);对于右侧,我们设至少有两个节点\((y_1,y_2)\)

    不妨设这里面有一些数是相邻的。

    假设排列为\(\dots,x_1,x_2,y_1,y_2,\dots\),则本段收益为\(dis_{x_1,x_2}+dis_{x_2,y_1}+dis_{y_1,y_2}\),其中\(dis\)为最小割大小。

    又有\(dis_{x_1,x_2}\geq z,dis_{x_2,y_1}= z,dis_{y_1,y_2}\geq z\)

    \(dis_{x_1,x_2}+dis_{x_2,y_1}+dis_{y_1,y_2}\geq 3z\)

    而另一种排列\(\dots,x_1,y_1,x_2,y_2,\dots\),收益\(=3z\)

    则显然,(每次选完这条边一端所有的数)的方案一定不劣于其它方案。

    因为断开一条边后,剩下的两端都仍是树,因此我们可以递归。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,cut[210][210];
    namespace MF{
    	const int N=2100,M=20100;
    	int head[N],cur[N],dep[N],cnt,S,T,res;
    	struct node{
    		int to,next,val,org;
    	}edge[M];
    	void ae(int u,int v,int w){
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=edge[cnt].org=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=edge[cnt].org=w,head[v]=cnt++;
    	}
    	queue<int>q;
    	inline bool bfs(){
    		memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
    		while(!q.empty()){
    			register int x=q.front();q.pop();
    			for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    		}
    		return dep[T]>0;
    	}
    	bool reach;
    	inline int dfs(int x,int flow){
    		if(x==T){
    			res+=flow;
    			reach=true;
    			return flow;
    		}
    		int used=0;
    		for(register int &i=cur[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
    			register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    			if(ff){
    				edge[i].val-=ff;
    				edge[i^1].val+=ff;
    				used+=ff;
    				if(used==flow)break;
    			}
    		}
    		return used;
    	}
    	inline void Dinic(){
    		res=0;
    		while(bfs()){
    			reach=true;
    			while(reach)reach=false,dfs(S,0x3f3f3f3f);
    		}
    	}
    	void initialize(){
    		for(int i=0;i<cnt;i++)edge[i].val=edge[i].org;
    	}
    }
    namespace GMT{
    	int ord[2100],cnt,head[2100],p,q,id,mn,res;
    	struct node{
    		int to,next,val;
    		bool vis;
    	}edge[4100];
    	void ae(int u,int v,int w){
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,edge[cnt].vis=false,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=w,edge[cnt].vis=false,head[v]=cnt++;
    	}
    	bool cmp(int x,int y){
    		return MF::dep[x]<MF::dep[y];
    	}
    	void work(int l,int r){
    		if(l==r)return;
    		MF::S=ord[l],MF::T=ord[r];
    		MF::Dinic(),ae(ord[l],ord[r],MF::res),MF::initialize();
    		sort(ord+l,ord+r+1,cmp);
    		int mid=upper_bound(ord+l,ord+r+1,0,cmp)-ord;
    		work(l,mid-1),work(mid,r);
    	}
    	bool vis[2100];
    	vector<int>v;
    	void dfs(int x,int fa){
    		for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to!=fa&&!edge[i].vis){
    			if(edge[i].val<mn)mn=edge[i].val,p=x,q=edge[i].to,id=i;
    			dfs(edge[i].to,x);
    		}
    	}
    	void solve(int x){
    		mn=0x3f3f3f3f,dfs(x,0);
    		if(mn==0x3f3f3f3f){v.push_back(x);return;}
    		edge[id].vis=edge[id^1].vis=true,res+=mn;
    		int u=p,v=q;
    		solve(u),solve(v);
    	}
    }
    int main(){
    	scanf("%d%d",&n,&m),memset(MF::head,-1,sizeof(MF::head)),memset(GMT::head,-1,sizeof(GMT::head));
    	for(int i=1,x,y,z;i<=m;i++)scanf("%d%d%d",&x,&y,&z),MF::ae(x,y,z);
    	for(int i=1;i<=n;i++)GMT::ord[i]=i;
    	GMT::work(1,n);
    	GMT::solve(1);
    	printf("%d\n",GMT::res);
    	for(int i=0;i<GMT::v.size();i++)printf("%d ",GMT::v[i]);
    	return 0;
    }
    

    LXIX.LOJ#115. 无源汇有上下界可行流

    (在这种题中,原汇点和源点用小写字母\(s\)\(t\)表示;新汇点和新源点用大写字母\(S\)\(T\)表示,中文称作伪汇点或新汇点)

    首先,我们要限制住流量。建一张新图,对于每条原图中的边\((x,y,lower,upper)\),在新图中连边\((x,y,upper-lower)\)。这样子,在新图中跑出的任何流都肯定符合原图中的流量限制,每条边的流量为(流量下界+新图中对应边的流量)。

    但是,满足了流量限制,我们还要满足流入和流出的流量限制。

    设立数组\(degree\)。对于每条边\((x,y,lower,upper)\)\(degree_y+=lower,degree_x-=lower\)

    这样子,最终的\(degree\)就是每个节点的初始流量。为正,意味着可以向其它点流流量,新图中连边\((S,x,degree_x)\);为负,意为着要靠其它点补贴它,连边\((x,T,-degree_x)\)。这样子就保证流入和流出的流量相等。

    则如果最终新图中流量跑满,就有一组可行流。每条边的流量为(流量下界+新图中对应边的流量)。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,d[210],sum,low[21000],id[21000];
    namespace MaxFlow{
    	const int N=1000,M=200000;
    	int head[N],cur[N],dep[N],cnt,S,T,res;
    	struct node{
    		int to,next,val;
    	}edge[M];
    	inline void ae(int u,int v,int w){
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	inline bool bfs(){
    		memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
    		while(!q.empty()){
    			register int x=q.front();q.pop();
    			for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    		}
    		return dep[T]>0;
    	}
    	bool reach;
    	inline int dfs(int x,int flow){
    		if(x==T){
    			res+=flow;
    			reach=true;
    			return flow;
    		}
    		int used=0;
    		for(register int &i=cur[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
    			register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    			if(ff){
    				edge[i].val-=ff;
    				edge[i^1].val+=ff;
    				used+=ff;
    				if(used==flow)break;
    			}
    		}
    		return used;
    	}
    	inline void Dinic(){
    		while(bfs()){
    			reach=true;
    			while(reach)reach=false,dfs(S,0x3f3f3f3f);
    		}
    	}
    }
    using namespace MaxFlow;
    signed main(){
    	scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=n+1,T=n+2;
    	for(register int i=1,x,y,l,r;i<=m;i++)scanf("%d%d%d%d",&x,&y,&l,&r),id[i]=cnt,low[i]=l,ae(x,y,r-l),d[x]-=l,d[y]+=l;
    	for(register int i=1;i<=n;i++)if(d[i]>0)ae(S,i,d[i]),sum+=d[i];else if(d[i]<0)ae(i,T,-d[i]);
    	Dinic();
    	if(sum!=res){puts("NO");return 0;}
    	puts("YES");
    	for(register int i=1;i<=m;i++)printf("%d\n",low[i]+edge[id[i]^1].val);
    	return 0;
    }
    

    LXXI.LOJ#116. 有源汇有上下界最大流

    首先,有源汇的情况,我们已经知道,是连一条边\((t,s,INF)\)来解决。这题也是。

    首先,先跑一遍有源汇可行流。因为\((t,s,INF)\)这条边是为了平衡源汇点的流量而设,因此这条边在跑完从\(S\)\(T\)的可行流后的流量,便是当前\(s\)\(t\)的流量。

    然后,我们拆去\((t,s,INF)\)和图中所有与伪源点\(S\)和伪汇点\(T\)(即与\(degree\)相连的那两个点)有关的边,并在\(s\)\(t\)再跑一遍最大流。则答案为(第一遍可行流时\((t,s,INF)\)上的流量+第二遍的最大流)。

    理解:第一遍是帮我们试探出了一组合法的解。之后,在断掉所有新加的边后,再跑最大流,就是正常网络流的操作(在残量网络上瞎搞,看能不能使最大流增大)。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,s,t,degree[310],sum;
    namespace MaxFlow{
    	const int N=1000,M=200000;
    	int head[N],cur[N],dep[N],cnt,S,T,res;
    	struct node{
    		int to,next,val;
    	}edge[M];
    	void ae(int u,int v,int w){
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	inline bool bfs(){
    		memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
    		while(!q.empty()){
    			register int x=q.front();q.pop();
    			for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    		}
    		return dep[T]>0;
    	}
    	bool reach;
    	inline int dfs(int x,int flow){
    		if(x==T){
    			res+=flow;
    			reach=true;
    			return flow;
    		}
    		int used=0;
    		for(register int &i=cur[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
    			register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    			if(ff){
    				edge[i].val-=ff;
    				edge[i^1].val+=ff;
    				used+=ff;
    				if(used==flow)break;
    			}
    		}
    		return used;
    	}
    	inline void Dinic(){
    		while(bfs()){
    			reach=true;
    			while(reach)reach=false,dfs(S,0x3f3f3f3f);
    		}
    	}
    }
    using namespace MaxFlow;
    int main(){
    	scanf("%d%d%d%d",&n,&m,&s,&t),memset(head,-1,sizeof(head)),S=n+1,T=n+2;
    	for(int i=1,x,y,l,r;i<=m;i++)scanf("%d%d%d%d",&x,&y,&l,&r),degree[y]+=l,degree[x]-=l,ae(x,y,r-l);
    	ae(t,s,0x3f3f3f3f);
    	for(int i=1;i<=n;i++)if(degree[i]>0)ae(S,i,degree[i]),sum+=degree[i];else ae(i,T,-degree[i]);
    	Dinic();
    	if(sum!=res){puts("please go home to sleep");return 0;}
    	for(int i=head[t];i!=-1;i=edge[i].next)if(edge[i].to==s)res=edge[i^1].val,edge[i].val=edge[i^1].val=0;
    	for(int i=head[S];i!=-1;i=edge[i].next)edge[i].val=edge[i^1].val=0;
    	for(int i=head[T];i!=-1;i=edge[i].next)edge[i].val=edge[i^1].val=0;
    	S=s,T=t;
    	Dinic();
    	printf("%d\n",res);
    	return 0;
    }
    

    LXXII.LOJ#117. 有源汇有上下界最小流

    首先先像无源汇可行流一样建图跑,然后连边\((t,s,INF)\),然后再跑。答案即为\((t,s,INF)\)的流量。

    理解:第一遍时,所有流量都被尽可能地压榨出去,这保证需要经过\((t,s,INF)\)的流量最小。然后连边后再跑,就保证了这是一组合法解。第一遍保证最优,第二遍保证可行。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,m,s,t,degree[100010],sum;
    namespace MaxFlow{
    	const int N=50100,M=2000000;
    	int head[N],cur[N],dep[N],cnt,S,T,res;
    	struct node{
    		int to,next,val;
    	}edge[M];
    	void ae(int u,int v,int w){
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	inline bool bfs(){
    		memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
    		while(!q.empty()){
    			register int x=q.front();q.pop();
    			for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    		}
    		return dep[T]>0;
    	}
    	bool reach;
    	inline int dfs(int x,int flow){
    		if(x==T){
    			res+=flow;
    			reach=true;
    			return flow;
    		}
    		int used=0;
    		for(register int &i=cur[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
    			register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    			if(ff){
    				edge[i].val-=ff;
    				edge[i^1].val+=ff;
    				used+=ff;
    				if(used==flow)break;
    			}
    		}
    		return used;
    	}
    	inline void Dinic(){
    		while(bfs()){
    			reach=true;
    			while(reach)reach=false,dfs(S,0x3f3f3f3f);
    		}
    	}
    }
    using namespace MaxFlow;
    int main(){
    	scanf("%d%d%d%d",&n,&m,&s,&t),memset(head,-1,sizeof(head)),S=n+1,T=n+2;
    	for(int i=1,x,y,l,r;i<=m;i++)scanf("%d%d%d%d",&x,&y,&l,&r),degree[y]+=l,degree[x]-=l,ae(x,y,r-l);
    	for(int i=1;i<=n;i++)if(degree[i]>0)ae(S,i,degree[i]),sum+=degree[i];else ae(i,T,-degree[i]);
    	Dinic();
    	ae(t,s,0x3f3f3f3f);
    	Dinic();
    	if(sum!=res){puts("please go home to sleep");return 0;}
    	for(int i=head[t];i!=-1;i=edge[i].next)if(edge[i].to==s)res=edge[i^1].val;
    	printf("%d\n",res);
    	return 0;
    }
    

    LXXIV.[WC2007]剪刀石头布

    神题。

    标签上写着“网络流”和“差分”,可是我怎么想也想不出它和网络流有什么关系。直到我看了题解。

    首先,这道题可以反面考虑,即最小化非三元环的三元组数。我们来观察一下一个典型的非三元环的出入度信息:一定是一个入度为\(2\),一个出度为\(2\),一个出入度都为\(1\)。这是唯一的情形,因为这个竞赛图肯定是完全图,任何一个三元组之间三条边肯定都连上了。它们要么是三元环,要么只有一条边反向了,因此这是唯一情形。

    我们以入度为例。则每有一个入度为\(2\)的点,就会拆散\(1\)个三元环。那么一个入度为\(3\)的点呢?显然,从三个指向它的点中任选两个点,再加上它自己,一定构成了一个非三元环。因此,一个入度为\(3\)的点拆散了\(C_3^2=3\) 个三元环。

    更一般地说,一个入度为\(deg_i\)的点,它共拆散了\(C_{deg_i}^2\)个三元环,其中\(deg_i\in [0,n)\)

    我们在分配一条未标明方向(即胜负未明)的边后,肯定有一个点的入度增加了\(1\)。考虑在\(deg_i+1\)后的新拆散三元环数量:

    \(C_{deg_i+1}^2-C_{deg_i}^2=\dfrac{deg_i(deg_i+1)}{2}-\dfrac{deg_i(deg_i-1)}{2}=deg_i\)

    也就是说,入度每增加\(1\),就会拆散(入度)个三元环。

    考虑用网络流解决这个问题。我们给每个未标明方向的边\((i,j)\)一个编号\(ord_{i,j}\),并连边\((S,ord_{i,j},1,0),(ord_{i,j},i,1,0),(ord_{i,j},j,1,0)\),表示这条边产生的效果是必须在\(i\)\(j\)两个点之间只选择一个点,并让它的入度加一。

    然后,对于\(\forall i \in [1,n],j \in [deg_i,n)\),连边\((i,T,1,j)\),表示这个点第一次入度加一会拆散\(deg_i\)个三元环,第二次会拆散\(deg_i+1\)个三元环,第三次……

    则答案为(总三元环数-最小费用),即\(ans=C_n^3-cost=\dfrac{n(n-1)(n-2)}{6}-cost\)

    哦,另外,每个点一开始的基础入度已经拆散了一些点。因此答案还要再减去\(\sum\limits_{i=1}^n\dfrac{deg_i(deg_i-1)}{2}\)

    \(ans=\dfrac{n(n-1)(n-2)}{6}-cost-\sum\limits_{i=1}^n\dfrac{deg_i(deg_i-1)}{2}\)

    至于输出方案,就看每个\(ord_{i,j}\)究竟把流量流给了\(i\)还是\(j\)

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,g[110][110],deg[110],ord[110][110];
    namespace MCMF{
    	const int N=100000,M=2000000;
    	int head[N],cnt,dis[N],fr[N],id[N],S,T,cost;
    	struct node{
    		int to,next,val,cost;
    	}edge[M];
    	void ae(int u,int v,int w,int c){
    		edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	bool in[N];
    	bool SPFA(){
    		memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
    		while(!q.empty()){
    			int x=q.front();q.pop(),in[x]=false;
    	//		printf("%d\n",x);
    			for(int i=head[x];i!=-1;i=edge[i].next){
    				if(!edge[i].val)continue;
    				if(dis[edge[i].to]>dis[x]+edge[i].cost){
    					dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
    					if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
    				}
    			}
    		}
    		if(dis[T]==dis[0])return false;
    		int x=T,mn=0x3f3f3f3f;
    		while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
    		cost+=dis[T]*mn,x=T;
    		while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
    		return true;
    	}
    }
    using namespace MCMF;
    int main(){
    	scanf("%d",&n),memset(head,-1,sizeof(head));
    	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++){
    		scanf("%d",&g[i][j]);
    		if(i==j)continue;
    		if(g[i][j]==0)deg[i]++;
    	}
    	S=n;
    	for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(g[i][j]==2)ord[i][j]=++S;
    	S++,T=S+1;
    	for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++)if(g[i][j]==2)ae(S,ord[i][j],1,0),ae(ord[i][j],i,1,0),ae(ord[i][j],j,1,0);
    	for(int i=1;i<=n;i++){
    		cost+=(deg[i]-1)*deg[i]/2;
    		for(int j=deg[i];j<=n;j++)ae(i,T,1,j);
    	}
    	while(SPFA());
    	printf("%d\n",n*(n-1)*(n-2)/6-cost);
    	for(int i=1;i<=n;i++)for(int j=i+1;j<=n;j++){
    		if(g[i][j]!=2)continue;
    		for(int k=head[ord[i][j]];k!=-1;k=edge[k].next)if(edge[k].to>=1&&edge[k].to<=n&&!edge[k].val)g[i][j]=edge[k].to;
    		if(g[i][j]==i)g[i][j]=0,g[j][i]=1;
    		else g[i][j]=1,g[j][i]=0;
    	}
    	for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",g[i][j]);puts("");}
    	return 0;
    }
    

    LXXVI.无限之环

    这道题太恶心了……它超过了我以前不知道怎么想的去用treap去写的疫情控制,荣膺我有生以来所写过的最长的代码……

    这题也要拆点,并且还是拆成\(5\)个点!!!分别设为\(O,A,B,C,D\),表示中,上,右,下,左五个方向。

    首先,明显,可以奇偶建图。奇点从源点连流量,偶点连向汇点。

    接下来我们只分析奇点操作,偶点就是将奇点操作反向我都是直接复制粘贴的

    我们要分情况讨论。

    首先,我们都要连边\((S,O,INF,0)\),从中点分配流量。

    1.O形,即\(1,2,4,8\)

    \(1\)为例,显然,有一条免费的边是\((O,A,1,0)\)

    如果逆向或正向旋转的话,费用为\(1\),连边\((A,B,1,1)\)\((A,D,1,1)\)

    如果\(180^{\circ}\)旋转的话,费用为\(2\),连边\((A,C,1,2)\)

    2.\(L\)形,即\(3,6,12,9\)。以\(3\)为例,显然,有两条免费的边\((O,A,1,0)\)\((O,B,1,0)\)

    如果旋转\(90^{\circ}\)的话,可能是\(A\)不变,\(B\)转到\(D\)\(B\)不变,\(A\)转到\(C\)(自己画图理解一下),因此连边\((A,C,1,1)\)\((B,D,1,1)\)

    如果旋转\(180^{\circ}\)的话,就是两个操作同时进行,即\(A\)转到\(C\)的同时\(B\)转到\(D\),费用为\(2\),刚好是前面两条边同时走的效果。

    3.T形,即\(7,11,13,14\)。以\(7\)为例,显然,有\(3\)条免费的边\((O,A,1,0),(O,B,1,0),(O,C,1,0)\)

    同时,如果\(A\)\(C\)空出来,费用为\(1\);如果\(B\)空出来,费用为\(2\);因此连边\((A,D,1,1),(B,D,1,2),(C,D,1,1)\)

    4.其它。这些要么转不了,要么转了跟没转一样,直接连。

    然后我们就做完了这道大毒瘤。

    代码:

    #pragma GCC optimize(3)
    #include<bits/stdc++.h>
    using namespace std;
    #define O(x,y) (x)*m+(y)
    #define A(x,y) (x)*m+(y)+n*m
    #define B(x,y) (x)*m+(y)+n*m*2
    #define C(x,y) (x)*m+(y)+n*m*3
    #define D(x,y) (x)*m+(y)+n*m*4
    int n,m,dx[4]={-1,0,1,0},dy[4]={0,1,0,-1},sum;
    namespace MCMF{
    	const int N=100000,M=2000000;
    	int head[N],cnt,dis[N],fr[N],id[N],S,T,cost,res;
    	struct node{
    		int to,next,val,cost;
    	}edge[M];
    	void ae(int u,int v,int w,int c){
    		edge[cnt].cost=c,edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].cost=-c,edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	bool in[N];
    	bool SPFA(){
    		memset(dis,0x3f,sizeof(dis)),dis[S]=0,q.push(S),in[S]=true;
    		while(!q.empty()){
    			int x=q.front();q.pop(),in[x]=false;
    	//		printf("%d\n",x);
    			for(int i=head[x];i!=-1;i=edge[i].next){
    				if(!edge[i].val)continue;
    				if(dis[edge[i].to]>dis[x]+edge[i].cost){
    					dis[edge[i].to]=dis[x]+edge[i].cost,fr[edge[i].to]=x,id[edge[i].to]=i;
    					if(!in[edge[i].to])in[edge[i].to]=true,q.push(edge[i].to);
    				}
    			}
    		}
    		if(dis[T]==0x3f3f3f3f)return false;
    		int x=T,mn=0x3f3f3f3f;
    		while(x!=S)mn=min(mn,edge[id[x]].val),x=fr[x];
    		res+=mn,cost+=dis[T]*mn,x=T;
    		while(x!=S)edge[id[x]].val-=mn,edge[id[x]^1].val+=mn,x=fr[x];
    		return true;
    	}
    }
    using namespace MCMF;
    int main(){
    	scanf("%d%d",&n,&m),memset(head,-1,sizeof(head)),S=n*m*5,T=n*m*5+1;
    	for(int i=0;i<n;i++)for(int j=0,x;j<m;j++){
    		scanf("%d",&x);
    		if((i+j)&1){
    			sum+=__builtin_popcount(x);
    			ae(S,O(i,j),0x3f3f3f3f,0);
    			if(i>0)ae(A(i,j),C(i-1,j),1,0);
    			if(i<n-1)ae(C(i,j),A(i+1,j),1,0);
    			if(j>0)ae(D(i,j),B(i,j-1),1,0);
    			if(j<m-1)ae(B(i,j),D(i,j+1),1,0);
    			if(x==0)continue;
    			if(x==1){//^
    				ae(O(i,j),A(i,j),1,0);
    				ae(A(i,j),B(i,j),1,1);
    				ae(A(i,j),C(i,j),1,2);
    				ae(A(i,j),D(i,j),1,1);
    			}
    			if(x==2){//>
    				ae(B(i,j),A(i,j),1,1);
    				ae(O(i,j),B(i,j),1,0);
    				ae(B(i,j),C(i,j),1,1);
    				ae(B(i,j),D(i,j),1,2);		
    			}
    			if(x==3){//^>
    				ae(O(i,j),A(i,j),1,0);
    				ae(O(i,j),B(i,j),1,0);
    				ae(A(i,j),C(i,j),1,1);
    				ae(B(i,j),D(i,j),1,1);			
    			}
    			if(x==4){//_
    				ae(C(i,j),A(i,j),1,2);
    				ae(C(i,j),B(i,j),1,1);
    				ae(O(i,j),C(i,j),1,0);
    				ae(C(i,j),D(i,j),1,1);		
    			}
    			if(x==5){//|
    				ae(O(i,j),A(i,j),1,0);
    				ae(O(i,j),C(i,j),1,0);			
    			}
    			if(x==6){//_>
    				ae(C(i,j),A(i,j),1,1);
    				ae(O(i,j),B(i,j),1,0);
    				ae(O(i,j),C(i,j),1,0);
    				ae(B(i,j),D(i,j),1,1);				
    			}
    			if(x==7){//^>_
    				ae(O(i,j),A(i,j),1,0);
    				ae(O(i,j),B(i,j),1,0);
    				ae(O(i,j),C(i,j),1,0);
    				ae(A(i,j),D(i,j),1,1);	
    				ae(B(i,j),D(i,j),1,2);	
    				ae(C(i,j),D(i,j),1,1);	
    			}
    			if(x==8){//<
    				ae(D(i,j),A(i,j),1,1);
    				ae(D(i,j),B(i,j),1,2);
    				ae(D(i,j),C(i,j),1,1);
    				ae(O(i,j),D(i,j),1,0);				
    			}
    			if(x==9){//<^
    				ae(O(i,j),A(i,j),1,0);
    				ae(D(i,j),B(i,j),1,1);
    				ae(A(i,j),C(i,j),1,1);
    				ae(O(i,j),D(i,j),1,0);					
    			}
    			if(x==10){//-
    				ae(O(i,j),B(i,j),1,0);
    				ae(O(i,j),D(i,j),1,0);				
    			}
    			if(x==11){//<^>
    				ae(O(i,j),A(i,j),1,0);
    				ae(O(i,j),B(i,j),1,0);
    				ae(A(i,j),C(i,j),1,2);
    				ae(B(i,j),C(i,j),1,1);	
    				ae(D(i,j),C(i,j),1,1);	
    				ae(O(i,j),D(i,j),1,0);					
    			}
    			if(x==12){//<_
    				ae(C(i,j),A(i,j),1,1);
    				ae(D(i,j),B(i,j),1,1);
    				ae(O(i,j),C(i,j),1,0);
    				ae(O(i,j),D(i,j),1,0);					
    			}
    			if(x==13){//<^_
    				ae(O(i,j),A(i,j),1,0);
    				ae(A(i,j),B(i,j),1,1);
    				ae(C(i,j),B(i,j),1,1);
    				ae(D(i,j),B(i,j),1,2);	
    				ae(O(i,j),C(i,j),1,0);	
    				ae(O(i,j),D(i,j),1,0);			
    			}
    			if(x==14){//<_>
    				ae(B(i,j),A(i,j),1,1);
    				ae(C(i,j),A(i,j),1,2);
    				ae(D(i,j),A(i,j),1,1);
    				ae(O(i,j),B(i,j),1,0);	
    				ae(O(i,j),C(i,j),1,0);	
    				ae(O(i,j),D(i,j),1,0);					
    			}
    			if(x==15){//+
    				ae(O(i,j),A(i,j),1,0);
    				ae(O(i,j),B(i,j),1,0);
    				ae(O(i,j),C(i,j),1,0);
    				ae(O(i,j),D(i,j),1,0);	
    			}
    		}else{
    			ae(O(i,j),T,0x3f3f3f3f,0);
    			if(x==0)continue;
    			if(x==1){//^
    				ae(A(i,j),O(i,j),1,0);
    				ae(B(i,j),A(i,j),1,1);
    				ae(C(i,j),A(i,j),1,2);
    				ae(D(i,j),A(i,j),1,1);
    			}
    			if(x==2){//>
    				ae(A(i,j),B(i,j),1,1);
    				ae(B(i,j),O(i,j),1,0);
    				ae(C(i,j),B(i,j),1,1);
    				ae(D(i,j),B(i,j),1,2);		
    			}
    			if(x==3){//^>
    				ae(A(i,j),O(i,j),1,0);
    				ae(B(i,j),O(i,j),1,0);
    				ae(C(i,j),A(i,j),1,1);
    				ae(D(i,j),B(i,j),1,1);			
    			}
    			if(x==4){//_
    				ae(A(i,j),C(i,j),1,2);
    				ae(B(i,j),C(i,j),1,1);
    				ae(C(i,j),O(i,j),1,0);
    				ae(D(i,j),C(i,j),1,1);		
    			}
    			if(x==5){//|
    				ae(A(i,j),O(i,j),1,0);
    				ae(C(i,j),O(i,j),1,0);			
    			}
    			if(x==6){//_>
    				ae(A(i,j),C(i,j),1,1);
    				ae(B(i,j),O(i,j),1,0);
    				ae(C(i,j),O(i,j),1,0);
    				ae(D(i,j),B(i,j),1,1);				
    			}
    			if(x==7){//^>_
    				ae(A(i,j),O(i,j),1,0);
    				ae(B(i,j),O(i,j),1,0);
    				ae(C(i,j),O(i,j),1,0);
    				ae(D(i,j),A(i,j),1,1);	
    				ae(D(i,j),B(i,j),1,2);	
    				ae(D(i,j),C(i,j),1,1);	
    			}
    			if(x==8){//<
    				ae(A(i,j),D(i,j),1,1);
    				ae(B(i,j),D(i,j),1,2);
    				ae(C(i,j),D(i,j),1,1);
    				ae(D(i,j),O(i,j),1,0);				
    			}
    			if(x==9){//<^
    				ae(A(i,j),O(i,j),1,0);
    				ae(B(i,j),D(i,j),1,1);
    				ae(C(i,j),A(i,j),1,1);
    				ae(D(i,j),O(i,j),1,0);					
    			}
    			if(x==10){//-
    				ae(B(i,j),O(i,j),1,0);
    				ae(D(i,j),O(i,j),1,0);				
    			}
    			if(x==11){//<^>
    				ae(A(i,j),O(i,j),1,0);
    				ae(B(i,j),O(i,j),1,0);
    				ae(C(i,j),A(i,j),1,2);
    				ae(C(i,j),B(i,j),1,1);	
    				ae(C(i,j),D(i,j),1,1);	
    				ae(D(i,j),O(i,j),1,0);					
    			}
    			if(x==12){//<_
    				ae(A(i,j),C(i,j),1,1);
    				ae(B(i,j),D(i,j),1,1);
    				ae(C(i,j),O(i,j),1,0);
    				ae(D(i,j),O(i,j),1,0);					
    			}
    			if(x==13){//<^_
    				ae(A(i,j),O(i,j),1,0);
    				ae(B(i,j),A(i,j),1,1);
    				ae(B(i,j),C(i,j),1,1);
    				ae(B(i,j),D(i,j),1,2);	
    				ae(C(i,j),O(i,j),1,0);	
    				ae(D(i,j),O(i,j),1,0);			
    			}
    			if(x==14){//<_>
    				ae(A(i,j),B(i,j),1,1);
    				ae(A(i,j),C(i,j),1,2);
    				ae(A(i,j),D(i,j),1,1);
    				ae(B(i,j),O(i,j),1,0);	
    				ae(C(i,j),O(i,j),1,0);	
    				ae(D(i,j),O(i,j),1,0);					
    			}
    			if(x==15){//+
    				ae(A(i,j),O(i,j),1,0);
    				ae(B(i,j),O(i,j),1,0);
    				ae(C(i,j),O(i,j),1,0);
    				ae(D(i,j),O(i,j),1,0);	
    			}
    		}
    	}
    	while(SPFA());
    	if(res!=sum)puts("-1");
    	else printf("%d\n",cost); 
    	return 0;
    }
    

    LXXXVI.CF132E Bits of merry old England

    题解

    XCIV.CF1009G Allowed Letters

    网络流各种玄学残量网络的代表,题解

    C.[Topcoder12158]SurroundingGame

    最后一题,我们将在这题中探索如下东西的本质:最大权闭合子图问题&对偶建图法。

    这两种东西,都可以被抽象成如下模型:

    \(n\) 件物品,每件物品要分到两个集合 \(\mathbb{A},\mathbb{B}\)之一。一件物品分到集合 \(\mathbb{A}\) 的代价是 \(a_i\),分到集合 \(\mathbb{B}\) 的代价是 \(b_i\);同时,若对于两件物品 \(i,j\),都分到 \(\mathbb{A}\) 中会有额外费用 \(aa_{i,j}\),都到 \(\mathbb{B}\) 中则会有 \(bb_{i,j}\)\(i\in\mathbb{A},j\in\mathbb{B}\) 则有 \(ab_{i,j}\)\(i\in\mathbb{B},j\in\mathbb{A}\) 则有 \(ba_{i,j}\)。所有代价都可能为正或负。求费用最小的分配方案。(当然,实际应用中也有求最大的,此时就全部取反权即可)

    例如,在“最大权闭合子图”问题中,我们会将所有东西分到两个集合中:选择的集合 \(\mathbb{A}\) 和不选择的集合 \(\mathbb{B}\)。而若一个点 \(i\) 被选择,它所连向的节点 \(j\) 却未被选择,此时方案是不合法的,即相当于 \(ab_{i,j}=\infty\),而其它东西(除了 \(a_i,b_i\))的值都为 \(0\)

    我们总是可以把问题抽象成一张长这样的图,通过上面的最小割——在最小割后,令 \(\mathbb{A}=\mathbb{S}\)\(\mathbb{B}=\mathbb{T}\)——来解决问题。

    如图,当 \(i\in\mathbb{S},j\in\mathbb{T}\) 时,代价为 \(a_i+b_j+ab_{i,j}\),而反映到图上则是 \(b+c+e\)

    同理,通过把 \(i,j\) 分配到不同集合,我们可以得到四组方程:

    \(\begin{cases}a_i+b_j+ab_{i,j}=b+c+e\\b_i+a_j+ba_{i,j}=a+d+f\\a_i+a_j+aa_{i,j}=e+f\\b_i+b_j+bb_{i,j}=a+b\end{cases}\)

    因为对于不同的 \((i,j)\) 对,\(a_i,a_j\) 等东西在所有情形中只能够被计算一次,可以在最后一次性加到对应的 \(S\) 边或 \(T\) 边上,所以我们这里就先不考虑它们,只考虑 \(ab,ba,aa,bb\) 这四个。

    于是现在方程便变为了

    \(\begin{cases}ab_{i,j}=b+c+e\\ba_{i,j}=a+d+f\\aa_{i,j}=e+f\\bb_{i,j}=a+b\end{cases}\)

    显然,在网络流中,任意边的边权都应该为正,不能出现负边,故应有 \(a,b,c,d,e,f\geq0\);但是,观察到在任意一组最小割中, \(a,e\) 两边中选且仅被选了一条边,\(b,f\) 两边中选且仅被选了一条边,这意味着我们可以将 \(a,e\) 两条边的权值同时加上某个数,最后在跑完最小割后再把加上的这个东西减去即可。例如,若 \(e<0\),则原本的 \((i,T,e)\) 这条边,可以被替换成 \((S,i,-e)\) 这条边,然后答案减去 \(-e\)。这就是最大权闭合子图中,对点的权值正负分别判断是连到 \(S\) 或是连到 \(T\) 的原因。有了这个trick,我们便不需要求 \(a,b,e,f\geq0\),只需保证 \(c,d\geq0\) 即可。

    显然,六个未知数,四个方程,一般来说没有唯一解;但是,本题特殊的地方在于通过加加减减,我们可以得出

    \(c+d=ab_{i,j}+ba_{i,j}+aa_{i,j}-bb_{i,j}\)

    设此式结果为 \(K\)。则明显,若 \(K<0\),则不可能存在任何一组合法的 \(c,d\) 解。而当 \(K\geq0\) 时,明显至少存在一组解,为图方便,直接令 \(c=d=\dfrac{K}{2}\) 即可,此时 \(c,d\) 两条有向边便可合并成一条无向边。

    \(c,d\) 的值一旦确定,则 \(a,b,e,f\) 也可直接通过解方程(明显现在已经被化成了有唯一解的四元四式方程组)解出。

    但是,在大多数题中,\(aa,bb,ab,ba\) 这四个东西不是全非零,所以大多数时候不需要解方程。

    我们发现,当 \(K<0\) 时,我们无法找到一组合法的分配方式;但是,如果原本应用的图满足二分图性质(即,所有的 \(i,j\) 可以被分作两个集合,集合内部的 \(aa,bb,ab,ba\) 全部为 \(0\),只有集合间的值才非零)的话,我们可以通过翻转一个集合(即,令对于一个集合中的点来说,\(\mathbb{A}=\mathbb{S}\)\(\mathbb{B}=\mathbb{T}\);而对于另一个集合来说,\(\mathbb{A}=\mathbb{T}\)\(\mathbb{B}=\mathbb{S}\))来使得 \(K\geq0\)。但是,若原图不是二分图,则本方法就不再适用了。

    幸运的是,大部分此类题中,要么直接有 \(K\geq0\)(例如最大权闭合子图),要么是二分图(例如本题)。二分图的常见场景即为网格图。

    在本题中,我们可以通过拆点来抽象出模型。我们定义 \(x\) 表示图中某个位置 \(x\) 是否放石头的状态,另外定义 \(x'\) 表示是否周围全都有石头的状态。设 \(c_x\) 表示放石头的代价,\(v_x\) 表示被占据的收益。现在考虑需要连的边。

    显然,当 \(x,x'\) 同时成立的情形,位置 \(x\) 的收益不能被计算两次,所以此处有额外的代价 \(v_x\)

    \(x\) 在棋盘上有一个相邻位置 \(y\)。则,若 \(x'\) 被选择,但 \(y\) 却没有被选择,显然这是不成立的。故此处有代价 \(\infty\)

    其它的代价就只是一个点被分到某个集合中产生的代价(可能为负,此时就是收益)了,这部分是容易的。

    现在正式考虑建图。事实上,本题的建图中并不需要解方程。我们发现,\(x\)\(x'\) 应该是反向(即一个是 \(\mathbb{S}\)\(\mathbb{T}\) 不选,一个是 \(\mathbb{T}\)\(\mathbb{S}\) 不选)的,因为它们是同侧有代价;而 \(x'\)\(y\) 应该是同向的,因为它们是异侧有代价。于是,我们得到 \(x\)\(y\) 是异侧的。这可能吗?

    可能,因为原图是网格图,可以被黑白染色。

    所以,我们不妨设 \(x\)\(\mathbb{S}\) 不选 \(\mathbb{T}\) 选的,而 \(x',y\) 则是 \(\mathbb{S}\)\(\mathbb{T}\) 不选的。

    \(x\) 选时,其与 \(S\) 的边应该被割去,代价是 \(c_x-v_x\)。所以连边 \((S,x,c_x-v_x)\)

    \(x\) 不选时,其没有代价,故其与 \(T\) 间无边。

    \(x'\) 选时,有边 \((x,T,-v_x)\)(负权因为是收益);当 \(x'\) 不选时,无代价,与 \(S\) 无边。

    \(y\) 以及其对应的 \(y'\) 的连边与 \(x\) 的相反。

    现在考虑连接两点间的边。\(x,x'\) 间的边,若画出图来,会发现是 \((x',x,v_x)\)\(y,x'\) 间的边,则是 \((x',y,\infty)\)

    在建图的时候,注意使用我们上文提到的将边权化正的trick。

    (均是有向边)

    这样,我们便得到了需要的图;求其最小割即可。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    const int N=810;
    const int M=2001000;
    int dx[4]={1,0,-1,0},dy[4]={0,1,0,-1};	
    namespace MaxFlow{
    	int head[N],cur[N],dep[N],cnt,S,T,res;
    	struct node{int to,next,val;}edge[M];
    	void ae(int u,int v,int w){
    //		printf("%d %d %d\n",u,v,w);
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	inline bool bfs(){
    		memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
    		while(!q.empty()){
    			register int x=q.front();q.pop();
    			for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    		}
    		return dep[T]>0;
    	}
    	bool reach;
    	inline int dfs(int x,int flow){
    		if(x==T){
    			res+=flow;
    			reach=true;
    			return flow;
    		}
    		int used=0;
    		for(register int &i=cur[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
    			register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    			if(ff){
    				edge[i].val-=ff;
    				edge[i^1].val+=ff;
    				used+=ff;
    				if(used==flow)break;
    			}
    		}
    		return used;
    	}
    	inline void Dinic(){
    		while(bfs()){
    			reach=true;
    			while(reach)reach=false,dfs(S,0x3f3f3f3f);
    		}	
    	}	
    }
    using namespace MaxFlow;
    class SurroundingGame{
    private:
    	int a[30][30],b[30][30],n,m;
    	int trans(char ip){
    		if('0'<=ip&&ip<='9')return ip-'0';
    		if('a'<=ip&&ip<='z')return ip-'a'+10;
    		if('A'<=ip&&ip<='Z')return ip-'A'+36;
    	}
    public:
    	int maxScore(vector<string>c,vector<string>w){
    		n=c.size(),m=c[0].size(),S=2*n*m,T=S+1,memset(head,-1,sizeof(head));
    		for(int i=0;i<n;i++)for(int j=0;j<m;j++)a[i][j]=trans(c[i][j])-trans(w[i][j]),b[i][j]=trans(w[i][j]);
    //		for(int i=0;i<n;i++,puts(""))for(int j=0;j<m;j++)printf("%d ",a[i][j]);puts("");
    //		for(int i=0;i<n;i++,puts(""))for(int j=0;j<m;j++)printf("%d ",b[i][j]);puts("");
    		int sum=0;
    		for(int i=0;i<n;i++)for(int j=0;j<m;j++){
    			if(a[i][j]>0){
    				if((i+j)&1)ae(S,i*m+j,a[i][j]);
    				else ae(i*m+j,T,a[i][j]);
    			}
    			if(a[i][j]<0){
    				if((i+j)&1)ae(i*m+j,T,-a[i][j]);
    				else ae(S,i*m+j,-a[i][j]);
    				sum+=-a[i][j];
    			}
    			sum+=b[i][j];
    			if((i+j)&1)ae(n*m+i*m+j,i*m+j,b[i][j]),ae(S,n*m+i*m+j,b[i][j]);
    			else ae(i*m+j,n*m+i*m+j,b[i][j]),ae(n*m+i*m+j,T,b[i][j]);
    			for(int k=0;k<4;k++){
    				int ii=i+dx[k],jj=j+dy[k];
    				if(ii>=n||ii<0||jj>=m||jj<0)continue;
    				if((i+j)&1)ae(n*m+i*m+j,ii*m+jj,0x3f3f3f3f);
    				else ae(ii*m+jj,n*m+i*m+j,0x3f3f3f3f);
    			}
    		}
    		Dinic();
    //		printf("%d %d\n",sum,res);
    		return sum-res;
    	}
    }my;
    

    CVII.[CTSC2008]祭祀

    定义“偏序集”为一个集合 \(\mathbb{S}\) 以及一种运算 \(\leq\) 构成的二元组 \((\mathbb{S},\leq)\),满足如下性质:

    • \(\forall x\in\mathbb{S},x\leq x\) 均成立

    • \(\forall x,y,z\in\mathbb{S}\text{ s.t. }x\leq y,y\leq z\),则 \(x\leq z\) 成立

    • \(\forall x,y\in\mathbb{S}\text{ s.t. }x\leq y,y\leq x\),则 \(x=y\) 成立

    典型的例子是DAG上是否可达的关系,例如本题,当对所有的边跑完传递闭包后,得到的转移矩阵 \(trans\) 即是一组偏序关系。另一个例子是集合间的包含关系(\(\subseteq\))。

    定义一组偏序集上的是一组有序序列 \(\{a\}\) 满足 \(\forall i<\Big|\{a\}\Big|\),均有 \(a_i\leq a_{i+1}\)

    定义一组偏序集上的反链是一组无序序列 \(\{a\}\) 满足 \(\forall i\neq j\in\{a\}\),均有 \(i\nleq j\text{ and } j\nleq i\)。换句话说,两两集合不相互包含。

    Dilworth定理:对于任何一组偏序集,其最长链长度等于其最小反链划分大小,其最长反链长度等于其最小链划分大小,其中一组划分指的是将一个集合划分成若干集合,使得两两集合的交集为空,所有集合的并集为全集。

    最长链长度一定等于最小反链划分大小,因为我们每次均可挑出所有极小元素组成一条反链;第一次挑选一定挑出了最长链上最小元素(因为其不可能有后继元素,不然最长链还能更长),第二次挑选一定挑出了最长链上次小元素(因为其所有的后继元素一定都是极小元素,不然最长链还能更长),以此类推,最后一次挑选一定挑出最长链上最大元素。

    关于最长反链长度的证明类似。

    回到本题。明显,本题我们需要找出传递闭包后的偏序集的最长反链长度,并输出方案。最长反链长度等于最小链划分;最小链划分,其与最小路径覆盖等价,因为DAG的偏序集仍是DAG,所以可以通过经典的拆点后建二分图,并求出最大独立集的套路来求出最小链划分大小。

    但是,求出最小链划分还不够,我们还要构造出一条最长反链。但首先先让我们找出求最大独立集的方法。

    最大独立集等于点数减最小点覆盖,因为对于最小点覆盖外的任意一对点对间都不可能有边(不然就违背了点覆盖的定义)。而最长反链,就由拆点后两端均在独立集内的点构成。具体而言,设独立集大小为 \(|I|\),反链大小为 \(|A|\),最小点覆盖大小为 \(|S|\),则应有 \(|I|=2n-|S|\)。考虑 \(|I|-|A|\),应为只有一端在 \(I\) 内的点数,必定是 \(\leq n\) 的。故必有 \(|I|-|A|\leq n\),即 \(|A|\geq|I|-n=n-|S|\)。而 \(n-|S|\),就是最小路径覆盖问题的答案,也即最小链划分大小,即最长反链长度。则 \(A\) 即为最长反链。

    于是问题转变为求出最大独立集,也即最小点覆盖的补集。故我们只需求出最小点覆盖。

    考虑求出最大匹配。考虑从右侧所有非匹配点出发遍历图,当从右到左时只走非匹配边,从左往右时只走匹配边,则最终一定得到众多两端均在右侧的增广路,因为右侧的非匹配点不可能连到左侧的非匹配点(不然可以直接连接两者,与最大匹配前提相悖),则从遍历到的左侧点一定可以沿某条匹配边回到右侧。我们在左侧选取所有被遍历到的点,以及右侧所有未被遍历到的点,它们构成一组最小点覆盖。

    显然,最小点覆盖的大小等于最大匹配大小,因为对于一个左侧遍历到的点,与其匹配的右侧点也一定被遍历到了,故此处只有左侧点被选择;对于一个右侧未被遍历到的点,因为其必定是一个被匹配的点(所有未被匹配的点都被作为起点进行了遍历),而其对应的左侧点又没有被选择,故此处只有右端点被选择;二者综合来看,即是所有匹配边选且仅被选一次,故上述结论成立。

    找到最小点覆盖后,取补集即得到了左侧未被遍历到的点和右侧遍历到的点,它们构成最大独立集。

    将上述结论结合后,便得到了求最长反链的方法。

    下面我们考虑什么点可能出现在最长反链内。因为我们上述解法,若二分图匹配采用 \(m\sqrt{n}\) 的Dinic法的话,单次是 \(O(n^{2.5})\) 的,所以对于每个点都跑一次匹配,复杂度也不过 \(O(n^{3.5})\),对于 \(n=100\) 是轻松通过的;于是我们考虑每个点,强制令其在反链内(这意味着所有它能到达或能到达它的点全部不可选择),并求出新图中的最长反链判断其是否比原图中长度只减小 \(1\) 即可。

    总复杂度 \(O(n^{3.5})\)

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    int n,m;
    namespace MaxFlow{
    	const int N=210,M=200000;
    	int head[N],cur[N],dep[N],cnt,S,T,res;
    	struct node{int to,next,val;}edge[M];
    	void ae(int u,int v,int w){
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	inline bool bfs(){
    		memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
    		while(!q.empty()){
    			register int x=q.front();q.pop();
    			for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    		}
    		return dep[T]>0;
    	}
    	bool reach;
    	inline int dfs(int x,int flow){
    		if(x==T){res+=flow,reach=true;return flow;}
    		int used=0;
    		for(register int &i=cur[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
    			register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    			if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;}
    		}
    		return used;
    	}
    	inline void Dinic(){while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}}
    }
    using namespace MaxFlow;
    bool trans[110][110],on[210],vis[210],vld[110];
    int mat[210];
    char s[110],t[110];
    void findpath(int x){
    	if(vis[x]==true)return;vis[x]=true;
    	for(int y=1;y<=n;y++)if(y!=mat[x]&&vld[y]&&trans[y][x-n]&&!vis[y])vis[y]=true,findpath(mat[y]);
    }
    int MaximalAntiChain(int ban){
    	memset(head,-1,sizeof(head)),cnt=res=0;
    	int all=0;
    	for(int i=1;i<=n;i++)vld[i]=!trans[ban][i]&&!trans[i][ban]&&i!=ban,all+=vld[i];
    	for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)if(vld[i]&&vld[j]&&trans[i][j])ae(i,j+n,1);
    	for(int i=1;i<=n;i++)if(vld[i])ae(S,i,1),ae(i+n,T,1);
    	Dinic();
    	return all-res;
    }
    void GetScheme(){
    	memset(mat,0,sizeof(mat));
    	for(int x=1;x<=n;x++)for(int i=head[x];i!=-1;i=edge[i].next)if(edge[i].to>n&&edge[i].to<=2*n&&!edge[i].val)mat[x]=edge[i].to,mat[edge[i].to]=x;
    //	for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts("");
    //	for(int i=1;i<=n;i++)printf("%d ",mat[i+n]);puts("");
    	memset(vis,false,sizeof(vis));
    	for(int i=1;i<=n;i++)if(vld[i]&&!mat[i+n])findpath(i+n);
    	for(int i=1;i<=n;i++)if(vld[i]&&!vis[i]&&vis[i+n])s[i]='1';else s[i]='0';
    }
    int main(){
    	scanf("%d%d",&n,&m),S=2*n+1,T=2*n+2;
    	for(int i=1,u,v;i<=m;i++)scanf("%d%d",&u,&v),trans[u][v]=true;
    	for(int k=1;k<=n;k++)for(int i=1;i<=n;i++)for(int j=1;j<=n;j++)trans[i][j]|=trans[i][k]&&trans[k][j];
    //	for(int i=1;i<=n;i++){for(int j=1;j<=n;j++)printf("%d ",trans[i][j]);puts("");}puts("");
    	int mx=MaximalAntiChain(0);printf("%d\n",mx);
    	GetScheme(),printf("%s\n",s+1);
    	for(int i=1;i<=n;i++)t[i]=(MaximalAntiChain(i)==mx-1?'1':'0');
    	printf("%s\n",t+1);
    	return 0;
    }
    

    CXIII.[北京省选集训2019]图的难题/[UOJ#168][UR #11]元旦老人与丛林

    两题是重题,第二题数据范围更大。

    首先,我们发现,如果原图的 \(|E|>2|V|-2\),显然是不合法的;

    再进一步思考,如果原图存在一个导出子图(包含一些点及所有两端均在该点集内的边的子图)\(G'=\{V',E'\}\) 使得 \(|E'|>2|V'|-2\),则也一定不合法。

    我们称“不存在上述导出子图”为“符合性质”。那么,如果原图 \(G\) 符合性质,是否就合法了呢?我们考虑归纳证明。

    首先,当 \(n=1\) 时显然成立。否则,假设对于所有 \(n'<n\) 均成立,考虑构造出 \(n\) 时的分配方案。

    考虑 \(n\) 个节点中度数最少的那个,不妨设为 \(x\)。当度数 \(\geq4\) 时,显然总边数不小于 \(\dfrac{4n}{2}=2n\),则此时整张图不符合上述 \(m\leq2n-2\) 的前提条件,可以舍去。

    则此度数只能为 \(0,1,2,3\)。前三者时,我们一定可以把任一条边分到 \(n-1\) 条边时的白图里,另一条分到黑图里,并且两张图均仍是森林。

    于是现在就只剩下度数为 \(3\) 的情形。

    因为 \(G\) 符合性质,所以其任意子图 \(G'\) 均有 \(|E'|\leq2|V'|-2\)。故我们定义一张子图 \(G'\) 是“满”的当且仅当 \(|E'|=2|V'|-2\)

    则若两张子图 \(G_1,G_2\) 都是满的,则它们的并也是满的。因为 \(G_1\cup G_2=G_1+G_2-G_1\cap G_2\),而因为 \(G\) 符合性质,所以 \(|E_1\cap E_2|\leq2|V_1\cap V_2|-2\);但是因为在上式中其前面是负号,所以有 \(|E_1\cup E_2|\geq2|V_1\cup V_2|-2\)。但是 \(G_1\cup G_2\) 也是 \(G\) 的子图,所以两者结合起来就只能有 \(|E_1\cup E_2|=2|V_1\cup V_2|-2\),即二者之并是满的。

    有了上述结论,我们再设上述度数为 \(3\) 的点连到了 \(a,b,c\) 三个点。则在删去 \(x\) 及其连边后,在剩余的 \(n-1\) 个点的图中,同时包含 \((a,b)\) 的满子图、同时包含 \((b,c)\) 的满子图、同时包含 \((a,c)\) 的满子图,这三者不可能同时出现,不然就可以把它们求并得到同时包含 \((a,b,c)\) 的满子图。得到这张图后,再加入 \(x\) 及其连边,便得到了一张 \(|E|=2|V|-1\) 的不合条件的图,与所有子图都符合条件矛盾。

    于是便证明了至少有一张上述满子图不存在,不妨设为 \((a,b)\) 的满子图不存在。而这时,如果我们往 \(n-1\) 个点的图中添加一条额外的 \((a,b)\) 边,显然这时整张图依旧符合性质,且点数为 \(n-1\),存在至少一种划分方案。在任一方案中,总有一棵森林包含 \((a,b)\),于是删去 \((a,b)\),连接 \((a,x)\)\((x,b)\),同时把 \((x,c)\) 加入另一棵森林。

    于是便构造出一种合法的分配方案,则 \(G\) 符合性质是充要条件。

    要判断是否存在不符条件的导出子图,等价于求 \(|E|-2|V|\)\(\max\),并观察其是否有 \(\leq-2\)。我们发现,若我们选择了一条边,则其两端的节点也必须被选择——这看上去像一个最大权闭合子图问题。我们化边为点,其中边的权值为 \(1\),点的权值为 \(-2\),然后求其最大权导出子图即可。

    需要注意的是,该导出子图必须非空。所以,我们必须枚举一个点,强制其在导出子图中(即将其权值设作 \(0\),然后最终求出来的最大权再减去 \(2\))。

    对于第一题,枚举点并暴力建图是可以通过的;然而对于第二题,暴力建图会喜获TLE,需要手动退流以保证复杂度。

    因为没退好流,所以最终还是T掉了。

    T掉的UOJ#168的代码:

    #include<bits/stdc++.h>
    using namespace std;
    int T,n,m;
    namespace MaxFlow{
    	const int N=6010,M=2000000;
    	int head[N],cur[N],dep[N],cnt,S,T,res;
    	struct node{int to,next,val;}edge[M];
    	void ae(int u,int v,int w){
    		edge[cnt].next=head[u],edge[cnt].to=v,edge[cnt].val=w,head[u]=cnt++;
    		edge[cnt].next=head[v],edge[cnt].to=u,edge[cnt].val=0,head[v]=cnt++;
    	}
    	queue<int>q;
    	inline bool bfs(){
    		memset(dep,0,sizeof(dep)),q.push(S),dep[S]=1;
    		while(!q.empty()){
    			register int x=q.front();q.pop();
    			for(register int i=cur[x]=head[x];i!=-1;i=edge[i].next)if(edge[i].val&&!dep[edge[i].to])dep[edge[i].to]=dep[x]+1,q.push(edge[i].to);
    		}
    		return dep[T]>0;
    	}
    	bool reach;
    	inline int dfs(int x,int flow){
    		if(x==T){res+=flow,reach=true;return flow;}
    		int used=0;
    		for(register int &i=cur[x];i!=-1;i=edge[i].next){
    			if(!edge[i].val||dep[edge[i].to]!=dep[x]+1)continue;
    			register int ff=dfs(edge[i].to,min(edge[i].val,flow-used));
    			if(ff){edge[i].val-=ff,edge[i^1].val+=ff,used+=ff;if(used==flow)break;}
    		}
    		return used;
    	}
    	inline int Dinic(int s,int t){S=s,T=t,res=0;while(bfs()){reach=true;while(reach)reach=false,dfs(S,0x3f3f3f3f);}return res;}
    }
    using MaxFlow::Dinic;
    using MaxFlow::edge;
    using MaxFlow::ae;
    using MaxFlow::cnt;
    using MaxFlow::head;
    int tmp[2010],id[2010];
    void read(int &x){
    	x=0;
    	char c=getchar();
    	while(c>'9'||c<'0')c=getchar();
    	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
    }
    int main(){
    	read(n),read(m),memset(head,-1,sizeof(head));
    	int S=n+m+1,T=n+m+2,sum=0;
    	for(int i=1,x,y;i<=m;i++)read(x),read(y),ae(S,n+i,1),ae(n+i,x,0x3f3f3f3f),ae(n+i,y,0x3f3f3f3f),sum+=1;
    	for(int i=1;i<=n;i++)id[i]=cnt,ae(i,T,2);
    	for(int i=1;i<=n;i++){
    		edge[id[i]].val=edge[id[i]^1].val=0;
    		sum+=Dinic(i,S);
    		sum-=Dinic(S,T);
    		if(sum>0){puts("No");return 0;}
    		edge[id[i]].val=2,edge[id[i]^1].val=0;
    	}
    	puts("Yes");
    	return 0;
    }
    

    CXXIII.[NOI2019] 序列

    最近的NOI咋都喜欢玩模拟费用流这种阴间玩意

    首先,这题不难看出是个费用流模型。但是我想的多数奇奇怪怪的网络流都是带上下界的,因此没有任何优化前景

    正解是说,这个东西可以被看作是二分图最大权匹配的模型,其中任意匹配的点对不超过 \(K-L\) 对。任意匹配,可以通过建一个中间点,然后左部点全部连到该点,该点再连到全部右部节点,这样就能处理;而现在有了匹配上界的限制,就把该中间点拆点,使得只有一条流量为 \(K-L\) 的边。

    因为这张图比较别致,所以可以考虑用模拟费用流优化。

    什么是模拟费用流呢?就是用人类智慧手动跑费用流。

    首先,二分图匹配问题要手动实现不是不可能的,只需要找到你匹配的点对即可。

    于是我们考虑一点点地往图上添加流量,直到流量到达 \(K\)

    我们记“任意流”为经由前述 \(K-L\) 的边的流量,并设 \(fre\) 表示该边目前剩余的流量;再记“绑定流”为左部右部的同一位置配对的流量。

    显然,任意时刻,任意流总是不小于绑定流。于是,若 \(fre\) 非零,就流任意流即可。流任意流的方法也很简单,找到左右部权值最大的点,连一块即可。

    否则,就只能流绑定流了。绑定流有如下流法:

    1. 直接流 \(i,i'\)(令带 \('\) 的表示右部节点)。显然此时取所有双方都未配对的点对中最大的那个即可。

    2. 考虑现行有一条任意流 \(i\rightarrow j'\)。我们可以找到任意一个未配对节点 \(k\),并将其修改成 \(i\rightarrow i',k\rightarrow j\),这样子就让 \(i',k\) 这两个点参与了匹配。

    3. 同上,只不过这里是找到 \(k'\),然后修改为 \(j\rightarrow j',i\rightarrow k'\)

    每次流绑定流就从三者中找到最优的那种流即可。

    可以发现,二三两种操作本质上是先退流再增广,因此替代了费用流的操作,也因此成功模拟了费用流。

    我们考虑要维护什么才能支持查询。

    首先,左右部未匹配的点显然是要维护的。我们各开一个大根堆维护其中权值最大的。

    然后,\(i+i'\) 这种直接流的也要开一个堆维护。

    再后,因为 \(i\rightarrow j'\) 中,\(i'\)\(j\) 两个点都分别在二、三两种方案中参与了匹配,所以我们还需要分别维护 \(i',j\) 的最大值。准确地说,\(i'\) 就是所有自身尚未匹配,但其左部对应节点已经匹配的右部节点,而 \(j\) 同理是所有自身尚未匹配,右部对应节点已经匹配的左部节点。继续开堆维护即可。

    至于 \(k\)\(k'\),因为也没有特别的限制,所以就选择左右部未匹配的点中权值最大的即可,我们上文已经维护过了。

    现在,就是一些具体的细节了。举例来说,因为随着某些操作的执行,另一些堆中的某些点可能已经无效了,但我们又不太能把它从堆里掏出来,因此在堆顶出堆的时候,先判断一下堆顶的元素是否合法即可,如果不合法就一直弹,直到堆顶合法,或是堆空掉。

    再举例来说,当我们流任意流时,可能会出现任意流流成了绑定流,也即两端位置相同的情形。这时候,要记得把它算成绑定流,也即回退给 \(fre\) 一点流量。

    还有一个例子,就是若我们有任意流匹配 \(i\rightarrow j',k\rightarrow i'\),完全可以把它修改成 \(i\rightarrow i',k\rightarrow j'\),使得 \(fre\) 凭空多了一点流量,且每个节点的匹配与否的状态无变化。因此,每当连边 \(i\rightarrow j'\) 时,都要判断 \(i'\) 是否已经匹配,或者 \(j\) 是否已经匹配,如果是的话就可以调整了。

    并且,调整可能还不是一次性的。还是上文的例子,当连边 \(k\rightarrow j'\) 时,仍有可能出现 \(k'\) 匹配或是 \(j\) 匹配的情形,也要递归下去处理;甚至,还会出现 \(k\rightarrow j'\) 任意流流成了绑定流的例子,也要判掉。

    于是综上起来,这里的 Adjust 函数就可以递归地处理上述操作,其中 mat,mbt 分别是左部的匹配对象,右部的匹配对象。

    void Adjust(int x,int y){//try to adjust the FREE-FLOW (x,y)
    	if(x==y){fre++;return;}//no need to adjust.
    	if(mbt[x]){mat[mbt[x]]=y,mbt[y]=mbt[x],mat[x]=mbt[x]=x,fre++,Adjust(mbt[y],y);return;}
    	if(mat[y]){mbt[mat[y]]=x,mat[x]=mat[y],mat[y]=mbt[y]=y,fre++,Adjust(x,mat[x]);return;}
    }
    

    最后的一个例子,是某些堆空掉的情形。此时,对应的方案就不合法,不能予以选择。

    时间复杂度 \(O(n\log n)\)

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    int T,n,m,fre,a[200100],b[200100],mat[200100],mbt[200100];
    ll res;
    priority_queue<pair<int,int> >as,bs,am,bm,o;//s:spare nodes; m:parterner matched,self spare nodes; o:pairs.
    void Adjust(int x,int y){//try to adjust the FREE-FLOW (x,y)
    	if(x==y){fre++;return;}//no need to adjust.
    	if(mbt[x]){mat[mbt[x]]=y,mbt[y]=mbt[x],mat[x]=mbt[x]=x,fre++,Adjust(mbt[y],y);return;}
    	if(mat[y]){mbt[mat[y]]=x,mat[x]=mat[y],mat[y]=mbt[y]=y,fre++,Adjust(x,mat[x]);return;}
    }
    void clear(priority_queue<pair<int,int> >&q){while(!q.empty())q.pop();}
    int main(){
    	scanf("%d",&T);
    	while(T--){
    		scanf("%d%d%d",&n,&m,&fre),fre=m-fre,res=0;
    		clear(as),clear(bs),clear(am),clear(bm),clear(o);
    		for(int i=1;i<=n;i++)scanf("%d",&a[i]),as.push(make_pair(a[i],i)),mat[i]=0;
    		for(int i=1;i<=n;i++)scanf("%d",&b[i]),bs.push(make_pair(b[i],i)),mbt[i]=0;
    		for(int i=1;i<=n;i++)o.push(make_pair(a[i]+b[i],i));
    //		printf("%d %d %d %d %d\n",as.size(),bs.size(),am.size(),bm.size(),o.size());
    		while(m--){
    //			printf("%d:%d\n",m,fre);
    //			for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts("");
    //			for(int i=1;i<=n;i++)printf("%d ",mbt[i]);puts("");
    			while(!o.empty()&&(mat[o.top().second]||mbt[o.top().second]))o.pop();
    			while(mat[as.top().second])as.pop();
    			while(mbt[bs.top().second])bs.pop();
    			while(!am.empty()&&(mat[am.top().second]||!mbt[am.top().second]))am.pop();
    			while(!bm.empty()&&(!mat[bm.top().second]||mbt[bm.top().second]))bm.pop();
    			if(fre){
    				int x=as.top().second,y=bs.top().second;as.pop(),bs.pop();
    				res+=a[x]+b[y];
    				mat[x]=y,mbt[y]=x,fre--;
    				if(!mbt[x])bm.push(make_pair(b[x],x));
    				if(!mat[y])am.push(make_pair(a[y],y));
    //				printf("FRE:%d %d\n",x,y);
    				Adjust(x,y);
    				continue;
    			}else{
    				int t1=0;if(!o.empty())t1=o.top().first;
    				int t2=0;if(!am.empty())t2=am.top().first+bs.top().first;
    				int t3=0;if(!bm.empty())t3=bm.top().first+as.top().first;
    //				printf("SIT:%d %d %d\n",t1,t2,t3);
    				if(t1>=t2&&t1>=t3){
    //					printf("OO:%d\n",o.top().second);
    					res+=t1;
    					mat[o.top().second]=mbt[o.top().second]=o.top().second;
    					o.pop();
    					continue;
    				}
    				if(t2>=t3){
    					int x=am.top().second,y=bs.top().second;am.pop(),bs.pop();
    //					printf("AM:%d %d\n",x,y);
    					res+=t2;
    					int z=mbt[x];
    					mat[z]=y,mbt[y]=z;
    					mat[x]=mbt[x]=x;
    					if(!mat[y])am.push(make_pair(a[y],y));
    					Adjust(z,y);
    				}else{
    					int x=as.top().second,y=bm.top().second;as.pop(),bm.pop();
    //					printf("BM:%d %d\n",x,y);
    					res+=t3;
    					int z=mat[y];
    					mbt[z]=x,mat[x]=z;
    					mat[y]=mbt[y]=y;
    					if(!mbt[x])bm.push(make_pair(b[x],x));
    					Adjust(x,z);
    				}
    			}
    		}
    //		for(int i=1;i<=n;i++)printf("%d ",mat[i]);puts("");
    //		for(int i=1;i<=n;i++)printf("%d ",mbt[i]);puts("");
    		printf("%lld\n",res);
    	}
    	return 0;
    }
    

    CXXV.UOJ#455. 【UER #8】雪灾与外卖

    题意:有 \(n\) 只老鼠和 \(m\) 个洞,老鼠分别位于 \(x_i\),洞分别位于 \(y_j\),最多能塞下 \(c_j\) 只老鼠,且塞一只老鼠的代价是 \(w_j\)。一只老鼠从位置 \(i\) 移动到位置 \(j\) 需要花费额外的代价 \(|i-j|\)。求使所有老鼠有洞进的最小代价。

    考虑从左往右遍历每只老鼠和每个洞,计算匹配。

    对于老鼠:

    1. 与之前的一个洞匹配。
    2. 抢走之前某只老鼠的洞。

    自然,老鼠也可以留着匹配以后的洞。但是为了避免出现老鼠配不上洞的情形,我们强制老鼠必须配上一个洞。但万一真的没有剩的洞了 呢?那就让它配上一个代价为 \(\infty\) 的虚洞,这样子就一定会被接下来的某个洞抢走。

    对于洞:

    1. 抢走之前某只洞的老鼠。
    2. 等待后面的某只老鼠与其匹配。

    这里发现洞的操作中没有直接与老鼠匹配的选项,因为老鼠都已经有匹配了(可能匹配的是虚洞)。

    具体而言,我们分别对老鼠和洞各维护一个小根堆。

    对于老鼠操作1和2,都直接与洞堆顶匹配即可。若洞堆顶的元素是 \(cost\),则答案增加 \(cost+x\),然后洞堆 pop。同时,为了方便退流,要同时再往老鼠堆中扔入一个 \(-cost-x\)。退流操作支持了洞操作1。

    可能有人会问,对于老鼠操作2,那个被抢走洞的老鼠不需要配对吗?但是我们发现,有一个显然的性质是老鼠与洞间的连线不可能交叉(不然可以把叉打开使得答案严格不增),因此一对老鼠与洞不会同时被抢走。故这个情形只需在洞的操作中处理即可。

    对于洞操作1,就直接与老鼠堆顶匹配即可。若老鼠堆顶的元素是 \(cost\),则答案增加 \(cost+w+y\)。但是需要注意的是,当 \(cost+w+y\geq0\) 时,抢了还不如不抢,因此此时就可以直接跳过操作1,执行操作2。同时,为了方便退流,还要往洞堆里扔入 \(-(cost+w+y)+w-y\)。退流操作支持了老鼠操作2。

    同时,洞操作1还要兼顾我们上文没有进行的操作,即,那个被抢走老鼠的洞还需要配对。我们不选择在抢的洞处统计,而选择在被抢的洞处统计。因此,每执行一个洞操作1,我们同时还要往老鼠堆中扔入 \(-w-y\),用来表示这个洞的老鼠被抢走了。

    对于洞操作2,就直接往洞堆里扔入 \(w-y\) 即可。洞操作2支持了老鼠操作1。

    但是我们发现这样搞总操作数是 \(O(\sum c)\) 级别的,不可能通过。但是,发现因为有着“一对老鼠与洞不会同时被抢走”的条件,所以如果能够使得每对只会被处理一次,复杂度就是正确的 \(O(n)\)

    于是,我们发现对于 \(-w-y\) 的项与 \(w-y\) 的项,其对于同一个洞的所有位置都是相等的。于是我们堆中元素需要两维,一维是值,一维是出现次数,即可将复杂度优化到均摊 \(O(n)\)。总复杂度 \(O(n\log n)\)

    如果要修改堆顶的元素的值可以使用 mutable 关键字。

    代码:

    #include<bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    int n,m,px[100100],py[100100],fod[100100],cst[100100],all;
    ll res;
    struct dat{ll v;mutable int t;dat(ll V,int T){v=V,t=T;}friend bool operator<(const dat&x,const dat&y){return x.v>y.v;}};
    priority_queue<dat>mou,hol;
    int main(){
    	scanf("%d%d",&n,&m);
    	for(int i=1;i<=n;i++)scanf("%d",&px[i]);
    	for(int i=1;i<=m;i++)scanf("%d%d%d",&py[i],&cst[i],&fod[i]),fod[i]=min(fod[i],n),all+=fod[i],all=min(all,n);
    	if(all<n){puts("-1");return 0;}
    	hol.emplace(0x3f3f3f3f3f,0x3f3f3f3f);
    	for(int i=1,j=1;i<=n||j<=m;){
    		if(i<=n&&(px[i]<=py[j]||j>m)){
    			ll c=hol.top().v+px[i];
    			if(!--hol.top().t)hol.pop();
    			res+=c,mou.emplace(-c-px[i],1);
    			i++;
    		}else{
    			int tot=0;
    			while(fod[j]&&!mou.empty()){
    				ll c=(mou.top().v+cst[j]+py[j]);
    				if(c>=0)break;
    				int mn=min(mou.top().t,fod[j]);
    				res+=c*mn,hol.emplace(-c+cst[j]-py[j],mn);
    				fod[j]-=mn,tot+=mn;
    				if(!(mou.top().t-=mn))mou.pop();
    			}
    			if(fod[j])hol.emplace(cst[j]-py[j],fod[j]);
    			if(tot)mou.emplace(-cst[j]-py[j],tot);
    			j++;
    		}
    	}
    	printf("%lld\n",res);
    	return 0;
    }
    
  • 相关阅读:
    ubuntu 16.04 安装 python selenium
    DNS 小问题
    Ubuntu下安装setuptools
    ubuntu16.04LTS更换阿里源
    Ubuntu下安装 Phantomjs
    root和user切换
    Navicat破解安装教程
    urllib2
    MySQL划重点-查询-聚合-分组
    vi编辑器
  • 原文地址:https://www.cnblogs.com/Troverld/p/14621397.html
Copyright © 2011-2022 走看看