hdu4975 A simple Gaussian elimination problem.(正确解法 最大流+删边判环)(Updated 2014-10-16)

时间:2023-07-27 21:39:20

这题标程是错的,网上很多题解也是错的。

http://acm.hdu.edu.cn/showproblem.php?pid=4975

2014 Multi-University Training Contest 10

A simple Gaussian elimination problem.

Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)
Total Submission(s): 458    Accepted Submission(s): 153

Problem Description
Dragon is studying math. One day, he drew a table with several rows
and columns, randomly wrote numbers on each elements of the table. Then
he counted the sum of each row and column. Since he thought the map will
be useless after he got the sums, he destroyed the table after that.

However Dragon's mom came back and found what he had done. She would
give dragon a feast if Dragon could reconstruct the table, otherwise
keep Dragon hungry. Dragon is so young and so simple so that the
original numbers in the table are one-digit number (e.g. 0-9).

Could you help Dragon to do that?

Input
The first line of input contains only one integer, T(<=30), the
number of test cases. Following T blocks, each block describes one test
case.

There are three lines for each block. The first line
contains two integers N(<=500) and M(<=500), showing the number of
rows and columns.

The second line contains N integer show the sum of each row.

The third line contains M integer show the sum of each column.

Output
Each output should occupy one line. Each line should start with "Case
#i: ", with i implying the case number. For each case, if we cannot get
the original table, just output: "So naive!", else if we can
reconstruct the table by more than one ways, you should output one line
contains only: "So young!", otherwise (only one way to reconstruct the
table) you should output: "So simple!".
Sample Input
3
1 1
5
5
2 2
0 10
0 10
2 2
2 2
2 2
Sample Output
Case #1: So simple!
Case #2: So naive!
Case #3: So young!
Source
Recommend
hujie   |   We have carefully selected several similar problems for you:  4980 4979 4978 4977 4975

题意:有n行m列矩阵,给出各行的和、各列的和,矩阵元素需要为0~9,判断无解、唯一解、多解。

题解:网络流+判环。

和hdu4888几乎一模一样,hdu4888题解链接:http://www.cnblogs.com/yuiffy/p/3891639.html

这题的做法就是hdu4888的做法,点上面的链接怒看如何建图,我下面说这题的不同之处。网上很多题解都是错的,标程也是错的。

这题标程有错,但没有反例的数据,所以数据还是对的。但标程使用的错误方法使它运行时间巨短,导致这题时限开得少,直接拿超碉优化的网络流+简单dfs判环也是过不了的。

正确做法应该是用更好的判环方法,我想到的是 dfs判环回溯时删边(不是删点),代码如下:

 bool dfs(const int &x,const int &prex) {///深搜判环
int biu=-;
walked[x]=true;
for (int i=head[x]; i!=-; i=e[i].next) {
if(e[i].v==prex){
biu=i;
continue;
}
if (e[i].cap>) {
if(walked[e[i].v]) return true;
if(dfs(e[i].v,x)) return true;
}
if(biu==-) head[x]=e[i].next;///删边,因为这条边为0或者走了这条边却没发现环
else e[biu].next=e[i].next;
biu=i;
}
walked[x]=false;
return false;
}

可以想到,如果选择一条边递归,然后从递归里出来了,说明没找到环,那么走这条边肯定找不到环,以后也不用走了,可以把这条边删了。或者这条边流量为0,也可以删了。

这样的话每条边最多只进一次,O(m)。

其实4888也可以用这种方法,不过4888还要输出结果,所以先记录结果再删。

然后我说一下网上常见的错误解法,因为没有反例数据,这种错误解法可以ac。

错误的dfs判环:

 bool walked[maxn];
bool dfs(const int &x,const int &preE) {
int pp=preE^;
for (int &i=head[x]; i!=-; i=e[i].next) {
if(i==(pp))continue;
if (e[i].cap>) {
if(walked[e[i].v]) return true;
walked[e[i].v]=true;
if(dfs(e[i].v,i)) return true;
walked[e[i].v]=false;
}
}
return false;
}

