『大 树形dp』

<更新提示>

<第一次更新>


<正文>

Description

滑稽树上滑稽果,滑稽树下你和我,滑稽树前做游戏,滑稽多又多。树上有 n 个节点,它们构成了一棵树,每个节点都有一个滑稽值。

一个大的连通块是指其中最大滑稽值和最小滑稽值之差不超过d。

每次你可以选择一个大的连通块并把它们删掉,请问你最少能用几次把这些节点都删掉呢?

Input Format

第一行两个整数 d 和 n。

第二行 n 个整数,分别表示每个节点的滑稽值。

接下来 n-1 行每行两个整数表示一条边。

Output Format

一行一个整数表示答案。

Sample Input

3 5
1 2 3 4 5
1 2
1 3
3 4
3 5

Sample Output

2

解析

一道思维题。

一看上去就很像树形\(dp\),不过限制好像很难维护。但是我们可以换一个方向考虑,我们把一个点权为\(a[x]\)的节点看做一个区间\([a[x],a[x]+d]\),那么一次合法的联通块删除操作必然满足至少有一个公共点被连通块内的所有区间覆盖。

想到这个就可以\(dp\)了,设\(g[x]\)代表删除子树\(x\)的最小代价,\(f[x][v]\)代表以\(x\)为根的子树中还存在一个未结算删除代价的连通块,其公共点为\(v\)的最小代价和。状态转移方程:

\[f[x][v]=\sum_{y\in son(x)}\min\{f[y][v],g[y]\},g[x]=\min_{v\in[a[x],a[x]+d]}\{f[x][v]+1\} \]

第一个方程的含义就是要么直接删除一棵子树,要么连接到当前点的连通块里,待会一起删除。第二个方程的含义就是找一个公共点,然后在节点\(x\)处把未结算的代价结算掉,删除连通块。

\(Code:\)

#include <bits/stdc++.h>
using namespace std;
const int N = 5020;
struct edge { int ver,next; } e[N*2];
int n,d,t,Head[N],a[N],f[N][N],g[N];
inline void insert(int x,int y) { e[++t] = (edge){y,Head[x]} , Head[x] = t; }
inline void input(void)
{
    scanf("%d%d",&d,&n);
    for (int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for (int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        insert( x , y );
        insert( y , x );
    }
}
inline void dp(int x,int fa)
{
    for (int i=a[x];i<=min(a[x]+d,5000);i++)
        f[x][i] = 0;
    for (int i=Head[x];i;i=e[i].next)
    {
        int y = e[i].ver;
        if ( y == fa ) continue;
        dp( y , x );
        for (int j=a[x];j<=min(a[x]+d,5000);j++)
            f[x][j] += min( f[y][j] , g[y] );
    }
    for (int i=a[x];i<=min(a[x]+d,5000);i++)
        g[x] = min( g[x] , f[x][i] + 1 );
}
int main(void)
{
    input();
    memset( f , 0x3f , sizeof f );
    memset( g , 0x3f , sizeof g );
    dp( 1 , 0 );
    printf("%d\n",g[1]);
    return 0;
}


<后记>

posted @ 2019-09-02 16:10  Parsnip  阅读(271)  评论(0编辑  收藏  举报