[JZOJ2701] 【GDKOI2012模拟02.01】矩阵
题目
题目大意
给你一个矩阵,然后你需要构造一个等长等宽的矩阵,其中矩阵的每一个数字都有一个固定的范围。
然后要使得行的和之间的差,列的和之间的差,的最大值最小。
思考历程
一开始见到这题时,我的心情是崩溃的。
为什么呢?因为这题似乎暴力都不能做……
感觉上一定有什么特别的结论或者是规律,但是时间不是很充足,所以我还是没有想出来。
比赛之后……
老王跑进小机房,大声地喊道:“这题可以网络流!!!”
诶,网络流?
比赛时的确没有往这个方向考虑,然后考虑的一下……并没有卵用啊……
正解
的确是网络流……
当我们见到不会做的题目的时候,应该往这三个方向考虑:DP、贪心、网络流。
这题的数据范围似乎不大(实际上是放屁!),好像不能DP,贪心又想不出来……
于是我们选择网络流!
首先,看到这题最大值最小,果断二分!
我们二分一个答案,现在问题转换成了构造一个矩阵,使得行、列的差都小于
首先我们判定这时的是否存在解。
有一个神奇的方法:对于每一行、每一列,我们都可以得到一个取值范围。我们分别通过行、列,得到整个矩阵的取值范围。然后我们看看用两种方式得出来的取值范围是否有交集,如果有,那么一定能够出解,否则不能。
具体怎么证明……
勉强感性理解一下吧。我们有了这个取值范围,那么在这个取值范围里面一定有一种可行的方案。这个东西可以用脑子强行构造出来。所以说,如果行和列有交集,那么就有可行的方案了……好草率……
其实第一次切这题的时候根本就没有这样判断,在每次判断时我直接跑可行流,速度奇慢,不过还是过了(比加了这个优化的老王还快,嘿嘿嘿)
求出了第一问的答案,现在我们考虑如何构造这个矩阵。
然后这题我们要用有上下界的网络流来做。
先说说怎么建模:
对于每个行和每个列,我们都建立一个节点,并且还要设立源点,汇点。
对于行和列和,我们建立一条从到的边,容量设为
对于每个行,我们建立一条从到的边,容量设为。其中表示这一行的和。
对于每个列,我们建立一条从到的边,容量设为。其中表示这一列的和。
(如果下界小于,那我们就将就将下界设为)
然后我们对这个图跑一遍上下界的网络流,
答案就是每一行和每一列之间的流量了。
具体怎么理解,其实我觉得,理解网络流一般都是用感性理解。
首先,每一个点只会影响一行一列,所以对于每行每列之间建立一条边,就代表这个点。
我们要使得每行的差、每列的差都小于,所以每行、每列的取值范围就出来了。当然由于流量不会是负数,所以我们将负数补为。
所以我们跑一遍可行流就可以了。
然后就是一个非常非常重要的问题,可行流怎么跑呢?
先说无源无汇的上下界可行流
对于一条弧,假设它的流量范围是。
我们希望它的流量在这个范围,所以流量至少为。所以我们先强制把它们的流量变成,那么残余的流量是。
然后我们发现这么简单粗暴肯定是不可以的,因为我们都知道,网络流有流量平衡,就是流入量等于流出量。
在建图的时候,我们将每一条弧的流量设为上界和下界的差,相当于已经强制性地将下界的流量流到了过去。
那么我们考虑添加一些附加弧。对于一个点,我们定义表示所有流入量减流出量。
上边在建图的时候,对于边范围,则d[u]-=a,d[v]+=a
然后,在粗暴地将这些弧塞流量过后,枚举,
如果,则有流量流入,所以从向建立一条容量为的弧。
如果,则有流量流出,所以从向建立一条容量为的弧。
其中和分别为虚源点和虚汇点,不要和和混淆!
然后我们就从开始,跑一遍最大流,如果每一条附加边都满流,那么此图存在可行流。
跑完最大流之后,如果存在可行流,每一条边的实际流量为它此时的流量加上下界的流量。
再说有源有汇上下界可行流
大部分的其实差不多,但我们要另外注意,和是不满足流量平衡的!
如何解决这个问题?直接从向连一条容量为无限大的边。
那么就转换成了之前的问题。
跑最大流的时候还是从开始。
然后就是诡异的时间复杂度……
众所周知,网络流的时间复杂度一直都很玄学。所以网络流基本没用复杂度可言。
看看这题,一共有条边,有的点,并且这只是原图上的。
我们还要转换模型来求上下界网络流呢!
所以边的数量超多,不过由于网络流玄学的复杂度,我还是以非常优秀的时间跑过了。
代码
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一般是不行的,所以考虑贪心和网络流。
在数据范围比较小的时候,就可以考虑一下网络流是否可做。
当然,网络流的时间复杂度很玄学,有时候会误导你对时间的判断。
比如这题,边数还是蛮多的……不过跑得挺快,或许是因为图本身的结构吧……层次特别少的那种。
还有,我终于会上下界网络流了!得意