POJ 2152 树型DP //很棒的题

题意:Z国有n个城市,从1到n给这些城市编号。城市之间连着高速公路,并且每两个城市之间有且只有一条通路。不同的高速公路可能有不同的长度。最近Z国经常发生火灾,所以当地政府决定在某些城市修建一些消防站。在城市k修建一个消防站须要花费大小为W[k]的费用。函数W对于不同的城市可能有不同的取值。如果在城市k没有消防站,那么它到离它最近的消防站的距离不能超过D[k]。每个城市在不超过距离D[k]的前提下,必须选择最近的消防站作为负责站。函数D对于不同的城市可能有不同的取值。为了节省钱,当地政府希望你用最少的总费用修建一些消防站,并且使得这些消防站满足上述的要求。摘自陈启峰论文,国家集训队论文集2006年

COGS上也有这道题,消防站,不过题目描述给简化了一下,思路少绕一个弯。

感觉这道题很棒,主要是自己并不会,去看了陈启峰的论文,了解到了一种解题的思想,可以去看一看那一篇论文,收获蛮大。

这道题还是很容易看出来用树型动规的,不过要用,还需要一点点变化,因为按照题目描述,想建立一个不错状态、设计出合法的转移是比较困难的。引用论文中的说法就是:题目的要求太严格了。因为要求对于每个城市,要求在D[k]范围内,选择最近的一个消防站作为负责站,因为最近这个词,使得状态设计很艰难,而COGS上简化的题意就直接略去了这个词。所以我们要简化题意、放宽限制。


这也算是通过这篇论文了解到的一种思想吧,当题目要求使得我们不太容易使用已知的算法来解决时,我们可以尝试放宽题目中的一些要求——在不影响解的正确性的前提下;或者说,当题目的要求过于宽泛使得我们无从下手时,我们同样可以在不影响解的正确性的前提下加一些条件。或许这种思想我们做题的时候经常用,不过看了论文是第一次表述出来这种思想。


对于“最近”这个词,思考我们删去后,对解是否有影响?当然没有,假如在范围内有多个消防站,我们解题时按照某一个作为负责站,但是”最近“的仍然是存在的,这一个状态仍然是合法的。
所以,我们可以设计出状态:f[i][j]表示城市i被j城市所建立的消防站负责时,以i为根的子树达到合法局面(每个城市都有相应消防站负责)需要的最小代价。当j不在i的子树中时,w[j]不计算在f[i][j]中。

之后我们就可以设计转移方程,不过当前的形式仍然不容易设计方程,因为这道题不像 vijos 小胖守皇宫 一样,一个节点只有三种情况:被自己看守,被父节点看守,被子节点看守。而且每个节点“被负责”的范围是不同的。比如当前子树i,i被其子树中某一个节点j负责,那么i到j之间的这一段节点被谁负责?这一段仍需要相应的转移,并不好设计。此时的局面就可以说是,要求过于宽泛,无从下手。那么我们需要加一些要求。

假如i被j负责,那么i~j路径上所有的点都被j负责。如果有这个条件,转移就会简单很多,那么这个条件正确与否,是否会影响解的正确性呢?答案是正确的,最优解的局面,必然会有一种情况满足这个条件。一个局面对于多种情况是因为我们之前改了题目的限制,负责站不一定是最近的。详细证明,去看看陈启峰的论文吧。(→.→反正我这篇博客基本是看着那篇论文写的Orz)

对于一个状态f[i][j]首先判断是否合法,即dist(i, j) <= D[i]是否成立,不成立则f[i][j] = INF。
1. 当j不在以i为根的子树内时:
此时对于i节点的每个子节点k都有两个选择,要么选择j为负责站,要么在以k为根的子树中选择负责站。为了节约时间,再开一个best数组,best[i] = min{j在以i为根的子树中 | f[i][j]},转移就是:
f[i][j] = Σ min{best[k], f[k][j]}
2. 当j == i时:
这时对于i的每个子节点k同样是两个选择,同上,只需要最后再加上一个w[i],在i建立消防站的花费即可。转移就是:
f[i][j] = w[i] + Σ min{best[k], f[k][j]}
3. 当j在以i为根的子树内且j != i时:
假如j在以i的子节点k0为根的子树中,那么对于其他子节点k,转移同上,仍然是两种,而k0的转移就只有一种f[k0][j]了。转移就是:
f[i][j] = f[k0][j] + Σ min{best[k], f[k][j]} (k != k0)

