POJ1417解题报告

  题意:给出n对人之间的朋友和敌人关系,已知它们一共是两波人,人数分别为p1、p2,问是否存在合理方案?若存在,输出方案。多case问题,以n=0,p1=0,p2=0为结束。
         分析:先并查集做出若干对集合,然后暴搜方案。。
         解题过程:WA
                             |   ①n\p1\p2均为0应为!(n||p1||p2)而不是!(n&&p1&&p2),没有搞清楚;
                             |   ②维护集合大小的sum数组忘了初始化为0。
                            ↓ 
                          TLE
                            |    用后缀和数组优化。③收获:后缀和数组可以做最优化剪枝。
                           ↓
                          AC
代码:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
using namespace std;
typedef int hd;
const hd maxn=2000;
hd fa[maxn],sum[maxn],p,p1,p2,lst[maxn],tot,maxs[maxn],mins[maxn];
inline hd find(hd x){
return fa[x]!=fa[fa[x]]?fa[x]=find(fa[x]):fa[x];
}
bool pd[maxn],flag,ans[maxn],final;
inline void work(hd i,hd sum1,hd sum2){
if(sum1+mins[i]>p1)return;
if(sum2+mins[i]>p2)return;
if(sum1+maxs[i]<p1)return;
if(sum2+maxs[i]<p2)return;
if(sum1==p1&&sum2==p2)
if(flag){
final=1;
return;
}
else{
flag=1;
for(hd i=0;i<tot;++i)
ans[lst[i]]=pd[lst[i]];
return;
}
if(i==tot)return;
pd[lst[i]]=1;
pd[lst[i+1]]=0;
work(i+2,sum1+sum[lst[i]],sum2+sum[lst[i+1]]);
if(final)return;
pd[lst[i]]=0;
pd[lst[i+1]]=1;
work(i+2,sum1+sum[lst[i+1]],sum2+sum[lst[i]]);
}
int main(){
hd n,p21,x,y,i;
char c;
// freopen("POJ1417.in","r",stdin);
scanf("%d%d%d",&n,&p1,&p2);
while(n||p1||p2){
for(i=1,p=p1+p2,p21=2*p+1;i<p21;++i)fa[i]=i;
for(i=1;i<=p;++i)sum[i]=1;
for(;i<p21;++i)sum[i]=0;
flag=final=0;
for(i=0;i<n;++i){
scanf("%d%d",&x,&y);
cin>>c;
if(c=='n'){
scanf("%*c");
if(find(x)!=find(y+p)){
sum[fa[x]]+=sum[fa[y+p]];
fa[fa[y+p]]=fa[x];
}
if(find(y)!=find(x+p)){
sum[fa[y]]+=sum[fa[x+p]];
fa[fa[x+p]]=fa[y];
}
}
else{
scanf("%*c%*c");
if(find(x)!=find(y)){
sum[fa[x]]+=sum[fa[y]];
fa[fa[y]]=fa[x];
}
if(find(x+p)!=find(y+p)){
sum[fa[x+p]]+=sum[fa[y+p]];
fa[fa[y+p]]=fa[x+p];
}
}
}
memset(pd,0,sizeof(pd));
tot=0;
for(i=1;i<=p;++i)
if(!pd[find(i)]){
lst[tot++]=fa[i];
pd[fa[i]]=1;
lst[tot++]=find(i+p);
pd[fa[i+p]]=1;
}
maxs[tot]=mins[tot]=0;
for(i=tot-2;i>-1;i-=2){
maxs[i]=maxs[i+1]=maxs[i+2]+max(sum[lst[i]],sum[lst[i+1]]);
mins[i]=mins[i+1]=mins[i+2]+min(sum[lst[i]],sum[lst[i+1]]);
}
memset(pd,0,sizeof(pd));
memset(ans,0,sizeof(ans));
work(0,0,0);
if(!final&&flag){
for(i=1;i<=p;++i)
if(ans[fa[i]])
printf("%hd\n",i);
printf("end\n");
}
else printf("no\n");
scanf("%d%d%d",&n,&p1,&p2);
}
return 0;
}
进阶:在网上又看了DP的解法。
        是把这当作一个体积等于价值的-1/1背包处理。对于一对集合来说,选且必选其中之一,又因为节点数=p1+p2,所以若一个背包里的总价值等于p1,则另一个背包里的总价值必为p2.所以我们只考虑一个背包就可以了。
        然后我们发现我们要求的其实应该是把容量为p1的背包装满的方案总数,类似0/1背包,我们只需把max改成+就可以了。
即得:f[i][j]=f[i-1][j-sum[a]]+f[i-1][j-sum[b]].f[0][k]=0,0<k<=p1.f[0][0]=1.
然后再开一个pred数组记录一下路径,因为若我们需要输出路径,则必存在f[i][j]=0+1或1+0,所以我们只需要记录1在哪里即可。
        由于sum[i]>=0,所以实际上我们是不能把第一维去掉的,但是我们可以改成滚动数组,这是可行的。但改成滚动数组没想到我烦了一个蛋疼的错误:
        定义时我写的是f[1000][2],但用的时候我用的是f[2][1000].
