树形DP | 1:—— 例题:小游戏

小游戏

成绩10开启时间2020年03月10日 星期二 07:55
折扣0.8折扣时间2020年04月7日 星期二 23:55
允许迟交关闭时间2020年04月7日 星期二 23:55

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

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

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

第一行是结点的数目。

接下来的几行,每行按如下格式描述一个结点:

结点标识符 : ( 道路的数目 ) 结点标识符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↵
1秒64M0

本题给出了一颗树,需要在若干节点上部兵,使得他们能看到所有的边。

1、对输入的处理 ——  图的存储

本题的树,也就是一个无向连通无环图。考虑用 邻接链表 来储存。(会采用vector容器简化链表的操作)

题目表明:每一条边只会输入一次。也就是说,对于边ab在输入时,如果在点a后输入了b,那么在点b后就不会再输入a了。而此题的图是一个无向图,为了方便我们的一般化对图的遍历,应该在处理输入且构建图时,要将两点之间的边补全:即对于边ab,我们可以通过b访问到a,也可以通过a访问过b。

2、动态寻求最优部署 —— 深搜

为确保每一条边都需要能被看到,那么每条边的两个端点中必须至少有一端有士兵每一个点有两种选择:放士兵、不放士兵。且根据树的结构特点:根节点 root 下有若干子节点,每一个字节点和其后裔又可以看作一棵子树。那么我们可以采用深搜:搜索root根节点,然后依次对 root 的每一棵子树继续搜索。(第一次root可以随便选择)

使用一个二维数组 dp[0..n-1, 0..1] 来记录最少人数 :

  • dp[root][0]:当 root 节点不放士兵时,以 root 为根的树需要的最少人数
  • dp[root][1]:当 root 节点放士兵时,以 root 为根的树需要的最少人数

本题应该是自底向上的解法:

先将子树(设其节点为next)的dp[next][0]、dp[next][1]计算出来,在此基础上才能计算子树上一层的树(设其节点为root)的dp[root][0]、dp[root][1]。最终的答案应该是dp[root][0]、dp[root][1]中的较小值。

构建状态方程: 

那么如何从子树的结果迁移到整体的树呢?分两种:

  • 如果 root 节点不放兵:其每一个子节点next必须放兵,必须选择 dp[next][1]
  • 如果 root 节点放兵:其每一个子节点next可以放兵也可以不放兵,应该选择 dp[next][0] 与 dp[next][1] 中的较小值,再加上root本身的一个

故状态转移方程如下:

dp[root][0] = \sum dp[next][1]

\bg_white dp[root][1] =1 + \sum min(dp[next][0], dp[next][1])


完整AC代码:

//
// Created by LittleCat on 2020/3/10.
//

#include <cstdio>
#include <cstring>
#include <bits/stdc++.h>

using namespace std;
#define MAX 1550

vector<int> point[MAX]; //map[i]储存节点i的临接节点集
bool vis[MAX];  //vis[i]表示节点i是否被访问过
int dp[MAX][2];

void DFS(int root) {
    vis[root] = true;   //标记已访问节点
    /* 依次遍历根节点的每一个子节点 */
    for (int i = 0; i < point[root].size(); i++) {
        int next = point[root][i];   //子节点
        if (vis[next])   //是已访问的前驱节点
            continue;  //避免往回搜索
        DFS(next);  //搜索子节点
        dp[root][0] += dp[next][1];
        dp[root][1] += min(dp[next][0], dp[next][1]);
    }
    dp[root][1]++;
}

int main() {
    int n;
    while (EOF != scanf("%d", &n)) {
        /* 初始化数组 */
        for (auto & i : point)
            i.clear();
        memset(vis, false, sizeof(vis));
        memset(dp, 0, sizeof(dp));
        /* 处理输入 */
        for (int i, k; n; n--) {
            scanf("%d:(%d)", &i, &k);  //前驱节点i,边数k
            for (int j; k; k--) {
                scanf("%d", &j);  //后继节点j
                point[i].push_back(j);
                point[j].push_back(i);
            }
        }
        int root = 0;
        DFS(root);
        printf("%d\n", min(dp[root][0], dp[root][1]));
    }
}

 


有任何问题欢迎评论交流,如果本文对您有帮助不妨点点赞,嘻嘻~  



欢迎关注个人公众号“ 鸡翅编程 ”,这里是认真且乖巧的码农一枚。

---- 做最乖巧的博客er,做最扎实的程序员 ----

旨在用心写好每一篇文章,平常会把笔记汇总成推送更新~

在这里插入图片描述

  • 14
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值