树形DP get !

· · 个人记录

入门:

传送门

一遍切绿怎么说,老哥稳。

定义 f[i][0/1] 表示以 i 为根的子树中,选自己(1) 或不选自己(0) 最多能选多少点。

设当前点为 u ,它的儿子们为 v ,显然转移方程为

f[u][1] = \sum f[v][0] f[i][0] = \sum \max(f[v][0],f[v][1])

主意第二个方程,点 u 不选时,它的儿子也可以不选,所以要取 \max

然后,就切了。。。

int f[MAXN][2];

void dfs(int u, int fa)
{
    f[u][1] = 1;
    f[u][0] = 0;
    for(int k=head[u];k;k=map[k].next)
    {
        int v = map[k].to;
        if(v == fa) continue;
        dfs(v,u);
        f[u][1] += f[v][0];
        f[u][0] += max(f[v][1],f[v][0]);
    }
}

int main(void)
{
    cin >> n;
    for(int i=1;i<n;i++)
    {
        int u,v;
        cin >> u >> v;
        add(u,v);   add(v,u);
    }
    dfs(1,0);
    cout << max(f[1][1],f[1][0]) << "\n";
    return 0;
}

经典:

传送门

void dfs(int u)
{
    if(head[u] == -1)   //没有儿子 
    {
        dp[u][0] = 0;
        dp[u][1] = c[u];
        return;
    }
    dp[u][1] = c[u];
    for(int k=head[u];k!=-1;k=tree[k].next)
    {
        int v = tree[k].to;
        dfs(v);
        dp[u][0] += max(dp[v][0],dp[v][1]); //不选上司,儿子可选可不选,挑个大的 
        dp[u][1] += dp[v][0];               //选了父亲,儿子不能选 
    }
    return;
}

int main()
{
    memset(head,-1,sizeof(head));   //我也不清楚为什么要赋 -1 但赋 0 就错
    cin >> n;
    for(int i=1;i<=n;i++)
    {
        cin >> c[i];
        num += i;
    }
    for(int i=1;i<=n;i++)
    {
        cin >> l >> k;
        num -= l;   //找出树根 
        add(l,k);
    }
    dfs(num);       //从树根开始dp
    cout << max(dp[num][0],dp[num][1]) << endl;
    return 0;
}

提高:

传送门

首先,十分感谢 HZW 奆老对我的帮助。

然后,让我们来分析一下本题

根据题意,我们需要开二维数组来记录每个节点相关的三个状态

即:

$f [ 1 ] [ i ]$ 表示该节点被自己覆盖。 $f [ 2 ] [ i ]$ 表示该节点被某一个子节点覆盖。 那么,得出三个状态转移方程: 1. $f [ 0 ] [ u ] += min( f [ 1 ] [ v ] , f [ 2 ] [ v ] );
    因为 u 被父节点覆盖时,v 节点本身和其子节点的状态无需考虑,取最优值。
  1. f [ 1 ] [ u ] += min( f [ 1 ] [ v ] , f [ 2 ] [ v ] , f [ 0 ] [ v ] );
    同上,不过也要将 v 节点的父节点考虑进去,因为其父节点就是 u 所以也要考虑它的值。
  2. f [ 2 ] [ u ] += min( f [ 1 ] [ v ] , f [ 2 ] [ v ] );
    同上,u 的子节点的花费由 v 或其子节点推出。

但是!

仔细一想,事情好像并没有这么简单,

由于 u 可能有许多子节点,所有子节点的最优值之和才是 f [ 2 ] [ u ]

所以要进行判断,如果有一个 v 节点的值比其子节点要优,则将其加入

反之,加入 f [ 2 ] [ v ]

还没完,

由于 u 必由一个子节点所覆盖,

所以当出现所有子节点最优值都是 f [ 2 ] [ v ] 时,

则要由一个与 f [ 2 ] [ v ] 差最小的 v 替代 f [ 2 ] [ v ]

由于之前已取了所有的 f [ 2 ] [ v ] ,所以只要将差值记录并加入即可。

int val[MAXN];
int f[3][MAXN];

void dp(int u,int fa)
{
    f[1][u] = val[u];
    f[0][u] = f[2][u] = 0;
    int x = 0x3f3f3f3f;
    int flag = 0;
    for(int k=head[u];k;k=tree[k].next)
    {
        int v = tree[k].to;
        if(v == fa) continue;
        dp(v,u);
        f[0][u] += min(f[2][v],f[1][v]);
        f[1][u] += min(min(f[0][v],f[1][v]),f[2][v]);
        if(f[1][v] < f[2][v])   flag = 1;
        else    x = min(x,f[1][v]-f[2][v]);
        f[2][u] += min(f[1][v],f[2][v]);
    }
    if(!flag)   f[2][u] += x;
}

