zoukankan      html  css  js  c++  java
  • P2279 消防局的设立 (树形DP or 贪心)

    (点击此处查看原题)

    树形DP写法

    看到这个题的要求,很容易相到这是一个树形DP的问题,但是dp数组应该如何设计并转移才是关键

    dp[i][0]代表当前结点可以向上覆盖2层,自身一定被覆盖
    dp[i][1]代表当前结点可以向上覆盖1层,自身一定被覆盖
    dp[i][2]代表当前结点可以向上覆盖0层,自身一定被覆盖
    dp[i][3]代表当前结点可以向下覆盖1层,表示自己不一定被覆盖,但是儿子一定全部被覆盖
    dp[i][4]代表当前结点可以向下覆盖2层,表示自己不一定被覆盖,但是孙子一定全部被覆盖

    所谓向上覆盖x层,即当前结点向上x个结点(祖先结点)作为根结点的树被完全覆盖的情况下,最少需要设立的消防站数目
    所谓向下覆盖x层,即当前结点向下x个结点(后代结点)作为根结点的树被完全覆盖的情况下,最少需要设立的消防站数目

    显然满足
    dp[i][0] >= dp[i][1] >= dp[i][2] >= dp[i][3] >= dp[i][4]

    dp[u][0] = 1 + min(dp[v][0~4])
    如果当前结点想要覆盖向上2层,则自身必然安置一个消防站,而u所有子节点就随意了

    dp[u][1] = min{ dp[v][0] + ∑dp[e][0~3] , dp[u][0] }
    覆盖到当前结点上1层,有两种情况:
    1)在其子节点安置了至少一个消防站,这样一来,不仅覆盖了当前结点上一层的结点,而且
    安置了消防站的子节点将覆盖他的兄弟结点,所以其兄弟结点至少保证自身子节点被完全覆盖即可
    2)当前结点安置消防站,覆盖了上两层的同时,也可以覆盖当前结点上一层

    dp[u][2] = min{dp[v][1] + ∑dp[e][2],dp[u][1],dp[u][0]}
    覆盖到当前结点上0层,分三种情况
    1)刚好覆盖到当前结点,则至少选择一个当前结点的孙子结点,由于这个消防站无法覆盖到当前结点的其他子节点,
    那么其余结点至少保证自身被覆盖即可
    2)可以覆盖到当前结点向上一层,同时覆盖了当前结点的子孙结点,此时即在当前结点的子节点设立消防站
    3)可以覆盖到当前结点向上二层,同时覆盖了当前结点的子孙结点,此时即在当前结点设立消防站

    dp[u][3] = ∑dp[v][2]
    覆盖当前结点所有的子节点,则保证当前结点的所有子节点被覆盖

    dp[u][4] = ∑dp[v][3]
    覆盖当前结点所有的孙子节点,则保证当前结点的所有子节点的子节点被覆盖

    最后,为了覆盖整棵树,我们输出dp[1]0]即可

    代码区

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<queue>
    #include<string>
    #include<fstream>
    #include<vector>
    #include<stack>
    #include <map>
    #include <iomanip>
    
    #define bug cout << "**********" << endl
    #define show(x, y) cout<<"["<<x<<","<<y<<"] "
    #define LOCAL = 1;
    using namespace std;
    typedef long long ll;
    const int inf = 1e9 + 7;
    const int mod = 1e9 + 7;
    const int Max = 1e3 + 10;
    
    int n, m, k;
    int head[Max], tot;
    int to[Max], Next[Max];
    int dp[Max][5];
    
    /*
     * dp[i][0]代表当前结点可以向上覆盖2层,自身一定被覆盖
     * dp[i][1]代表当前结点可以向上覆盖1层,自身一定被覆盖
     * dp[i][2]代表当前结点可以向上覆盖0层,自身一定被覆盖
     * dp[i][3]代表当前结点可以向下覆盖1层,表示自己不一定被覆盖,但是儿子一定全部被覆盖
     * dp[i][4]代表当前结点可以向下覆盖2层,表示自己不一定被覆盖,但是孙子一定全部被覆盖
     *
     * 所谓向上覆盖x层,即当前结点向上x个结点(祖先结点)作为根结点的树被完全覆盖的情况下,最少需要设立的消防站数目
     * 所谓向下覆盖x层,即当前结点向下x个结点(后代结点)作为根结点的树被完全覆盖的情况下,最少需要设立的消防站数目
     *
     * 显然满足
     * dp[i][0] >= dp[i][1] >= dp[i][2] >= dp[i][3] >= dp[i][4]
     *
     * dp[u][0] = 1 + min(dp[v][0~4])
     * 如果当前结点想要覆盖向上2层,则自身必然安置一个消防站,而u所有子节点就随意了
     *
     * dp[u][1] = min{ dp[v][0] + ∑dp[e][0~3] , dp[u][0] }
     * 覆盖到当前结点上1层,有两种情况:
     * 1)在其子节点安置了至少一个消防站,这样一来,不仅覆盖了当前结点上一层的结点,而且
     * 安置了消防站的子节点将覆盖他的兄弟结点,所以其兄弟结点至少保证自身子节点被完全覆盖即可
     * 2)当前结点安置消防站,覆盖了上两层的同时,也可以覆盖当前结点上一层
     *
     * dp[u][2] = min{dp[v][1]  +  ∑dp[e][2],dp[u][1],dp[u][0]}
     * 覆盖到当前结点上0层,分三种情况
     * 1)刚好覆盖到当前结点,则至少选择一个当前结点的孙子结点,由于这个消防站无法覆盖到当前结点的其他子节点,
     * 那么其余结点至少保证自身被覆盖即可
     * 2)可以覆盖到当前结点向上一层,同时覆盖了当前结点的子孙结点,此时即在当前结点的子节点设立消防站
     * 3)可以覆盖到当前结点向上二层,同时覆盖了当前结点的子孙结点,此时即在当前结点设立消防站
     *
     * dp[u][3] = ∑dp[v][2]
     * 覆盖当前结点所有的子节点,则保证当前结点的所有子节点被覆盖
     *
     * dp[u][4] = ∑dp[v][3]
     * 覆盖当前结点所有的孙子节点,则保证当前结点的所有子节点的子节点被覆盖
     *
     * 最后,为了覆盖整棵树,我们输出dp[1]0]即可
     */
    
    void add(int u, int v)
    {
        to[tot] = v;
        Next[tot] = head[u];
        head[u] = tot++;
    }
    
    void dfs(int u)
    {
        dp[u][0] = 1;
        dp[u][1] = inf;
        dp[u][2] = inf;
        dp[u][3] = 0;
        dp[u][4] = 0;
        for (int i = head[u]; i != -1; i = Next[i])
        {
            int v = to[i];
            dfs(v);
            dp[u][0] += dp[v][4];
            dp[u][3] += dp[v][2];
            dp[u][4] += dp[v][3];
        }
        if (head[u] == -1)                //没有子节点了,此时dp[u][0~2]必须使得u安置消防站
        {
            dp[u][1] = dp[u][2] = 1;
            return;
        }
        for (int i = head[u]; i != -1; i = Next[i])
        {
            int v = to[i];
    
            int sum1 = 0, sum2 = 0;        //记录∑dp[e][3]和∑dp[e][4]
            for (int j = head[u]; j != -1; j = Next[j])
            {
                int e = to[j];
                if (e == v)
                    continue;
                sum1 += dp[e][3];
                sum2 += dp[e][2];
            }
            dp[u][1] = min(dp[u][1], dp[v][0] + sum1);
            dp[u][2] = min(dp[u][2], dp[v][1] + sum2);
        }
        for (int i = 1; i <= 4; i++)    //最后综合处理一下
            dp[u][i] = min(dp[u][i], dp[u][i - 1]);
    }
    
    int main()
    {
    #ifdef LOCAL
    //    freopen("input.txt", "r", stdin);
    //    freopen("output.txt", "w", stdout);
    #endif
        memset(head, -1, sizeof(head));
        tot = 0;
    
        scanf("%d", &n);
        for (int v = 2, u; v <= n; v++)
            scanf("%d", &u), add(u, v);
        dfs(1);
        printf("%d
    ", dp[1][2]);
        return 0;
    }
    View Code

    贪心写法(适用于在树种,求点覆盖半径为k的最小点覆盖)

    其实这种解法对于这一类型的题目非常的适合,主要体现在不需要像树形DP一样确定状态并且易于理解,这种解法的思想如下:

    我们用dis[i]表示结点i到最近的消防站的最短距离,如果dis[i] > k ,说明结点i不在已存在的消防站的覆盖范围内,为了保证消防站利用率最大化,我们在结点i向上第k个结点,记作x,也就是结点i覆盖的极限范围出处设立消防站,这样一来不仅使得结点i被覆盖,还可以覆盖更多的结点,然后我们更新x向上k层范围内所有结点的dis,即更新各点到消防站的最近距离,重复这一过程,统计消防站的数目即可

    代码区

    #include<iostream>
    #include<cstdio>
    #include<algorithm>
    #include<cstring>
    #include<queue>
    #include<string>
    #include<fstream>
    #include<vector>
    #include<stack>
    #include <map>
    #include <iomanip>
    
    #define bug cout << "**********" << endl
    #define show(x, y) cout<<"["<<x<<","<<y<<"] "
    #define LOCAL = 1;
    using namespace std;
    typedef long long ll;
    const int inf = 1e9 + 7;
    const int mod = 1e9 + 7;
    const int Max = 1e3 + 10;
    
    struct Node
    {
        int depth;                //记录当前结点深度
        int id;                    //记录结点编号
    } node[Max];
    
    int n, k;
    int dis[Max];                //dis[i]记录结点i到最近的消防站的最近距离
    int fa[Max];                //dp[i]记录i的父节点
    
    bool cmp(Node node1, Node node2)
    {
        return node1.depth > node2.depth;
    }
    
    int main()
    {
    #ifdef LOCAL
        //    freopen("input.txt", "r", stdin);
        //    freopen("output.txt", "w", stdout);
    #endif
        scanf("%d", &n);
    
        k = 2;                              //结点覆盖半径为2,根据实际情况改变
    
        node[1].depth = 0;
        node[1].id = 1;
        dis[0] = dis[1] = inf;              //处理根结点(这里灵活建图即可)
    
        for (int v = 2, u; v <= n; v++)
        {
            scanf("%d", &u);                //构建的边为 u --> v
            node[v].depth = node[u].depth + 1;
            node[v].id = v;
            fa[v] = u;
            dis[v] = inf;
        }
        sort(node + 1, node + 1 + n, cmp);
        int sum = 0;
    
        for (int i = 1; i <= n; i++)
        {
            int son = node[i].id;
            int now = node[i].id;           //当前结点
    
            for (int j = 1; j <= k; j++)    //处理出k个祖先结点到当前结点的最佳距离
            {
                now = fa[now];
                dis[son] = min(dis[son], dis[now] + j);
            }
    
            if (dis[son] > k)                //代表每个结点覆盖半径k
            {
                dis[now] = 0;                //此处总是在最远祖先处设立消防站
                sum++;
                for (int j = 1; j <= k; j++) //之后由最远祖先结点向上k层更新其余点的距离
                {
                    now = fa[now];
                    dis[now] = min(dis[now], j);
                }
            }
        }
        printf("%d
    ", sum);
        return 0;
    }
    View Code
  • 相关阅读:
    2014年寒假学习规划
    二十进制数的加法--【英雄会】
    使用IBM SVC构建vSphere存储间集群
    游戏服务器学习笔记 2———— 准备工作
    php判断正常访问和外部访问
    游戏服务器学习笔记 3———— firefly 的代码结构,逻辑
    数学基础知识 ——(1)高等数学
    动态内存与智能指针
    Numpy(4)—— 保存和导入文件
    Numpy(3)—— 线性代数相关函数
  • 原文地址:https://www.cnblogs.com/winter-bamboo/p/11516016.html
Copyright © 2011-2022 走看看