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;
}
View Code

 

 

转载于:https://www.cnblogs.com/wd-one/p/4454549.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述 给出一个$n\times m$的矩阵,每个位置上有一个非负整数,代表这个位置的海拔高度。一开始时,有一个人站在其中一个位置上。这个人可以向上、下、左、右四个方向移动,但是只能移动到海拔高度比当前位置低或者相等的位置上。一次移动只能移动一个单位长度。定义一个位置为“山顶”,当且仅当从这个位置开始移动,可以一直走到海拔高度比它低的位置上。请问,这个矩阵中最多有多少个“山顶”? 输入格式 第一行两个整数,分别表示$n$和$m$。 接下来$n$行,每行$m$个整数,表示整个矩阵。 输出格式 输出一个整数,表示最多有多少个“山顶”。 样例输入 4 4 3 2 1 4 2 3 4 3 5 6 7 8 4 5 6 7 样例输出 5 算法1 (递归dp) $O(nm)$ 对于这道题,我们可以使用递归DP来解决,用$f(i,j)$表示以$(i,j)$为起点的路径最大长度,那么最后的答案就是所有$f(i,j)$中的最大值。 状态转移方程如下: $$ f(i,j)=\max f(x,y)+1(x,y)是(i,j)的下一个满足条件的位置 $$ 注意:这里的状态转移方程中的$x,y$是在枚举四个方向时得到的下一个位置,即: - 向上:$(i-1,j)$ - 向下:$(i+1,j)$ - 向左:$(i,j-1)$ - 向右:$(i,j+1)$ 实现过程中需要注意以下几点: - 每个点都需要搜一遍,因此需要用双重for循环来枚举每个起点; - 对于已经搜索过的点,需要用一个数组$vis$来记录,防止重复搜索; - 在进行状态转移时,需要判断移动后的点是否满足条件。 时间复杂度 状态数为$O(nm)$,每个状态转移的时间复杂度为$O(1)$,因此总时间复杂度为$O(nm)$。 参考文献 C++ 代码 算法2 (动态规划) $O(nm)$ 动态规划的思路与递归DP类似,只不过转移方程和实现方式有所不同。 状态转移方程如下: $$ f(i,j)=\max f(x,y)+1(x,y)是(i,j)的下一个满足条件的位置 $$ 注意:这里的状态转移方程中的$x,y$是在枚举四个方向时得到的下一个位置,即: - 向上:$(i-1,j)$ - 向下:$(i+1,j)$ - 向左:$(i,j-1)$ - 向右:$(i,j+1)$ 实现过程中需要注意以下几点: - 每个点都需要搜一遍,因此需要用双重for循环来枚举每个起点; - 对于已经搜索过的点,需要用一个数组$vis$来记录,防止重复搜索; - 在进行状态转移时,需要判断移动后的点是否满足条件。 时间复杂度 状态数为$O(nm)$,每个状态转移的时间复杂度为$O(1)$,因此总时间复杂度为$O(nm)$。 参考文献 C++ 代码

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值