[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\n",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("\n");
	}
	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一般是不行的,所以考虑贪心和网络流。
在数据范围比较小的时候,就可以考虑一下网络流是否可做。
当然,网络流的时间复杂度很玄学,有时候会误导你对时间的判断。
比如这题,边数还是蛮多的……不过跑得挺快,或许是因为图本身的结构吧……层次特别少的那种。
还有,我终于会上下界网络流了!得意

posted @ 2018-12-29 12:19  jz_597  阅读(152)  评论(0编辑  收藏  举报