网上很多地方都说这个int &i是个优化,加了这个优化就能过。其实这不是优化,这是乱搞,碰巧没有反例数据。

int &i=head[x]的意思是把i当成head[x]的引用,也就是i其实就是head[x]。

这样,i=e[i].next相当于head[x] = e[head[x]].next,是会改变head[x]的,也就是把上一条出边给扔了!这和我上面说的删边不一样,我上面的是只删递归过的边,而这个会把不能走的边也删掉。这题里不能走的边就是直接回头的那条边。

这样会发生什么情况呢?我来画个图:

hdu4975 A simple Gaussian elimination problem.(正确解法 最大流+删边判环)(Updated 2014-10-16)

1和3、2和3、2和4之间有双向边,而1和4之间只有单向边。当深搜1->3->2->4时,会把4的出边全扔了。本来我们应该找到的环是1->4->2->3->1,可是4的出边已经没了,找不到这个环。

由于大家建图的方法、网络流的方法、dfs的顺序不一致,所以这题不好找到对所有代码都成立的反例输入。如果谁发现好的样例的话希望能写出来,怒艹错误解法。

这个错解有人说是比赛时试出来的;还有人比赛时试验只在m,n<=200时判多解,居然正好也能A…怕了……还是希望以后出题人能认真一点,弄好标程和数据。

再说一下标程的错误解法:标程也是回溯时删东西,不过不是删边是删点……点和你无冤无仇,为什么删它?搜完一个点所有出边可不能保证这个点就没用了…就像我上面给的例子的点4。

代码:

 //#pragma comment(linker, "/STACK:102400000,102400000")
