树形dp

树形dp

[[USACO 2008 Jan G]Cell Phone Network]([1003-USACO 2008 Jan G]Cell Phone Network_2021秋季算法入门班第八章习题:动态规划2 (nowcoder.com))

给你一颗树,问最少选多少个点可以把整棵树覆盖,每选一个点它会覆盖它周围和它直接相连的点。

思路:

由于一个点被覆盖有三种情况,分别是自己覆盖自己,被儿子覆盖或者被父亲覆盖。

所以不能单纯像边覆盖一样设某个点选或不选。

应该还要把不选分为被父亲覆盖还是被儿子覆盖。

\(f[i][0]\) 表示 \(i\) 选,自己覆盖自己

\(f[i][1]\) 表示 \(i\) 不选,被儿子覆盖

\(f[i][2]\) 表示 \(i\) 不选,被父亲覆盖

方程:

\(f[i][0]=\Sigma{min(f[d][0],f[d][1],f[d][2])}\) 儿子随便被谁覆盖都行

对于靠儿子 \(f[i]][1]\) 很显然会有靠哪个儿子的问题。

这里分情况讨论:

​ 如果儿子中不选都比选少,那么就找那个选和不选差别最小的儿子来选,然后其他都在选和不选中取小的

​ 如果儿子中有一个选了比不选少,那个贡献父亲的就选它,然后其他点都在选和不选中取小的。

\(f[i][1]=(\Sigma min(f[d][0],f[d][1]))+increse\)

这个 \(increse\) 是选的那个来贡献父亲的 \((f[d][0]-f[d][1])\)

\(f[i][2]=\Sigma min(f[d][0],f[d][1])\) 这里没有 \(f[d][2]\) 因为 \(i\) 不选

#include<bits/stdc++.h>
using namespace std;
#define INF 0x3f3f3f3f
const int maxn = 10100;
int f[maxn][3];
vector<int> v[maxn];
void dfs(int pos,int fa)
{
    f[pos][0] = 1;
    f[pos][1] = 0;
    f[pos][2] = 0;
    int inc = INF;
    for(auto d:v[pos])
    {
        if(d==fa) continue;
        dfs(d, pos);
        f[pos][0] += min(f[d][0], min(f[d][1], f[d][2]));
        f[pos][1] += min(f[d][0], f[d][1]);
        f[pos][2] += min(f[d][0], f[d][1]);
        inc = min(f[d][0] - f[d][1], inc);
    }
    if(inc<0)
        inc = 0;
    f[pos][1] += inc;
}
int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n - 1;i++)
    {
        int x, y;
        cin >> x >> y;
        v[x].push_back(y);
        v[y].push_back(x);
    }
    dfs(1, -1);
    cout << min(f[1][0], f[1][1]);
}

