树形dp

树形dp

树形dp的性质:没有环,dfs不会重复,而且具有明显而又严格的层数关系。

判断一道题是否是树形dp:首先判断数据结构是否是一棵树,然后是否符合动态规划的要求。如果都符合,那么是一道树形dp的问题。我们需要通过下面几个步骤来解题。

1.建树

建树过程中,我们需要通过数据量和题目的要求,选择合适的树的存储方式。

一般来说,如果节点数小于5000,我们一般可以用邻接矩阵来存储。如果节点数更多,我们一般采用邻接表来存储。(注意,采用邻接表时,我们需将边开到2*n,因为边是双向)。如果是二叉树或则多叉树转二叉树,我们可以用一个brother[]child[]来存储。

2.写出树规方程

通过观察孩子与父亲之间的关系建立方程。通常我们认为,树形DP的写法有两种:

1:根到叶子:不过这种动态规划在实际问题中运用的不多。

2:叶子到根:即根的子节点传递有用的信息给根,之后根得出最优解。

注意:上面两种方法一般来说是不能相互转换的(个别情况是个例),且第二种方法适用的普遍性广泛很多。

3.入门例题

1:poj 2342

问题描述:对于一棵树,每个节点有自己的权值,取了一个节点其相邻的父节点和子节点就没有价值了。求最大价值。

