『学习笔记』树上dp

树上 dp,顾名思义,就是在树上进行动态规划。

既然 dp 是思想,那么就直接拿题来吧。

P1352 没有上司的舞会

题目大意

某大学有 \(n\) 个职员,编号为 \(1 \dots n\)

他们间的从属关系可以构成一棵以校长为根的有根树。

现在需要选择一些人来参加一个宴会,第 \(i\) 个职员来了会为宴会增加 \(r_i\) 的快乐指数。但是,如果某个职员的直接上司来参加了宴会,这个人就一定不会来了。

这里的直接上司定义为某个职员的直接父亲节点。

求能够获得的最大快乐指数。

思路

显然,对于一个节点 \(u\),我们是可以通过求出他的子节点 \(v\) 的答案来求出他的答案的。于是考虑 dp。

每个职员有两种情况:来与不来。

所以我们定义状态:\(f_{i,j}(j \in \{0,1\})\) 表示第 \(i\) 个职员,来与不来的情况下,他与他的所有子节点能够达到的最大快乐指数。

转移方程很好想:

  • \(u\) 不来:显然,\(u\) 的直接下属可以自由选择来与不来,那么 \(f_{u,0}\) 就等于 \(\sum f_{v,0}+f_{v,1}\) 了。其中 \(v\) 分别表示 \(u\) 的一个子节点。若没有某个子节点,则相应的子节点值为 \(0\)

  • \(u\) 来:那么下属就只能不来了。但是 \(u\) 来会获得快乐指数 \(r_u\),所以 \(f_{u,1}=r_u+\sum f_{v,0}\)

那么转移方程就出来了:

\[f_{i,j}= \begin{cases} \sum f_{v,0}+f_{v,1}&j=0\\ r_u+\sum f_{v,0}&j=1\\ \end{cases} \]

对于初始条件,将 \(f_{i,j}\) 全部设为 \(0\) 即可。

时间复杂度 \(\mathcal{O}(n)\)

答案即为 \(\min\{f_{root,0},f_{root,1}\}\)

代码

#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
    T X=0; bool flag=1; char ch=getchar();
    while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
    while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
    if(flag) return X;
    return ~(X-1);
}

const int N=6e3+5;
struct edge{
    int to,nxt;
}e[N];
int n,u,v,rt;
int r[N],s[N]; // r 用于记录每个职员来的快乐指数,s 用于记录入度(判根节点)
int head[N],top;
int f[N][2]; // f[i][j]:第 i 个职员来(j=1)与不来(j=0)所能获得的最大快乐指数

void add(int u,int v){
    top++;
    e[top].to=v;
    e[top].nxt=head[u];
    head[u]=top;
}

void dp(int u){
    // 初始化
    f[u][0]=0,f[u][1]=r[u];
    for(int i=head[u]; i; i=e[i].nxt){ // 遍历两个子节点
        int v=e[i].to;
        dp(v);
        // 需要求出子树最优解后才能求当前节点的
        f[u][0]+=max(f[v][0],f[v][1]);
        f[u][1]+=f[v][0];
    }
}

int main(){
    n=read();
    for(int i=1; i<=n; i++)
        r[i]=read();
    for(int i=1; i<n; i++){
        u=read(),v=read();
        add(v,u);
        s[u]++;
    }
    // 找根节点
    for(int i=1; i<=n; i++)
        if(!s[i]){
            rt=i;
            break;
        }
    dp(rt);
    printf("%d\n",max(f[rt][0],f[rt][1]));
    return 0;
}

P2016 战略游戏

题目大意

有一个城堡,其中的道路形成一棵无根树。当一个士兵站在其中某个节点上时,可以看守与这个节点有连边的节点。

现在给你这颗树,请你算出满足所有节点有士兵或被所有相邻节点的士兵看守,最少需要放多少士兵。

也就是说,一个节点要么是一个士兵,要么是一片空地,如果是空地,所有与这个空地相邻的节点都必须有士兵。

思路

显然,这题也可以通过某个节点子节点的解来求这个节点的解,可以 dp。

无脑设计状态:\(f_{i,j}(j \in \{0,1\})\) 表示第 \(i\) 个节点与它的所有后代满足要求,第 \(i\) 个节点放与不放士兵的最优解。

那么就来分别考虑两种情况(以 \(u\) 为父节点,\(v\) 为各个子节点):

  • 不放士兵(\(j=0\)):不放的话,子节点就一定要放。故 \(f_{u,0}=\sum f_{v,1}\)

  • 放士兵(\(j=1\)):这种情况子节点就自由选择,但不确定子节点放还是不放的解更优,所以 \(f_{u,1}=\sum\max\{f_{v,0},f_{v,1}\}+1\),这里 \(+1\) 是因为 \(u\) 自己也要放一个士兵。

时间复杂度 \(\mathcal{O}(n)\)

代码

#include <iostream>
using namespace std;
template<typename T=int>
inline T read(){
    T X=0; bool flag=1; char ch=getchar();
    while(ch<'0' || ch>'9'){if(ch=='-') flag=0; ch=getchar();}
    while(ch>='0' && ch<='9') X=(X<<1)+(X<<3)+ch-'0',ch=getchar();
    if(flag) return X;
    return ~(X-1);
}

const int N=1e4+5;
struct edge{
    int to,nxt;
}e[N];
int n,k,u,v;
int head[N],top;
int f[N][2];

void add(int u,int v){
    top++;
    e[top].to=v;
    e[top].nxt=head[u];
    head[u]=top;
}

void dp(int u,int fa){
    f[u][0]=0,f[u][1]=1; // 初始化
    for(int i=head[u]; i; i=e[i].nxt){
        int v=e[i].to;
        if(v==fa) continue; // 防止死递归
        dp(v,u);
        f[u][0]+=f[v][1];
        f[u][1]+=min(f[v][0],f[v][1]);
    }
}

int main(){
    n=read();
    for(int i=1; i<=n; i++){
        u=read()+1,k=read();
        // 把下标从 [0,n) 变成 [1,n]
        while(k--){
            v=read()+1; // 同上
            add(u,v);
            add(v,u);
        }
    }
    // 根节点的父亲随便写(恶臭/bx/bx
    dp(1,114514);
    printf("%d\n",min(f[1][0],f[1][1]));
    return 0;
}

习题

posted @ 2022-07-23 16:34  仙山有茗  阅读(70)  评论(0编辑  收藏  举报