[[CTSC1997] 选课]([P2014 CTSC1997] 选课 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

题目描述

在大学里每个学生,为了达到一定的学分,必须从很多课程里选择一些课程来学习,在课程里有些课程必须在某些课程之前学习,如高等数学总是在其它课程之前学习。现在有 \(N\) 门功课,每门课有个学分,每门课有一门或没有直接先修课(若课程 a 是课程 b 的先修课即只有学完了课程 a,才能学习课程 b)。一个学生要从这些课程里选择 \(M\) 门课程学习,问他能获得的最大学分是多少?

输入格式

第一行有两个整数 \(N\) , \(M\) 用空格隔开。( \(1 \leq N \leq 300\) , \(1 \leq M \leq 300\) )

接下来的 \(N\) 行,第 \(I+1\) 行包含两个整数 $k_i $和 \(s_i\), \(k_i\) 表示第I门课的直接先修课,\(s_i\) 表示第I门课的学分。若 \(k_i=0\) 表示没有直接先修课(\(1 \leq {k_i} \leq N\) , \(1 \leq {s_i} \leq 20\))。

输出格式

只有一行,选 \(M\) 门课程的最大得分。

样例 #1

样例输入 #1

7  4
2  2
0  1
0  4
2  1
7  1
7  6
2  2

样例输出 #1

13

思路:树上背包模板题。

状态转移方程:\(f[u][j]=max(f[u][j],f[son][k]+f[u][j-k])\)

细节就是 \(f[u][j]\) 要初始化为 \(val[u]\),然后由于没有先修课的课先修课为0,所以把0作为树根,但是由于 \(f[u][j]\) 表示的是以 \(u\) 为子树的树中选了 \(j\) 节课(包括自己),所以当 \(u\)\(0\) 的时候会多选了一门虚空课,所以总选课数要+1。

#include <bits/stdc++.h>
using namespace std;
int k;
int f[310][310];
vector<int> g[310];
int val[310];
void dfs(int u)
{
    f[u][1] = val[u];//初始化f[u][1] 就可以了
    for (auto d : g[u])
    {
        int to = d, w = val[d];
        dfs(to);
        for (int j = k + 1; j >=0; j--)
        {
            for (int kk = 0; kk < j; kk++)//这里必须要留位置给子树的根节点
            {
                f[u][j] = max(f[u][j], f[u][j - kk] + f[to][kk]);
            }
        }
    }
}
int main()
{
    int n;
    cin >> n>>k;
    for (int i = 1; i <= n;i++)
    {
        int pre, v;
        cin >> pre >> val[i];
        g[pre].push_back(i);
    }
    dfs(0);
    cout << f[0][k+1];
}

dls \(O(n^2)\)写法

#include<bits/stdc++.h>
using namespace std;
const int N = 310;
const int inf = 1 << 29;
int n,q;
int sz[N], a[N], dp[N][N];
vector<int> g[N];
/*
in:
5 8
1 1 2 2
10 -1 4 10 -5
1 1
1 2
1 3
1 4 
1 5
2 1
2 2
2 3

out:
10
14
19
23
18
-1
9
4
*/
void dfs(int u)
{
    sz[u] = 0;
    static int tmp[N];
    for(auto d:g[u])
    {
        dfs(d);
        for (int i = 0; i <= sz[u] + sz[d];i++)
            tmp[i] = -inf;
        for (int i = 0; i <= sz[u];i++)
        {
            for (int j = 0; j <= sz[d];j++)
            {
                tmp[i + j] = max(tmp[i + j], dp[u][i] + dp[d][j]);
            }
        }
        sz[u] += sz[d];
        for (int i = 0; i <= sz[u] + sz[d];i++)
            dp[u][i] = tmp[i];
    }
    if(u!=0)
    {
        sz[u] += 1;
        for (int i = sz[u]; i >= 1; i--)
        {
            dp[u][i] = dp[u][i - 1] + a[u];
        }
        dp[u][0] = 0;
    }
}
int main()
{
    scanf("%d%d", &n,&q);
    for(int i=1;i<=n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        a[i]=y;
        g[x].push_back(i);
    }
    dfs(0);
    printf("%d",dp[0][q]);
}

求树上两点最长距离板子

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 100005;   
ll f[N];
vector<int> g[N];
ll val[N];
const ll INF = 0x8f8f8f8f;
ll res = -INF;
int n;
void dfs(int u, int fa)
{
    ll mx1 = 0, mx2 = 0;
    for(auto d:g[u])
    {
        if(d==fa)
            continue;
        dfs(d, u);
        if(mx1<f[d])
            mx2 = mx1, mx1 = f[d];
        else if(mx2<f[d])
            mx2 = f[d];
    }
    res = max(res, mx1 + mx2 + val[u]);
    f[u] = mx1 + val[u];
}

signed main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n;i++)
    {
        scanf("%lld", &val[i]);
    }
    for (int i = 1; i < n; i++)
    {
        ll u, v;
        scanf("%lld %lld", &u, &v);
        g[u].push_back(v), g[v].push_back(u); //默认权值为1
    }
    dfs(1, -1); //以1为树的根,所以1没有父节点,因此赋为-1
    printf("%lld\n", res);
}

Tree(牛客)

懒得写了,直接看题解)

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e6 + 5;
const int MOD = 1e9 + 7;
ll f[N];
ll ans[N];
vector<int> g[N];
ll poww(ll x, int y)
{
    ll ans = 1, base = x;
    while (y)
    {
        if (y & 1)
            ans = (ans * base) % MOD;
        base = (base * base) % MOD;
        y >>= 1;
    }
    return ans;
}
void dfs1(int u,int fa)
{
    f[u] = 1;
    for(auto d:g[u])
    {
        if(d==fa)
            continue;
        dfs1(d,u);
        (f[u] *= (f[d] + 1))%=MOD;
    }
}
void dfs2(int u,int fa)
{
    for(auto d:g[u])
    {
        if(d==fa)
            continue;
        if ((f[d] + 1) % MOD == 0)
        {
            dfs1(d, -1);
            ans[d] = f[d];
            continue;
        }
        ans[d] = (f[d] * ((ans[u] * poww(f[d] + 1, MOD - 2) % MOD + 1) % MOD))%MOD;
        dfs2(d, u);
    }
}
int main()
{
    int n;
    cin >> n;
    for (int i = 0; i < n - 1;i++)
    {
        int x, y;
        cin >> x >> y;
        g[x].push_back(y);
        g[y].push_back(x);
    }
    dfs1(1, -1);
    ans[1] = f[1];
    dfs2(1, -1);
    for (int i = 1; i <= n;i++)
    {
        cout << ans[i] << endl;
    }
}

树上mex(省赛被卡题)

ACM—河南省赛 J 树上mex - 知乎 (zhihu.com)