分析可得状态转移方程:当取 i 节点时:(dp[i][1]+=dp[j][0]);当不取 i 节点时:(dp[[i][0]+=max(dp[j][1],dp[j][0]));上面的 j 都表示为节点 i 的下属。

show code:

#include <bits/stdc++.h>
#define debug(x) cout << #x << " = " << x << endl; 

using namespace std;
typedef long long LL;
const int maxn = 1e4 + 10;
const int inf = 0x3f3f3f3f;
const double eps = 1e-6, pi = acos(-1);
int gcd(int a,int b)    { return b == 0 ? a : gcd(b, a % b); }

int h[maxn], n;
struct edge{
    int nex, to;
}Edge[maxn];
int head[maxn], tot, vis[maxn], rt;
int f[maxn][2];

inline void add(int from, int to){
    Edge[++tot].to = to;
    Edge[tot].nex = head[from];
    head[from] = tot; 
}
void dfs(int u)
{
    f[u][0] = 0;
    f[u][1] = h[u];
    for(int i = head[u]; i != -1; i = Edge[i].nex){
        int v = Edge[i].to;
        dfs(v);
        f[u][0] += max(f[v][0], f[v][1]);
        f[u][1] = max(f[u][1], f[u][1] + f[v][0]);
    }
}

int main()
{
    scanf("%d", &n);
    memset(head, -1, sizeof(head));
    for(int i = 1; i <= n; ++i){
        scanf("%d", &h[i]);
    }
    for(int i = 1; i < n; ++i){
        int x, y;
        scanf("%d %d", &x, &y);
        add(y, x);  vis[x] = 1;
    }
    for(int i = 1; i <= n; ++i){
        if(!vis[i]) {rt = i; break;}
    }
    dfs(rt);
    printf("%d
", max(f[rt][0], f[rt][1]));
}

2:POJ 1947

题意:有n个点组成的树,问至少删除多少条边才能获得一颗有p个节点的子树。

我们定义状态dp[i][j]表示以 i 为根节点生成节点数为 j 的最少删的边的个数,所以我们可以得到状态转移方程:

[dp[i][j]=min(dp[i][j],dp[i][j-k]+dp[i.son][k]-2) ]

上面的 -2 表示

#include <bits/stdc++.h>

using namespace std;
const int maxn=200;
const int inf=0x3f3f3f3f;
int dp[maxn][maxn];     //dp[i][j]代表以i为根节点,生成节点数为k所删掉的边
int a,b,n,p,root;       //分别代表边的两个端点,边的总数,最后要得到的边的数量,根节点
int fa[maxn];
vector<int> tree[maxn];
void dfs(int x)
{
    int len=tree[x].size();
    for(int i=0;i<len;++i)      //不能从1到Len,因为vector数组下标是从0开始的
    {
        dfs(tree[x][i]);     //递归子节点
        for(int j=p;j>1;j--)    //j=1情况下面已近初始化过了
            for(int k=1;k<j;++k)
                dp[x][j]=min(dp[x][j],dp[x][j-k]+dp[tree[x][i]][k]-2);
    }
}

int main()
{
    ios::sync_with_stdio(false);

    memset(fa,false,sizeof(fa));
    cin>>n>>p;
    for(int i=1;i<n;++i){
        cin>>a>>b;
        tree[a].push_back(b);
        fa[b]=a;
    }
    root=1;
    while(fa[root])     //找根节点
        root=fa[root];
    for(int i=1;i<=n;++i)   //初始化dp
    {
        //以i为根,生成节点数为1的子树所需删掉的根,每个节点都有个父节点+1,根节点也有个虚拟父节点,方便处理
        dp[i][1]=tree[i].size()+1;          //这里也是为什么下面状态转移时要-2的原因了
        for(int j=2;j<=p;++j)
            dp[i][j]=inf;
    }
    dfs(root);
    dp[root][p]--;              //将根节点的虚拟节点删去
    int res=inf;
    for(int i=1;i<=n;++i)
        res=min(res,dp[i][p]);
    cout<<res<<endl;

    system("pause");
    return 0;
}

3.换根树形dp poj-3585

换根dp一般是遍历两次无根树中的所有结点, 第一次随便以一个点作为根自下而上跑一次树形dp, 第二次则还以刚刚那个点作为根节点, 自上而下的找到以另外一点为根的权值和原来的树形dp跑出的权值之间的关系, 对于每个点O(1)换根。故时间复杂度为O(n)。

题意:树的每条边有自己的权值,以哪个结点作为源点使得总权值最大(如果一个结点有多个节点,只能取最小的)

1:O(n^2)的做法

直接上状态转移方程:

[g[root]=sum_{son∈son(root)}left{ egin{aligned} v(root,son) size(son)=1\ min(g[son],v) size(son)>1\ end{aligned} ight. ]

每一个点按照上面方法做一遍树形dp即可。

2:O(n)的做法

由于这是一颗无根树,不同的根会产生不同的答案,故我们只需思考一下如何进行换根。而换根的主要思路就是如何处理根与根的转化。

f[i]表示以 i 为根的子树中,答案的最大值,g[i]表示以 i 为根节点的到子端点的最大流量。所以有状态转移方程:

[f[i] =left{ egin{aligned} v(i,j) size(j)=1\ g[j]+min(v(i,j),f[i]-min(g[j],v(i,j))) size(j)>1\ end{aligned} ight. ]

show code:

#include <bits/stdc++.h>

using namespace std;
const int maxn = 2e5 + 10;

struct edge{
    int nex, to, val;
}Edge[maxn<<1];
int head[maxn], tot;
int n, de[maxn], f[maxn], dis[maxn];

inline void init()
{
    tot = 0;
    for(int i = 1; i <= n; ++i){
        dis[i] = de[i] = 0;
        head[i] = -1;
    }
}
inline void add(int from, int to, int val){
    Edge[++tot].to = to;
    Edge[tot].nex = head[from];
    Edge[tot].val = val;
    head[from] = tot;
}
void dp(int u, int fa)         //树形dp, 自下而上
{
    for(int i = head[u]; i != -1; i = Edge[i].nex){
        int v = Edge[i].to;
        if(fa == v) continue;
        dp(v, u);
        if(de[v] == 1)  dis[u] += Edge[i].val;          //一定要注意边界条件!
        else dis[u] += min(Edge[i].val, dis[v]);
    }
}   
void dfs(int u, int fa)         //换根, 每个点O(1)换根, n割点O(n)
{
    for(int i = head[u]; i != -1; i = Edge[i].nex){
        int v = Edge[i].to;
        if(fa == v) continue;                           //找到新根与原来的dis之间的关系
        if(de[u] == 1)  f[v] = Edge[i].val + dis[v];
        else f[v] = dis[v] + min(Edge[i].val, f[u] - min(Edge[i].val, dis[v]));
        dfs(v, u);
    }
}

int main()
{
    int t; scanf("%d", &t);
    while(t--){
        scanf("%d", &n);
        init();
        for(int i = 1; i < n; ++i){
            int a, b, c;
            scanf("%d %d %d", &a, &b, &c);
            add(a, b, c), add(b, a, c);
            de[a]++, de[b]++;
        }
        int rt = 1;             //随便选一个点作为根节点做一下树形dp
        dp(rt, -1);    f[1] = dis[1];
        dfs(rt, -1);            //再利用刚刚那个根堆每个点换一下根
        int ans = 0;
        for(int i = 1; i <= n; ++i){
            ans = max(ans, f[i]);
        }
        printf("%d
", ans);
    }
}
原文地址:https://www.cnblogs.com/StungYep/p/12254143.html