现在已经调好了,两个DP函数都是可以用的,不过我最初写的那种会慢一点,因为多一个dfs求每个点的子树中都有那些节点。

#include <cstdio>
#include <algorithm>
#include <cstring>
#include <bitset>
#include <vector>
using namespace std;

int n, w[1005], d[1005], best[1005], dis[1005][1005], f[1005][1005];

bitset <1005> B[1005];
vector <int> G[1005];

struct Edge{int a, b, c;}E[1005];

int get_v(int u, int i)
{
    return E[G[u][i]].a == u ? E[G[u][i]].b : E[G[u][i]].a;
}

void dfs(int u, int fa)
{
    B[u][u] = 1;
    for(int i = 0; i < G[u].size(); i++)
    {
        int v = get_v(u, i);
        if(v != fa) {dfs(v, u);B[u] |= B[v];}
    }
}

void dfs2(int beg, int end, int u, int fa, int len)
{
    dis[beg][u] = dis[u][beg] = len;
    for(int i = 0; i < G[u].size(); i++)
    {
        int v = get_v(u, i);
        if(v != fa && !dis[u][v]) dfs2(beg, end, v, u, len+E[G[u][i]].c);
    }
}

int dist(int i, int j)
{
    if(i == j || dis[i][j]) return dis[i][j];
    dfs2(i, j, i, 0, 0);
    return dis[i][j];
}

void DP(int u, int fa)
{
    for(int i = 0; i < G[u].size(); i++)
    {
        int v = get_v(u, i);
        if(v != fa) DP(v, u);
    }
    for(int j = 1; j <= n; j++) if(dist(u, j) <= d[u])  
    {
        f[u][j] = 0;
        if(B[u][j] && u != j)
        {
            int k0;
            for(int k = 0; k < G[u].size(); k++)
            {
                int l = get_v(u, k);
                if(B[l][j] && l != fa){k0 = l; break;}
            }
            for(int k = 0; k < G[u].size(); k++)
            {
                int l = get_v(u, k);
                if(l != fa && l != k0) f[u][j] += min(best[l], f[l][j]);
            }
            f[u][j] += f[k0][j];
        }
        else
        {   
            for(int k = 0; k < G[u].size(); k++)
            {
                int l = get_v(u, k);
                if(l != fa) f[u][j] += min(best[l], f[l][j]);
            }
            f[u][j] += (u==j)*w[u];
        }
        if(B[u][j]) best[u] = min(best[u], f[u][j]);
    }
}

/*void DP(int u, int fa)
{
    for(int i = 0; i < G[u].size(); i++)
    {
        int v = get_v(u, i);
        if(v != fa) DP(v, u);
    }
    for(int i = 1; i <= n; i++) if(dist(u, i) <= d[u])
    {
        f[u][i] = w[i];
        for(int j = 0; j < G[u].size(); j++)
        {
            int v = get_v(u, j);
            if(v != fa) f[u][i] += min(best[v], f[v][i]-w[i]);
        }
        best[u] = min(best[u], f[u][i]);
    }
}*/


int main()
{
    int T; scanf("%d", &T);

    while(T--)
    {
        memset(B, 0, sizeof B);
        memset(G, 0, sizeof G);
        memset(dis, 0, sizeof dis);
        memset(f, 0x3f, sizeof f);
        memset(best, 0x3f, sizeof best);

        scanf("%d", &n);
        for(int i = 1; i <= n; i++) scanf("%d", w+i);
        for(int i = 1; i <= n; i++) scanf("%d", d+i);
        for(int i = 1; i < n; i++)
        {
            scanf("%d %d %d", &E[i].a, &E[i].b, &E[i].c);
            G[E[i].a].push_back(i);
            G[E[i].b].push_back(i);  
        }

        dfs(1, 0);

        DP(1, 0);

        printf("%d\n", best[1]);
    }
    return 0;
}
  • 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、付费专栏及课程。

余额充值