其实只要把树建对了,思路就很显然了。

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 5;
vector<int> g[N];
int ans[N];
int sze[N];
int val[N];
int minn[N];
int n;
void dfs(int u,int fa)
{
    sze[u] = 1;
    minn[u] = 0x3f3f3f3f;
    for(auto d:g[u])
    {
        if(d==fa)
            continue;
        dfs(d, u);
        sze[u] += sze[d];
        minn[u] = min(minn[u], minn[d]);
    }
    if(u==0){
        int maxn = 0;
        for(auto d:g[u])
        {
            maxn = max(maxn, sze[d]);
            //cout << sze[d] << "sze" << endl;
        }
        ans[u] = maxn;
        return;
    }
    if(minn[u]>u)
        ans[u] = n - sze[u];
    else
    {
        ans[u] = 0;
        //cout << "fu" << endl;
    }
    minn[u] = min(u, minn[u]);
}
int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n;i++)
        scanf("%d", &val[i]);
    for (int i = 2; i <= n;i++)
    {
        int fa;
        scanf("%d", &fa);
        g[val[fa]].push_back(val[i]);
        g[val[i]].push_back(val[fa]);
    }
    dfs(0, -1);
    for (int i = 0; i < n;i++)
        printf("%d ", ans[i]);
    printf("%d\n",n);
}

[HAOI2015] 树上染色(典题)

题目描述

有一棵点数为 \(n\) 的树,树边有边权。给你一个在 \(0 \sim n\) 之内的正整数 \(k\) ,你要在这棵树中选择 \(k\) 个点,将其染成黑色,并将其他 的 \(n-k\) 个点染成白色。将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间的距离的和的收益。问受益最大值是多少。

输入格式

第一行包含两个整数 \(n,k\)

第二到 \(n\) 行每行三个正整数 \(u, v, w\),表示该树中存在一条长度为 \(w\) 的边 \((u, b)\)。输入保证所有点之间是联通的。

输出格式

输出一个正整数,表示收益的最大值。

样例 #1

样例输入 #1

3 1
1 2 1
1 3 2

样例输出 #1

3

提示

对于 \(100\%\) 的数据,\(0 \leq n,k \leq 2000\)

思路:

第一次遇到这种树上要求两两点之间的距离和。

这种要算点对之间路径的长度和的题,难以统计每个点的贡献.这个时候一般考虑算每一条边贡献了哪些点对.
知道这个套路以后,那么这题就很好做了.

状态:设\(dp[u][i]\)表示 \(u\) 节点(子树里有 \(i\) 个黑点)的子树的边的贡献的和.
转移:转移就很好想了,知道 \(v\) 内的黑点个数 \(j\) ,知道 \(v\) 内的白点数目 \(sz[v]−j\) ,知道总共的黑点数目 \(m\) ,知道总共的白点数目 \((n−m)\) ,知道边权 \(w\) ,那么转移方程显然就是:
\(dp[u][i]=max(dp[v][j]+w∗(m−j)∗j+w∗(sz[v]−j)∗[n−m−(sz[v]−j)]+dp[u][i-j])\) \(v\)\(u\) 的儿子

有点类似于树形背包的思想,一个一个的加入考虑的子树。

image-20221107155724424

比如当前考虑到第三个子树,里面选了 j 个黑点,那么方程的意思其实是在前面的子树里选了 i-j 个黑点,后面没考虑到的子树没有选黑点的最大权值和,这还是背包思想,而慢慢加入子树考虑的写法会让复杂度降为 \(O(n^2)\)。说到底就是道树形背包题。

#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 2010;
ll f[N][N];
ll tmp[N];
vector<pair<ll, ll>> g[N];
int sz[N];
int n, k;
void dfs(int u,int fa)
{
    f[u][0] = f[u][1] = 0;//对叶子的初始化
    sz[u] = 1;
    for(auto d:g[u])
    {
        ll v = d.first, w = d.second;
        if(v==fa)
            continue;
        dfs(v, u);
        sz[u] += sz[v];
        for (int i = 0; i <= k;i++)
            tmp[i] = f[u][i];
        for (int i = 0; i <=k; i++)
        {
            for (int j = 0; j <= min(sz[v], i); j++)
            {
                if(tmp[i-j]==-1)//一些不存在的状态处理
                    continue;
                ll res = f[v][j] + j * (k - j) * w + (sz[v] - j) * (n - k - sz[v] + j) * w;
                f[u][i] = max(f[u][i], res+tmp[i-j]);
            }
        }
    }
}
int main()
{
    memset(f, -1, sizeof(f));
    cin >> n >> k;
    for (int i = 1; i < n;i++)
    {
        int u, v, w;
        cin >> u >> v >> w;
        g[u].push_back(make_pair(v, w));
        g[v].push_back(make_pair(u, w));
    }
    dfs(1, 0);
    cout << f[1][k] << endl;
}
posted @ 2022-10-17 20:41  Jayint  阅读(42)  评论(0)    收藏  举报