• [JZOJ2701] 【GDKOI2012模拟02.01】矩阵


    题目

    在这里插入图片描述

    题目大意

    给你一个矩阵,然后你需要构造一个等长等宽的矩阵,其中矩阵的每一个数字都有一个固定的范围[L,R][L,R]
    然后要使得行的和之间的差,列的和之间的差,的最大值最小。


    思考历程

    一开始见到这题时,我的心情是崩溃的。
    为什么呢?因为这题似乎暴力都不能做……
    感觉上一定有什么特别的结论或者是规律,但是时间不是很充足,所以我还是没有想出来。
    比赛之后……
    老王跑进小机房,大声地喊道:“这题可以网络流!!!”
    诶,网络流?
    比赛时的确没有往这个方向考虑,然后考虑的一下……并没有卵用啊……


    正解

    的确是网络流……
    当我们见到不会做的题目的时候,应该往这三个方向考虑:DP、贪心、网络流。
    这题的数据范围似乎不大(实际上是放屁!),好像不能DP,贪心又想不出来……
    于是我们选择网络流!

    首先,看到这题最大值最小,果断二分!
    我们二分一个答案ansans,现在问题转换成了构造一个矩阵,使得行、列的差都小于ansans
    首先我们判定这时的ansans是否存在解。
    有一个神奇的方法:对于每一行、每一列,我们都可以得到一个取值范围。我们分别通过行、列,得到整个矩阵的取值范围。然后我们看看用两种方式得出来的取值范围是否有交集,如果有,那么一定能够出解,否则不能。
    具体怎么证明……

    勉强感性理解一下吧。我们有了这个取值范围,那么在这个取值范围里面一定有一种可行的方案。这个东西可以用脑子强行构造出来。所以说,如果行和列有交集,那么就有可行的方案了……好草率……

    其实第一次切这题的时候根本就没有这样判断,在每次判断时我直接跑可行流,速度奇慢,不过还是过了(比加了这个优化的老王还快,嘿嘿嘿)
    求出了第一问的答案,现在我们考虑如何构造这个矩阵。
    然后这题我们要用有上下界的网络流来做。
    先说说怎么建模:
    对于每个行和每个列,我们都建立一个节点,并且还要设立源点SS,汇点TT
    对于行和列uuvv,我们建立一条从uuvv的边,容量设为[L,R][L,R]
    对于每个行uu,我们建立一条从SSuu的边,容量设为[rsumuans,rsumu+ans][rsum_u-ans,rsum_u+ans]。其中rsumrsum表示这一行的和。
    对于每个列vv,我们建立一条从vvVV的边,容量设为[csumvans,csumv+ans][csum_v-ans,csum_v+ans]。其中csumcsum表示这一列的和。
    (如果下界小于00,那我们就将就将下界设为00
    然后我们对这个图跑一遍上下界的网络流,
    答案就是每一行和每一列之间的流量了。

    具体怎么理解,其实我觉得,理解网络流一般都是用感性理解。
    首先,每一个点只会影响一行一列,所以对于每行每列之间建立一条边,就代表这个点。
    我们要使得每行的差、每列的差都小于ansans,所以每行、每列的取值范围就出来了。当然由于流量不会是负数,所以我们将负数补为00
    所以我们跑一遍可行流就可以了。

    然后就是一个非常非常重要的问题,可行流怎么跑呢?
    先说无源无汇的上下界可行流
    对于一条弧(u,v)(u,v),假设它的流量范围是[a,b][a,b]
    我们希望它的流量在这个范围,所以流量至少为aa。所以我们先强制把它们的流量变成aa,那么残余的流量是bab-a
    然后我们发现这么简单粗暴肯定是不可以的,因为我们都知道,网络流有流量平衡,就是流入量等于流出量。
    在建图的时候,我们将每一条弧的流量设为上界和下界的差,相当于已经强制性地将下界的流量流到了过去。
    那么我们考虑添加一些附加弧。对于一个点xx,我们定义dxd_x表示所有流入量减流出量
    上边在建图的时候,对于边(u,v)(u,v)范围[a,b][a,b],则d[u]-=a,d[v]+=a
    然后,在粗暴地将这些弧塞流量过后,枚举xx
    如果dx>0d_x>0,则有流量流入xx,所以从ssssxx建立一条容量为dxd_x的弧。
    如果dx<0d_x<0,则有流量流出xx,所以从xxtttt建立一条容量为dx-d_x的弧。
    其中sssstttt分别为虚源点和虚汇点,不要和SSTT混淆!
    然后我们就从ssss开始,跑一遍最大流,如果每一条附加边都满流,那么此图存在可行流。
    跑完最大流之后,如果存在可行流,每一条边的实际流量为它此时的流量加上下界的流量。
    再说有源有汇上下界可行流
    大部分的其实差不多,但我们要另外注意,SSTT是不满足流量平衡的!
    如何解决这个问题?直接从TTSS连一条容量为无限大的边。
    那么就转换成了之前的问题。
    跑最大流的时候还是从ssss开始。

    然后就是诡异的时间复杂度……
    众所周知,网络流的时间复杂度一直都很玄学。所以网络流基本没用复杂度可言。
    看看这题,一共有4040040400条边,有402402的点,并且这只是原图上的。
    我们还要转换模型来求上下界网络流呢!
    所以边的数量超多,不过由于网络流玄学的复杂度,我还是以非常优秀的时间跑过了。


    代码

    using namespace std;
    #include <cstdio>
    #include <cstring>
    #include <algorithm>
    #define N 200
    #define M 200
    int n,m;
    int a[N+1][M+1];
    int rsum[N+1],csum[M+1];
    int L,R;
    struct EDGE{
    	int to,c;
    	EDGE *las;
    } e[1000001];
    int ne;
    EDGE *last[N+M+2+2+10];
    inline void link(int u,int v,int c){
    	e[++ne]={v,c,last[u]};
    	last[u]=e+ne;
    }
    #define rev(ei) (e+(int((ei)-e)^1))
    int ss,tt,S,T,nr[N+1],nc[M+1];//nr表示列的编号,nc表示行的编号
    int in[N+M+2+2+10]; 
    EDGE *p[N+1][M+1];//表示某行某列对应的边
    inline int sap(int,int);
    inline void work(int);
    inline bool ok(int);
    int main(){
    //	freopen("in.txt","r",stdin);
    	scanf("%d%d",&n,&m);
    	for (int i=1;i<=n;++i)
    		for (int j=1;j<=m;++j)
    			scanf("%d",&a[i][j]),rsum[i]+=a[i][j],csum[j]+=a[i][j];
    	scanf("%d%d",&L,&R);
    	for (int i=1;i<=n;++i)
    		nr[i]=i;
    	for (int i=1;i<=m;++i)
    		nc[i]=n+i;
    	S=n+m+1,T=n+m+2;
    	ss=n+m+3,tt=n+m+4;
    	int l=0,r=1000000000,res=-1;
    	while (l<=r){
    		int mid=l+r>>1;
    		if (ok(mid))
    			r=(res=mid)-1;
    		else
    			l=mid+1;
    	}
    	printf("%d
    ",res);
    	work(res);
    	for (int i=1;i<=n;++i){
    		for (int j=1;j<=m;++j)
    			printf("%d ",p[i][j]->c+L);
    		printf("
    ");
    	}
    	return 0;
    }
    EDGE *cur[N+M+2+2+10];
    int gap[N+M+2+2+10],h[N+M+2+2+10];
    bool BZ;
    inline int sap(int x,int s){//sap求最大流,不解释
    	if (x==tt)
    		return s;
    	int have=s;
    	for (EDGE *ei=cur[x];ei;ei=ei->las){
    		cur[x]=ei;
    		if (h[x]==h[ei->to]+1 && ei->c){
    			int t=sap(ei->to,min(ei->c,have));
    			have-=t,ei->c-=t,rev(ei)->c+=t;
    			if (!have)
    				return s;
    		}
    	}
    	cur[x]=last[x];
    	if (!--gap[h[x]])
    		BZ=0;
    	++h[x];
    	++gap[h[x]];
    	return s-have;
    }
    inline void work(int lim){
    	ne=-1;
    	memset(last,0,sizeof last);
    	memset(in,0,sizeof in);
    	for (int i=1;i<=n;++i){
    		int down=max(rsum[i]-lim,0),up=rsum[i]+lim;
    		link(S,nr[i],up-down),link(nr[i],S,0);
    		in[S]-=down,in[nr[i]]+=down;
    	}
    	for (int i=1;i<=m;++i){
    		int down=max(csum[i]-lim,0),up=csum[i]+lim;
    		link(nc[i],T,up-down),link(T,nc[i],0);
    		in[nc[i]]-=down,in[T]+=down;
    	}
    	for (int i=1;i<=n;++i)
    		for (int j=1;j<=m;++j){
    			link(nr[i],nc[j],R-L),link(nc[j],nr[i],0);
    			in[nr[i]]-=L,in[nc[j]]+=L;
    			p[i][j]=e+ne;
    		}
    	link(T,S,1000000000),link(S,T,0);
    	for (int i=1;i<=n+m+2;++i)
    		if (in[i]<0)
    			link(i,tt,-in[i]),link(tt,i,0);
    		else if (in[i]>0)
    			link(ss,i,in[i]),link(i,ss,0);
    	memset(gap,0,sizeof gap);
    	memset(h,0,sizeof h);
    	gap[0]=n+m+4;
    	memset(cur,0,sizeof cur);
    	BZ=1;
    	while (BZ)
    		sap(ss,1000000000);//最大流没有什么卵用,所以我们只需要得到跑完最大流之后的残余网络就行了
    }
    inline bool ok(int lim){
    	int rl=0,rr=0,cl=0,cr=0;//分别为通过行、通过列得出来的范围
    	for (int i=1;i<=n;++i){
    		int l=max(rsum[i]-lim,m*L),r=min(rsum[i]+lim,m*R);//不用和0比较,因为m*L大于等于0.下同
    		if (l>r)
    			return 0;
    		rl+=l,rr+=r;
    	}
    	for (int i=1;i<=m;++i){
    		int l=max(csum[i]-lim,n*L),r=min(csum[i]+lim,n*R);
    		if (l>r)
    			return 0;
    		cl+=l,cr+=r;
    	}
    	if (rr<cl || cr<rl)
    		return 0;
    	return 1;
    }
    

    总结

    还是那句话,如果有做不出来的题目,想一想DP、贪心、网络流。
    如果是构造,DP一般是不行的,所以考虑贪心和网络流。
    在数据范围比较小的时候,就可以考虑一下网络流是否可做。
    当然,网络流的时间复杂度很玄学,有时候会误导你对时间的判断。
    比如这题,边数还是蛮多的……不过跑得挺快,或许是因为图本身的结构吧……层次特别少的那种。
    还有,我终于会上下界网络流了!得意

  • 相关阅读:
    迷 宫
    车厢调度
    快速幂
    2804 最大最小数质因数
    3022 西天收费站
    2291 糖果堆
    1464 装箱问题 2
    Exists/In/Any/All/Contains操作符
    window.onscroll
    zIndex 属性设置元素的堆叠顺序。
  • 原文地址:https://www.cnblogs.com/jz-597/p/11145247.html
Copyright © 2020-2023  润新知