int main(void) 
{
    cin >> n;
    for(int i=1,u,w,m;i<=n;i++)
    {
        cin >> u >> w >> m;
        val[u] = w;
        for(int j=1,v;j<=m;j++)
        {
            cin >> v;
            add(u,v);   add(v,u); 
        }
    }
    dp(1,-1);
    cout << min(f[1][1],f[2][1]);
    return 0;
}

鸡掰:

传送门

这个题,真恶心

恶心恶心,真恶心,再这么恶心要哭了哦~~~

特别要注意边界问题

不要和我一样心态爆炸边打代码边大喊 F*K !

好吧好吧,进入正题

这是一个森林,不好 DP (虽然有大佬直接DP的)

所以,

先转成二叉树(虚点 + 左儿子右兄弟)

然后,方程就好想了

再结合记忆化实现

struct node{
    int L,R;
    int w;
    node(){L = R = -1;}
}tree[MAXN];

int DP(int u, int k)
{
    if(k==0 || u==-1)   return 0;
    if(f[u][k] != 0)    return f[u][k];
    f[u][k] = max(f[u][k],DP(tree[u].R,k));
    for(int i=0;i<k;i++)
        f[u][k] = max(f[u][k],DP(tree[u].L,i)+DP(tree[u].R,k-1-i)+tree[u].w);
    return f[u][k];
}

int main(void)
{
    cin >> n >> m;
    for(int i=1,u,W;i<=n;i++)
    {
        cin >> u >> W;  //输入时转二叉树
        tree[i].R = tree[u].L;
        tree[u].L = i;
        tree[i].w = W;
    }
    cout << DP(0,m+1);
    return 0;
}

图P:

传送门

图上 DP

要把状态转移到 n 上,因为图可能不连通

由于有双向边,所以当 u 的状态被 fa 更新时,还要继续遍历 u

void dfs(int u, int fa, int minn)
{
    int flag = 1;
    minn = min(minn,a[u]);
    if(minn < m[u])
    {
        flag = 0;
        m[u] = minn;
    }
    f[u] = max(f[u],a[u]-m[u]);
    if(f[u] < f[fa])    //更新 u,并继续搜 u
    {
        f[u] = f[fa];
        flag = 0;
    }
    if(flag)    return;
    for(int k=head[u];k;k=map[k].next)
        dfs(map[k].to,u,minn);
}

int main(void)
{
    cin >> n >> r;
    memset(m,0x3f,sizeof m);
    for(int i=1;i<=n;i++)
        cin >> a[i];
    for(int i=1;i<=r;i++)
    {
        int u,v,f;
        cin >> u >> v >> f;
        add(u,v);
        if(f == 2)  add(v,u);
    }
    dfs(1,0,INF);
    cout << f[n];
    return 0;
}

贪心:

传送门

P 的题,用贪心做的,思路很好,记一下

```cpp bool cmp(int a, int b) { return dep[a] > dep[b]; } int main(void) { cin >> n; dep[1] = 0; fa[1] = 0; len[1] = len[0] = MAXN; a[1] = 1; for(int i=2;i<=n;i++) { cin >> fa[i]; dep[i] = dep[fa[i]]+1; a[i] = i; len[i] = MAXN; } sort(a+1,a+1+n,cmp); for(int i=1;i<=n;i++) { int u = a[i]; int FA = fa[u]; int gr = fa[FA]; len[u] = min(len[u],min(len[FA]+1,len[gr]+2)); if(len[u] > 2) { ans++; len[gr] = 0; len[fa[gr]] = min(len[fa[gr]],1); len[fa[fa[gr]]] = min(len[fa[fa[gr]]],2); } } cout << ans; return 0; } ``` # 细节: [传送门](https://www.luogu.org/problemnew/show/P1875) 这道题的做法有很多,可以用 $Dijikstra$ ,据 $LDL$ 说还可以用 差分约束 不过我一眼看出的是 树P 总的来说,这是一道阅读向的树形 $DP$ ,重点在题意理解。 首先,题目给你一种药的配方 $A + B$ ,但这并不意味着这一一棵二叉树,因为一种药的配方可以有很多,而且也可以出现环(被这里卡了很久)。 所以,要用类似链式前向星的链表**以两种材料为一组**来存图,$DP$ 时注意打标记,不然会 $M$ (其实是 $RE$) 说道这里,可能你就会发现,最本质的东西仍未被解决,那就是**为什么可以 $DP$**。 首先考虑无环的情况,那么正确性是显然的,而且非常的水。 如果加上环的话,就会有一种药不断向上合成后又合成回来,由于价格都是正整数,这种做法显然非常的愚蠢,所以这种合成方法不可能是最优的,那么只要保证程序不在环上 $D$ 来 $D$ 去就好了。 还有,这道题的数据范围真的很迷人,要往大里开 ```cpp int val[MAXN],f[MAXN]; int vis[MAXN]; int l,r,root; void dfs(int u) { for(int k=head[u];k;k=tree[k].next) { int L = tree[k].L; int R = tree[k].R; vis[u] = 1; if(!vis[L]) dfs(L); if(!vis[R]) dfs(R); if( val[u] > val[L]+val[R]) { val[u] = val[L]+val[R]; f[u] = f[L]*f[R]; } else if(val[u] == val[L]+val[R]) f[u] += f[L]*f[R]; } } int main(void) { cin >> n; for(int i=1;i<=n;i++) f[i] = 1; for(int i=1;i<=n;i++) cin >> val[i]; while(scanf("%d%d%d",&l,&r,&root) != EOF) { ++root; ++l; ++r; tree[++cnt] = (node){l,r,head[root]}; head[ root] = cnt; } dfs(1); cout << val[1] << " " << f[1]; return 0; } ``` # 变形: [传送门](https://www.luogu.org/problemnew/show/P3554) 一遍切蓝!! 二分我猜是可以很快看出来的,就是要二分每次涂的点数,因为手造几组样例后会发现直接 $DP$ 找答案仿佛很难(好像根本不可行,但我不会证明),所以可以二分判定可行性。 那么我们 $DP$ 什么呢?可以想到,判断二分出的答案可不可行的依据是在每个点时需要涂的点数,因为树 $P$ 是倒推的过程,如果最后在根节点需要涂的点数等于 $mid$ ,那就说明可行。 所以我们**定义 $f[i]$ 为以点 $i$ 为根的子树中下一步需要涂多少点(不包括自己)。** 那么对于点 $u$ ,设 $v$ 是它的子节点,则子节点们所需的点数为 $\sum f[v]$ 再加上子节点本身,就是点 $u$ 所需涂的点数。所以方程就出来了: $f[u] = \sum (f[v]+1)

