<算法学习> 小游戏(贪心)

目录

问题描述

问题分析

问题解决


问题描述

谷同学很喜欢玩计算机游戏,特别是战略游戏,但是有时他不能尽快找到解所以常常感到很沮丧。现在面临如下问题:他必须在一个中世纪的城堡里设防,城堡里的道路形成一棵无向树。要在结点上安排最少的士兵使得他们可以看到所有边。你能帮助他吗?

你的任务是给出士兵的最少数目。

输入:包含多组数据。每组数据表示一棵树,在每组数据中:

           第一行是结点的数目。

           接下来的几行,每行按如下格式描述一个结点:结点标识符 : ( 道路的数目 ) 结点标识符1  结点标识符2  ......  结点标识符道路的数目或者结点标识符 : (0)

对于 n (0<n<=1500) 个结点,结点标识符是一个从 0 到 n - 1 的整数。每条边在测试用例中只出现一次。

对于每组数据,各给出一个整数表示士兵的最少数目。

测试输入期待的输出时间限制内存限制额外进程
测试用例 1以文本方式显示
  1. 4↵
  2. 0:(1) 1↵
  3. 1:(2) 2 3↵
  4. 2:(0)↵
  5. 3:(0)↵
  6. 5↵
  7. 3:(3) 1 4 2↵
  8. 1:(1) 0↵
  9. 2:(0)↵
  10. 0:(0)↵
  11. 4:(0)↵
以文本方式显示
  1. 1↵
  2. 2↵

问题分析

这道题的思路比较简单。我们最先想到的一种解法就是按结点度数从大到小排列,每遍历到一个结点就把这个结点和与它相关的所有边都删除,然后更新剩余结点度数。直到边数为0.

但是这种想法显然在存储、遍历和删除时都会很麻烦。

于是重新考虑题目解法。一开始我进入了一个误区,认为所有度数大于等于2的结点都要放置一个士兵。但是后来想到一种情况:

这样的话1、2、4的度都>=2,但是其实只需要放置2和4就可以链接所有边。

根据以上分析,仔细考虑题目,我们可以发现,实际上我们可以从叶子结点所对应的父结点着手开始考虑。

即,如果我们能先找到一个叶子结点,然后把士兵放在其父结点上,把它的父结点连接的所有边删除,把父节点也删除。下一轮继续寻找叶子结点,重复上述操作。这样我们实际上就较为顺畅地实现了最开始的那种解法。

问题解决

int const MM=1501; //定义最大结点数
int map[MM][MM];  //定义图,存储各个顶点有无边
int degree[MM];   //定义记录每个结点的度的数组
int main() {  
    int n;  
    while (cin>>n) //有输入才开始本轮循环
    {  
        for (int i = 0; i < n; i++) //读入数据
        {  
            int index; //先读入结点下标
            cin>>index;
            getchar();getchar(); //吸收:和(
            int d;  
            cin >> d;  //读入该结点所连的边数
            getchar();  //吸收)
            degree[index]+=d; //更新该结点的度
            for (int j = 0; j < d; j++) //依次读入该结点所连边的另一端结点的下标
            {  
                getchar(); //吸收下标前的空格
                int x;   
                cin>>x; //读入另一端的结点下标
                map[index][x]=1; //在图中标记这两个点之间有边,无向图要标记两次
                map[x][index]=1; 
                degree[x]++;  //另一端结点度数也++
            }
            getchar();  //吸收回车
        } 
        int ans = 0,sum=0;   //初始化士兵数ans
        while(sum<n)  //sum是用来记录有多少个结点被访问到了,都被访问到后不再进入循环
        {
            for (int i = 0; i < n; i++) 
            {  
                if (degree[i] == 1) //找到度数为1的结点,即叶子结点
                { 
                    degree[i]=0; //父结点连线去掉后该叶子结点度数肯定为0,此处先行更新
                    sum++;  //叶子结点已遍历,所以已遍历结点数++
                    int j;
                    for(j=0;j<n;j++)  //寻找该叶子结点的父结点
                    {
                        if(map[i][j]==1)  //有边证明父结点找到
                        {
                            ans++;
                            map[j][i]=0;  //去掉该边
                            map[i][j]=0;
                            degree[j]=0;  //父结点度数也归0
                            sum++;  //父结点已遍历到,所以已遍历结点数++
                            break; //找到父结点后退出循环,j即为父结点下标
                        }
                    }
                    for(int k=0;k<n;k++)  //遍历父结点所连的边
                    {
                        if(map[j][k]==1)  //如果某结点与该父结点有边
                        {
                            map[j][k]=0;  //删除该边
                            map[k][j]=0;
                            if(degree[k]==1)  //如果当前遍历到的某结点也是叶结点
                            {
                                degree[k]=0; //度归0
                                sum++;  //该叶子结点已遍历,所以已遍历结点数++
                            }
                            else
                            {
                                degree[k]--;  //否则度--,因为度不为0之后还得遍历这个结点
                            }
                        }
                    }
                }  
            } 
        }
        cout<<ans<<endl;  //输出最少士兵个数
    }  
    return 0;  
}

其中需要注意的是,每轮先找到一个叶子结点,在找这个叶子结点的父结点时,遍历的时候用该叶子结点对应的行,即map[i][j](不是map[j][i]),是因为叶子结点只有一条边,所以找到了

map[i][j]=1的 j 就找到了它的父结点,就可以退出了。

另外,当遍历父结点连接的其他边时也要分情况,若另一端结点不为叶结点,不能直接degree[k]=0;sum++;  而要degree[k]--;  因为度数不为0之后还要遍历。

注:

这个代码没有考虑只有一个根节点的情况,考虑的话也比较简单,加上一个判断n是否为1,若n=1直接输出1并continue进入下一轮即可。

但是不用考虑孤立顶点(完全没有边或有的点没有边)的情况和形成环的情况,因为这是一棵树,除了上述根节点孤立外不可能有额外孤立的点,也不可能成环。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值