#include<cstdio>
#include<cmath>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<map>
#include<set>
#include<stack>
#include<queue>
using namespace std;
#define ll long long
#define usll unsigned ll
#define mz(array) memset(array, 0, sizeof(array))
#define minf(array) memset(array, 0x3f, sizeof(array))
#define REP(i,n) for(i=0;i<(n);i++)
#define FOR(i,x,n) for(i=(x);i<=(n);i++)
#define RD(x) scanf("%d",&x)
#define RD2(x,y) scanf("%d%d",&x,&y)
#define RD3(x,y,z) scanf("%d%d%d",&x,&y,&z)
#define WN(x) prllf("%d\n",x);
#define RE freopen("D.in","r",stdin)
#define WE freopen("1biao.out","w",stdout)
#define mp make_pair
#define pb push_back int ans;
int r[],co[];
int k,nr,nc;
int sumr,sumc;
const int maxn=;//点数
const int maxm=;//边数
const int inf=;//MAXINT
struct vnode {
int v,next;
int cap;
};
int cnt,head[maxn];
int h[maxn],g[maxn],d[maxn];//g[i]为标号为i的结点个数,h[i]为i结点的标号,d[]当前弧优化,记录当前弧
bool found;
int n,m,st,ed;//n个点m条边
int augc,flow;//augc为增广路容量,flow为最大流
vnode e[maxm]; inline void add(const int &x,const int &y,const int &z) {
e[cnt].v=y;
e[cnt].cap=z;
e[cnt].next=head[x];
head[x]=cnt;
cnt++; e[cnt].v=x;
e[cnt].cap=;
e[cnt].next=head[y];
head[y]=cnt;
cnt++; } bool walked[maxn];
bool dfs(const int &x,const int &prex) {///深搜判环
int biu=-;
walked[x]=true;
for (int i=head[x]; i!=-; i=e[i].next) {
if(e[i].v==prex)continue;
if (e[i].cap>) {
if(walked[e[i].v]) return true;
if(dfs(e[i].v,x)) return true;
}
if(biu==-) head[x]=e[i].next;///删边,因为这条边为0或者走了这条边却没发现环
else e[biu].next=e[i].next;
biu=i;
}
walked[x]=false;
return false;
} void aug(const int &m) {
int mini,minh=n-;
int augco=augc;
int &i=d[m];///当前弧优化
if (m==ed) { //如果当前结点为汇点
found=true;
flow+=augc; //增加流量
return;
}
for (; i!=-; i=e[i].next) { //寻找容许边
//printf("m=%d,i=%d,e[i].v=%d,e[i].cap=%d,e[i].next=%d\n",m,i,e[i].v,e[i].cap,e[i].next);
//getchar();
if (e[i].cap && h[e[i].v]+==h[m]) { //如果残留容量大于0,如果是容许边
if (e[i].cap < augc) augc=e[i].cap;//如果容许边流量小于当前增广路流量 则更新增广路流量
//d[m]=i; //把i定为当前弧
aug(e[i].v); //递归
if (h[st]>=n) return; //GAP 如果源点距离标号大于n 则停止算法
if (found) break; //如果找到汇点 则退出寻找
augc=augco;//没找到就还原当前的流
}
}
if (!found) { //重标号
for (int i=head[m]; i!=-; i=e[i].next) //找那个标号,这里不能用d[m]开始,不然会蛋疼
if (e[i].cap && h[e[i].v]<minh) {
minh=h[e[i].v];
mini=i;
}
g[h[m]]--; //GAP 距离为
if (!g[h[m]]) h[st]=n; //GAP
h[m]=minh+;
d[m]=mini;
g[h[m]]++; //GAP
} else {
//修改残量
e[i].cap-=augc;
e[i^].cap+=augc;
}
} inline void farm() {
int i,j,x,y,z;
memset(head,-,sizeof(head));
cnt=;
n=nc+nr+;
st=n-;
ed=n;
for(i=; i<=nc; i++)
add(st,i,co[i]);
for(i=; i<=nr; i++)
add(nc+i,ed,r[i]);
for(i=; i<=nc; i++)
for(j=; j<=nr; j++)
add(i,j+nc,k);
memset(h,,sizeof(h));
memset(g,,sizeof(g));
g[]=n;
flow=;
for(i=; i<=n; i++)
d[i]=head[i];//当前弧初始化
while(h[st]<n) {
augc=inf;//初始化增广路容量为正无穷大
found=false;
aug(st);//从源点开始找
}
if(flow!=sumr) {
ans=;
return;
}
memset(walked,false,sizeof(walked));
for(i=; i<=nr; i++) { ///因为建图的特性,有环的话环肯定包含1~nr中的点,也就是表示行的结点
if(dfs(i,-)) {
ans=;
return;
}
}
ans=;
//printf("%d\n",flow);
} inline void read(int &a) {///读入优化
char ch;
bool flag = false;
a = ;
while(!((((ch = getchar()) >= '') && (ch <= '')) || (ch == '-')));
if(ch != '-') {
a *= ;
a += ch - '';
} else {
flag = true;
}
while(((ch = getchar()) >= '') && (ch <= '')) {
a *= ;
a += ch - '';
}
if(flag) {
a = -a;
}
} int main() {
//RE;
//WE;
int i,j,cas=,t;
scanf("%d",&t);
while(t--) {
scanf("%d%d",&nr,&nc);
k=;
sumr=;
sumc=;
for(i=; i<=nr; i++) {
//scanf("%d",&r[i]);
read(r[i]);
sumr+=r[i];
}
for(i=; i<=nc; i++) {
//scanf("%d",&co[i]);
read(co[i]);
sumc+=co[i];
}
ans=;
if(sumr==sumc)farm();
if(ans==) printf("Case #%d: So naive!\n",cas++);
else if(ans!=) {
printf("Case #%d: So young!\n",cas++);
} else {
printf("Case #%d: So simple!\n",cas++);
}
}
//cout<<"end";
return ;
}

2014-10-16 Update

srm菊苣指出这个居然过不了4888,我试了一下,居然真的wa!

然后发现了一个小错误,if(e[i].v==prex)continue,continue之前没有修改biu。加上biu=i就能过了。

之前居然挂了错的代码这么久,见谅!我逗!