poj1417 true liars(并查集 + DP)详解
这个题做了两天了。首先用并查集分类是明白的, 不过判断是否情况唯一刚开始用的是搜索。总是超时。 后来看别人的结题报告, 才恍然大悟判断唯一得用DP.
题目大意:
一共有p1+p2个人,分成两组,一组p1个,一组p2个。给出N个条件,格式如下:
x y yes表示x和y分到同一组
x y no表示x和y分到不同组
问分组情况是否唯一,若唯一则按从小到大顺序输出,否则输出no。保证不存在矛盾条件,但是有可能出现x=y的情况。
题目分析: 题中会给我们一些信息, 告诉我们那些是同一类, 哪些是不同类。 当然刚开始的时候我们无法判断那一类是好人、坏人。 那么我们不妨把有关系的点(无论他们的关系是yes还是 no)全归为一类, 他们有一个相同的父节点。然后用一个数组(relation[])记录他与父节点的关系(0代表同类, 1代表异类)。当然因为所给的只是一部分信息, 所以有可能无法把所有点归为一类(例如:1,2 yes 3,4 yes。只表明1,2同类 , 3,4同类, 1,3的关系并不知道)。那么不妨设几个不同的集合(以父节点为划分标准)每个集合分为两类 ,与父节点同类(relation[] = 0), 与父节点不同类(relation[] = 1)。此时我们问题转变为答案是否唯一。取每个集合中的一种类型,且仅取一种(同类或不同类)。看累计人数得p1的情况是否唯一。
#include<iostream> #include<cstdio> #include<string.h> using namespace std; int n, n1, n2, mi, mx, key, flag, a[605][2], vis[605][605][2], ans[605][2], v[605], d[605][605], pre[610], relation[605]; int find(int x)//寻找最根部的父亲节点 { if(pre[x] == x) return x; else if(pre[x] != x) { int t = pre[x]; pre[x] = find(pre[x]);//边寻找最根部父亲节点,边更新原父亲节点的信息 relation[x] = (relation[t] + relation[x]) % 2;//类似于种类并查集的更新 } return pre[x]; } void work(int a, int b, int c)//合并节点 { int fx = find(a); int fy = find(b); if(fx != fy) { pre[fx] = fy; relation[fx] = (relation[a] + relation[b] + c) % 2; } } void dp() { int k = 1; for(int i = mi+1; i <= (n1+n2); i++) { if(v[i] == 1) { k++; int t1 = ans[i][0]; int t2 = ans[i][1]; int mx = min(t1, t2); for(int j = n1; j >= mx; j--) { d[k][j] = d[k-1][j-t1] + d[k-1][j-t2]; if(d[k-1][j-t1] == 1 && d[k-1][j-t2] == 0) { vis[k][j][0] = i; vis[k][j][1] = 0; } else if(d[k-1][j-t2] == 1 && d[k-1][j-t1] == 0) { vis[k][j][0] = i; vis[k][j][1] = 1; } } } } } int main() { while(scanf("%d%d%d", &n, &n1, &n2) != EOF) { if(n == 0 && n1 == 0 && n2 == 0) break; //初始化所有节点得父节点和relation for(int i = 1; i <= n1+n2; i++){pre[i] = i; relation[i] = 0;} for(int i = 1; i <= n; i++) { int a, b; char c[10]; scanf("%d%d%s", &a, &b, &c); if(strcmp(c, "yes") == 0) work(a, b, 0); else if(strcmp(c, "no") == 0) work(a, b, 1); } //这里注意一下:到这一步,有可能存在一些点的父节点不是最跟的节点 for(int i = 1; i <= n1+n2; i++) int t = find(i); memset(v, 0, sizeof(v)); //ans[i][0]表示与最根节点i同类的节点个数, ans[i][1]代表与最根节点i不同类的节点个数 memset(ans, 0, sizeof(ans)); flag = 0;//存储一共有多少个不同的最跟部的父节点 mi = 10e9; for(int i = 1; i <= n1+n2; i++) { int x = pre[i]; int y = relation[i]; if(x < mi) mi = x; ans[x][y]++; if(v[x] == 0) { v[x] = 1; flag++; } } /*d[i][j]前i个集合中累计人数为j时有多少种可能, vis[i][j][]保存每一个集合选了哪一类, 为最后输出用。 vis[i][j][0]表示前i个集合中累计人数为j时的最根节点,vis[i][j][1] 表示前i个集合中累计人数为j时,最根节点为vis[i][j][0]时,与根节点的关系*/ memset(d, 0, sizeof(d)); memset(vis, 0, sizeof(vis)); d[1][ans[mi][0]]++; d[1][ans[mi][1]]++; vis[1][ans[mi][0]][0] = mi; vis[1][ans[mi][0]][1] = 0; vis[1][ans[mi][1]][0] = mi; vis[1][ans[mi][1]][1] = 1; dp(); if(d[flag][n1] == 1)//如果前flag个集合中累计人数为n1的可能为1时,有唯一解 { int j = n1; for(int i = flag; i >= 1; i--)//从后往前推 { a[i][0] = vis[i][j][0]; a[i][1] = vis[i][j][1];//记录第i个集合中取得是哪一类(同类0, 不同类1) j -= ans[a[i][0]][a[i][1]]; } for(int i = 1; i <= n1+n2; i++) { for(int j = 1; j <= flag; j++) { int f = pre[i]; int ff = relation[i]; if(f == a[j][0] && ff == a[j][1]) printf("%d\n", i); } } printf("end\n"); } else printf("no\n"); } return 0; }