就这么一个蛋疼的错误花了我两个小时时间检查。。这玩意儿为什么难查呢。。因为它不会越界,交上去是WA,而不是CE,在下面对拍怎么拍都一样。。。
唉。。所以我以后一定要小心这种地方,写代码一定要思路清晰。
代码: 
#include<iostream>
using namespace std;
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
typedef int hd;
const int MAXN=1000;
hd fa[MAXN],sum[MAXN],pred[MAXN][MAXN],f[2][MAXN],lst[MAXN];
bool pd[MAXN];
inline void dfs(hd i,hd j){
if(i)dfs(i-2,j-sum[pred[i][j]]);
pd[pred[i][j]]=1;
}
inline hd find(hd x){
return fa[x]!=fa[fa[x]]?fa[x]=find(fa[x]):fa[x];
}
int main(){
hd n,p1,p2,p,p21,i,x,y,tot,xp,yp,tmp1,tmp2,j;
bool k,pk;
char c;
freopen("POJ1417.in","r",stdin);
scanf("%d%d%d",&n,&p1,&p2);
while(n||p1||p2){
for(i=1,p=p1+p2,p21=(p<<1)+1;i<p21;++i)fa[i]=i;
memset(sum,0,sizeof(sum));
for(i=1;i<=p;++i)sum[i]=1;
for(i=0;i<n;++i){
scanf("%d%d",&x,&y);
cin>>c;
xp=x+p;
yp=y+p;
if(c=='n'){
scanf("%*c");
if(find(x)!=find(yp)){
sum[fa[x]]+=sum[fa[yp]];
fa[fa[yp]]=fa[x];
sum[find(y)]+=sum[find(xp)];
fa[fa[xp]]=fa[y];
}
}
else{
scanf("%*c%*c");
if(find(x)!=find(y)){
sum[fa[x]]+=sum[fa[y]];
fa[fa[y]]=fa[x];
sum[find(xp)]+=sum[find(yp)];
fa[fa[yp]]=fa[xp];
}
}
}
tot=2;
memset(pd,1,sizeof(pd));
for(i=1;i<=p;++i)
if(pd[find(i)]){
pd[fa[i]]=0;
pd[find(i+p)]=0;
lst[tot++]=fa[i];
lst[tot++]=fa[i+p];
}
memset(f,0,sizeof(f));
memset(pd,0,sizeof(pd));
f[0][0]=1;
for(i=2;i<tot;i+=2)
for(k=(i>>1)&1,pk=!k,j=p1,tmp1=j-sum[lst[i]],tmp2=j-sum[lst[i+1]];j>-1;--j,--tmp1,--tmp2){
f[k][j]=(tmp1<0?0:f[pk][tmp1])+(tmp2<0?0:f[pk][tmp2]);
if(tmp1>=0&&f[pk][tmp1])pred[i][j]=lst[i];
else pred[i][j]=lst[i+1];
}
if(f[k][p1]!=1)printf("no\n");
else{
dfs(tot-2,p1);
for(i=1;i<=p;++i)
if(pd[fa[i]])
printf("%d\n",i);
printf("end\n");
}
scanf("%d%d%d",&n,&p1,&p2);
}
return 0;
}
对拍:这次因为实在查不出来错,就请了liangjs和zky学长,liangjs帮我写了个对拍,也算是学了一下,基本会了一些格式了。
要用一些随机生成函数和cstdlib里面的东西。
代码:
------------------duipai.cpp----------------------
 
#include <cstdio>
#include <cstdlib>
#include <process.h>
#include <ctime>
#define MAXN 2003
int n, p1, p2, m;
int f[MAXN], l[MAXN];
/*0:friend 1*/
int find(int x)
{
if (x == f[x])
return x;
int ans = find(f[x]);
l[x] ^= l[f[x]];
f[x] = ans;
return ans;
}
int main()
{
srand(time(0) * _getpid());
int T = 1;
while (T--)
{
m = rand() % 5 + 1;
p1 = rand() % 5, p2 = rand() % 5;
if(!p1&&!p2)p1++;
printf("%d %d %d\n", m, p1, p2);
n = p1 + p2;
for (int i = 0; i < n; ++i)
f[i] = i, l[i] = 0;
while (m--)
{
int u, v, k;
while (1)
{
u = rand() % n, v = rand() % n;
k = rand() % 2;
if (find(u) != find(v))
{
l[f[u]] = k ^ l[u] ^ l[v];
f[f[u]] = f[v];
break;
}
else if (l[u] ^ l[v] == k)
break;
}
printf("%d %d %s\n", u + 1, v + 1, !k ? "yes" : "no");
}
}
printf("0 0 0\n");
return 0;
}
-----------------check.cpp--------------------
#include <cstdlib>
int main()
{
while (1)
{
system("duipai > POJ1417.in");
system("POJ1417 > std.txt");
system("POJ1417(DP) > out.txt");
int f = system("fc out.txt std.txt");
if (f)
break;
}
return 0;
}  
注:
srand(time(0) * _getpid());设一个不相同的随机种子,time(0)是取得当前Unix时间,_getpid()  是取得进程识别码。

综: 几个主要收获是来自DFS和DP。
        ①利用后缀和数组最优化剪枝。
        ②0/1背包→-1/1背包的思路转换,改变背包方程求背包方案总数,DP中路径的记录,都是值得我学习的。

你可能感兴趣的:(dp,poj,DFS,并查集)