但是这样有点不妥,显然点数越加越多,中间缺少了涂色的过程,所以每次转移完后要涂一次色。则方程就变成了这样:

f[u] = \max(0,\sum (f[v]+1)-mid)

其中为防止出现负数要与 0max ,由于每次都减掉了 mid ,所以 f[i] 可以理解为i 为根的子树中涂了 mid 个点后还剩多少点 ,所以如果 f[1] = 0 ,则可行。

void dfs(int u, int fa, int mid)
{
    int sum = 0;
    for(int k=head[u];k;k=map[k].next)
    {
        int v = map[k].to;
        if(v == fa) continue;
        dfs(v,u,mid);
        sum += f[v]+1;
    }
    f[u] = max(0,sum-mid);
}

bool check(int mid)
{
    memset(f,0,sizeof f);
    dfs(1,0,mid);
    if(f[1] == 0) return 1;
    else          return 0;
}

int main(void)
{
    cin >> n;
    for(int i=1;i<n;i++)
    {
        int u,v;
        cin >> u >> v;
        add(u,v);   add(v,u);
    }
    int L = 0;
    int R = n;
    while(L <= R)
    {
        int mid = (L+R)>>1;
        if(check(mid)) R = mid-1;
        else           L = mid+1;
    }
    cout << L;
    return 0;
}

递推:

传送门

是的,有时候树形 DP 要结合容斥来递推求解。

以样例为例子,我们可以一遍 DFS 求出点 1 的不方便程度,然后我们计算点 3 的值,经过思考我发现,点 3 的值其实是可以由点 1 转移的,因为从点 1 改成点 3 ,以点 3 为根的子树(包括点 3 )都少走了 1 到 3 的路程,而其他点都要多走这一段路,由此得出转移方程:

f[3] = f[1]+sum*w-2*w*size[3] ```cpp int n; long long ans,sum; long long c[MAXN]; long long f[MAXN]; long long size[MAXN]; long long shit[MAXN]; void dfs(int u, int fa) { shit[u] = c[u]; for(int k=head[u];k;k=map[k].next) { int v = map[k].to; int w = map[k].w; if(v == fa) continue; size[v] = size[u]+w; f[1] += size[v]*c[v]; dfs(v,u); shit[u] += shit[v]; } } void dp(int u, int fa) { for(int k=head[u];k;k=map[k].next) { int v = map[k].to; int w = map[k].w; if(v == fa) continue; f[v] = f[u]-(2*shit[v]*w-sum*w); ans = min(ans,f[v]); dp(v,u); } } int main(void) { cin >> n; for(int i=1;i<=n;i++) cin >> c[i], sum += c[i]; for(int i=1;i< n;i++) { int u,v,w; cin >> u >> v >> w; add(u,v,w); add(v,u,w); } dfs(1,0); ans = f[1]; dp(1,0); cout << ans; return 0; } ```