动态规划之树形DP专题(附题目清单)

之所以这样命名树规,是因为树形DP的这一特殊性: 没有环,dfs是不会重复,而且具有明显而又严格的层数关系。 利用这一特性,我们可以很清晰地根据题目写出一个在树(型结构)上的记忆化搜索的程序。而深搜的特点,就是“不撞南墙不回头”。这一点在之后的文章中会详细的介绍。

动态规划:
  问题可以分解成若干相互联系的阶段,在每一个阶段都要做出决策,全部过程的决策是一个决策序列。要使整个活动的总体效果达到最优的问题,称为多阶段决策问题。动态规划就是解决多阶段决策最优化问题的一种思想方法。
阶段:
  将所给问题的过程,按时间或空间(树归中是空间,即层数)特征分解成若干相互联系的阶段,以便按次序去求每阶段的解。
状态:
  各阶段开始时的客观条件叫做状态。
决策:
  当各段的状态取定以后,就可以做出不同的决定,从而确定下一阶段的状态,这种决定称为决策。 (即孩子节点和父亲节点的关系)
策略:
  由开始到终点的全过程中,由每段决策组成的决策序列称为全过程策略,简称策略。
状态转移方程:
  前一阶段的终点就是后一阶段的起点,前一阶段的决策选择导出了后一阶段的状态,这种关系描述了由k阶段到k+1阶段(在树中是孩子节点和父亲节点)状态的演变规律,称为状态转移方程。
目标函数与最优化概念:
  目标函数是衡量多阶段决策过程优劣的准则。最优化概念是在一定条件下找到一个途径,经过按题目具体性质所确定的运算以后,使全过程的总效益达到最优。
树的特点与性质:
1、 有n个点,n-1条边的无向图,任意两顶点间可达
2、 无向图中任意两个点间有且只有一条路
3、 一个点至多有一个前趋,但可以有多个后继
4、 无向图中没有环;

拿到一道树规题,我们有以下3个步骤需要执行:

判断是否是一道树规题:

即判断数据结构是否是一棵树,然后是否符合动态规划的要求。如果是,那么执行以下步骤,如果不是,那么换台。

建树:通过数据量和题目要求,选择合适的树的存储方式。

如果节点数小于5000,那么我们可以用邻接矩阵存储,如果更大可以用邻接表来存储(注意边要开到2*n,因为是双向的。这是血与泪的教训)。如果是二叉树或者是需要多叉转二叉,那么我们可以用两个一维数组brother[],child[]来存储)。

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

a.根到叶子: 不过这种动态规划在实际的问题中运用的不多。本文只有最后一题提到。
b.叶子到根: 既根的子节点传递有用的信息给根,完后根得出最优解的过程。这类的习题比较的多。


【SSOI307】加分二叉树

【在线测试提交传送门】

【问题描述】

  设一个n个节点的二叉树tree的中序遍历为(l,2,3,…,n),其中数字1,2,3,…,n为节点编号。每个节点都有一个分数(均为正整数),记第i个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数
 若某个子树为空,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
 试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出;
 (1)tree的最高加分
 (2)tree的前序遍历

【输入格式】

第1行:一个整数n(n<30),为节点个数。
第2行:n个用空格隔开的整数,为每个节点的分数(分数<100)。

【输出格式】

第1行:一个整数,为最高加分(结果不会超过4,000,000,000)。
第2行:n个用空格隔开的整数,为该树的前序遍历。

【输入样例1】

5
5 7 1 2 10

【输出样例1】

145
3 1 2 4 5

【解题思路】

看到这个问题,我们首先应该想到的是这道题是否属于动态规划,而这里我们发现,结合问题,如果整棵树的权值最大,必然有左子树的权值最大,右子树的权值也最大,符合最优性原理。所以是动态规划。

但这不是一道树规的题目。因为我们可以用区间动规的模型解决掉:直接定义一个f[i][j]表示从i到j的最大值,则f[i][j]=max(f[i][k-1]*f[k+1][j]+a[k]),枚举k即可。接下来是如何建树的问题,只有把树建好了,才能输出其前序遍历。于是,我们看到了两个关键词:二叉树,中序遍历。有了这两个关键词,加上区间动规,这棵树就能建起来了。根据二叉树的特性来建树(这里不再具体讨论树的详细的构造了,中序遍历和前序遍历不懂得自己百度)。所以这颗树的前序遍历,只需要边动规边记录下root[i][j]=k表示i到j的根为k即可确定树的构造。

拿到一道题目,首先我们要做的是看清题目,判断这是一道考察什么算法的题目。只有建立在正确思路基础下的算法,才是有意义的,正确的算法,也是事半功倍的算法。而此题是批着 树形 外观的 非树形动态规划题。而真正的树形动态规划是在树上做动态规划。

#include 
using namespace std;

const int ee=50,e=-999999999;
int n;
int a[ee]={0},f[ee][ee],root[ee][ee]={0};//f(i,j)中序遍历为i,i+1,…,j的二叉树的最大加分

//**若根节点的下表是k,则左端点的是k-1,右端点是k+1;
void front(int x,int y)
{
    if(root[x][y]!=0)
        cout<' ';
    if(root[x][root[x][y]-1]!=0)    front(x,root[x][y]-1);
    if(root[root[x][y]+1][y]!=0)    front(root[x][y]+1,y);
}

int main()
{
    //memset 赋初值不能为1 memset(f,1,sizeof(f));
    cin>>n;

    for(int i=0;i<=n;i++)
    {
        for(int j=0;j<=n;j++)
            f[i][j]=1;
    }
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        f[i][i]=a[i];
        root[i][i]=i;
    }
    //区间长度
    for(int len=1;len<=n;len++)
    {
        //区间起点
        for(int i=1;i<=n;i++)
        {
            //终点
            int j=i+len;
            if(j<=n)
            {
                int temp=e;
                //因为是中序排列
                for(int k=i;k<=j;k++)
                {
                    if(temp < (f[i][k-1]*f[k+1][j]+a[k]))
                    {
                        temp=f[i][k-1]*f[k+1][j]+a[k];
                        root[i][j]=k;
                    }
                }
                f[i][j]=temp;
            }
        }
    }
    cout<1][n];

    //前序遍历
    cout<1,n);

return 0;
}

【SSOI562】二叉苹果树

【在线测试提交传送门】

【问题描述】

有一棵苹果树,如果树枝有分叉,一定是分 2 叉(就是说没有只有 1 个儿子的结点)。 这棵树共有 N 个结点(叶子点或者树枝分叉点),编号为 1-N,树根编号一定是 1。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有 4 个树枝 的树:
  2       5
   \       /
   3    4
    \   /
      1 
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。 给定需要保留的树枝数量,求出最多能留住多少苹果。

【输入格式】

第 1 行 2 个数,N 和 Q(1≤Q≤ N,1<N≤100)。
N 表示树的结点数,Q 表示要保留的树枝数量。接下来 N-1 行描述树枝的信息。
每行 3 个整数,前两个是它连接的结点的编号。第 3 个数是这根树枝上苹果的数量。 每根树枝上的苹果不超过 30000 个。

【输出格式】

一个数,最多能留住的苹果的数量。

【输入样例1】

5 2
1 3 1
1 4 10
2 3 20
3 5 20

【输出样例1】

21

【解题思路】

首先,可以肯定的是,这是一道有关树规的题目,父节点和子节点存在着相互关联的阶段关系。
 第一步完成。 
再执行第二步:我们观察到题目数据量不大,所以有两种选择:邻接矩阵和邻接表。因为邻接矩阵的代码简单,思路清晰,所以建议能写邻接矩阵的时候就不要写邻接表了。我们设ma[x][y]为边的值,因为树是双向的,所以要再记录ma[y][x]。
   设tree[v,1]为节点v的左子树,tree[v,2]为节点v的右子树,然后我们再递归建树(因为树是递归定义的,所以很多时候建树都要考虑递归)。
   建树的问题解决的了,我们就要列状态转移方程了。根据求什么设什么的原则,我们定义f[i][j]表示以i为节点的根保留k条边的最大值,那么f[v][k]=max(f[v][k],(f[tree[v][1]][i]+f[tree[v][2]][k-i-1]+num[v])),我们枚举i就可以了。正如我开头提到的。因为树是递归定义的所以我们可以用记忆化搜索的形式(dfs)来具体实现。而树本身严格分层,而且没有环。所以是不会重复的。
F[1][Q+1]就是答案。因为题目中给的是边的权值,而我们在处理时将每条边的权值全赋给其所连的父节点和子节点中的子节点(将关于边的问题转化为关于点的问题),所以最后是Q+1,表示点的数目。
#include

using namespace std;

const int ee=105;
int n,q;
int tree[ee][5]={0},ma[ee][ee]={0},num[ee]={0},f[ee][ee]={0};

void preproccess()
{
    for(int i=0;i<=n;i++)
        for(int j=0;j<=n;j++)
        {
            ma[i][j]=-1;
            ma[j][i]=-1;
        }
}

void maketree(int v);

void build(int x,int y,int lor)//lor means left or right
{
    num[y]=ma[x][y];
    tree[x][lor]=y;
    ma[x][y]=-1;ma[y][x]=-1;
    maketree(y);
}

void maketree(int v)
{
    int lr=0;
    for(int i=0;i<=n;i++)
        if(ma[v][i]>=0)//如果分叉了,那么记录
        {
            lr++;      //1 or 2 表示左支还是右支;
            build(v,i,lr);//存入并递归
            if(lr==2)    return;
        }
}

void dfs(int v,int k)
{
    if(k==0)    f[v][k]=0;
    else if(tree[v][1]==0 && tree[v][2]==0) f[v][k]=num[v];
    else
    {
        f[v][k]=0;
        for(int i=0;iif(f[tree[v][1]][i]==0)    dfs(tree[v][1],i);
            if(f[tree[v][2]][k-i-1]==0)    dfs(tree[v][2],k-i-1);
            f[v][k]=max(f[v][k],(f[tree[v][1]][i]+f[tree[v][2]][k-i-1]+num[v]));
        }
    }
}

int main()
{
    cin>>n>>q;
    preproccess();

    for(int i=0;iint x,y,xy;
        scanf("%d%d%d",&x,&y,&xy);
        ma[x][y]=xy;
        ma[y][x]=xy;
    }

    //建树;
    maketree(1);
    dfs(1,q+1);
    cout<1][q+1];
return 0;
}

【SSOI560】选课

【在线测试提交传送门】

【问题描述】

  学校实行学分制。每门的必修课都有固定的学分,同时还必须获得相应的选修课程学分。 学校开设了 N(N<300)门的选修课程,每个学生可选课程的数量M是给定的。学生选修了这M门课并考核通过就能获得相应的学分。
  在选修课程中,有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其它的一些课程的基础上才能选修。例如《Frontpage》必须在选修了《Windows操作基础》之后才能选修。我们称《Windows操作基础》是《Frontpage》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。每门课都有一个课号,依次为 1,2,3,…。例如:
课号 先修课号 学分
1 1
2 1 1
3 2 3
4 3
5 2 4
表中 1 是 2 的先修课,2 是 3、4 的先修课。如果要选 3,那么 1 和 2 都一定已被选修过。
你的任务是为自己确定一个选课方案,使得你能得到的学分最多,并且必须满足先修课优先的原则。假定课程之间不存在时间上的冲突。

【输入格式】

输入文件的第一行包括两个整数 N、M(中间用一个空格隔开),其中 1≤N≤300,1≤M
≤N。
以下 N 行每行代表一门课。课号依次为 1,2,…,N。每行有两个数(用一个空格隔开), 第一个数为这门课先修课的课号(若不存在先修课则该项为 0),第二个数为这门课的学分。 学分是不超过 10 的正整数。

【输出格式】

输出文件只有一个数:实际所选课程的学分总数。

【输入样例1】

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

【输出样例1】

13

【解题思路】

f[i][j]表示以i为节点的根的选j门课的最大值,然后有两种情况: i不修,则i的孩子一定不修,所以为0;i修,则i的孩子们可修可不修(在这里其实可以将其转化为将j-1个对i的孩子们进行资源分配的问题,也属于背包问题);答案是f[1][m]。
多叉转二叉。
 因为之前我们说过“在树的存储结构上,我们一般选的都是二叉树,因为二叉树可以用静态数组来存储,并且状态转移也很好写(根节点只和左子节点和右子节点有关系)。”所以转换成二叉树无疑是一种不错的选择。
 我们开两个一维数组,b[i](brother)&c[i](child)分别表示节点i的孩子和兄弟,以左孩子和右兄弟的二叉树的形式存储这样,根节点之和两个节点有关系了,状态转移的关系少了,代码自然也就好写了。
 我们依旧f[i][j]表示以i为节点的根的选j门课的最大值,那么两种情况:1.根节点不选修则f[i][j]=f[b[i]][j];2.根节点选修f[i][j]=f[c[i]][k]+f[b[i]][j-k-1]+a[i](k表示左孩子学了k种课程);取二者的最大值即可。
 当题目中的数据结构是多叉树的时候,我们有两种选择:直接在多叉树上动规,或者转化为二叉树后动规。毫无疑问,二叉树上的动规是简洁的。但是,并不是说所有的多叉树都需要转化。
#include
using namespace std;

const int e=320;
int n,m;
int c[e]={0},b[e]={0},s[e]={0},f[e][e]={0};//c[]:means child ; b[]:means brother

void maketree()//多叉转二叉
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int ta,tb;
        scanf("%d%d",&ta,&tb);
        s[i]=tb;
        if(ta==0) ta=n+1;
        b[i]=c[ta];
        c[ta]=i;
    }
}

void dfs(int x,int y)
{
    if(f[x][y]>=0)    return;
    if(x==0 || y==0) {  f[x][y]=0;return;}
    dfs(b[x],y);
    //max()f[b[x]][y];
    for(int k=0;k//不取根节点
        dfs(c[x],y-k-1);//取根节点
        f[x][y]=max(f[x][y] , max(f[b[x]][y] , f[b[x]][k]+f[c[x]][y-k-1]+s[x]));
    }
    //cout<
    return;
}

int main()
{

    memset(f,-1,sizeof(f));

    maketree();
    dfs(c[n+1],m);

    cout<1]][m]<return 0;
}

【SSOI559】选课(输出方案)

【在线测试提交传送门】

【问题描述】

  学校实行学分制。每门的必修课都有固定的学分,同时还必须获得相应的选修课程学分。 学校开设了 N(N<300)门的选修课程,每个学生可选课程的数量M是给定的。学生选修了这M门课并考核通过就能获得相应的学分。
  在选修课程中,有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其它的一些课程的基础上才能选修。例如《Frontpage》必须在选修了《Windows操作基础》之后才能选修。我们称《Windows操作基础》是《Frontpage》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。每门课都有一个课号,依次为 1,2,3,…。例如:
课号 先修课号 学分
1 1
2 1 1
3 2 3
4 3
5 2 4
表中 1 是 2 的先修课,2 是 3、4 的先修课。如果要选 3,那么 1 和 2 都一定已被选修过。
你的任务是为自己确定一个选课方案,使得你能得到的学分最多,并且必须满足先修课优先的原则。假定课程之间不存在时间上的冲突。

【输入格式】

输入文件的第一行包括两个整数 N、M(中间用一个空格隔开),其中 1≤N≤300,1≤M
≤N。
以下 N 行每行代表一门课。课号依次为 1,2,…,N。每行有两个数(用一个空格隔开), 第一个数为这门课先修课的课号(若不存在先修课则该项为 0),第二个数为这门课的学分。 学分是不超过 10 的正整数。

【输出格式】

第一行只有一个数,即实际所选课程的学分总数。
以下N行每行有一个数,表示学生所选课程的课号。n行学生选课的课号按从小到大的顺序输出。

【输入样例1】

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

【输出样例1】

13
2
3
6
7

【解题思路】

既然树是递归定义的,所以我们依旧使用递归的形式来记录路径:使用一个bool数组ans来进行递归, 
分两种情况:取(1)和不取(0)。
然后,我们继续利用已经求得的f[i][j]的值来思考如何找到路径:
首先定义一个path()函数。如果f[i][j]=f[b[i]][j],那么节点i必然没有取,让ans[i]=0;否则,节点i一定取到了。(为什么呢?其实,这是依照第一问的dfs来思考的,第一问的dfs是这样定义的,所以我们就这样考虑了。)然后依照上一问,if(f[x][y]==f[b[x]][k-1]+f[c[x]][y-k]+s[x]),那么我们在i节点后选的一定是以上的方案,在这时让ans[i]=1,继续深搜path()即可。最后从1到n依次输出取到的点即可。
#include
using namespace std;

const int e=520;
int c[e]={0},b[e]={0},f[e][e]={0},s[e]={0};
bool ans[e]={0};
int n,m;

void maketree()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)
    {
        int ta,tb;
        scanf("%d%d",&ta,&tb);
        s[i]=tb;
        b[i]=c[ta];
        c[ta]=i;
    }
}

void dfs(int x,int y)//以x为节点,取y个
{
    if(f[x][y]>=0)    return;
    if(x==0 || y==0)
    {
        f[x][y]=0;
        return;
    }
    else    dfs(b[x],y);
    int tm=f[b[x]][y],tp=0;

    for(int k=1;k<=y;k++)
    {
        dfs(b[x],k-1);
        dfs(c[x],y-k);
        tp=max(tp,f[b[x]][k-1]+f[c[x]][y-k]+s[x]);
    }

    if(tp>tm)    f[x][y]=tp;
    else    f[x][y]=tm;

    return;
}

void path(int x,int y)
{
    if(x==0 || y==0)
        return;
    if(f[x][y]==f[b[x]][y])    path(b[x],y);
    else
    {
        for(int k=1;k<=y;k++)
        {
            if(f[x][y]==f[b[x]][k-1]+f[c[x]][y-k]+s[x])
            {
                path(b[x],k-1);
                path(c[x],y-k);
                ans[x]=1;
                return;
            }
        }
    }
}

int main()
{
    memset(f,-1,sizeof(f));
    cout<0]<<' '<2]<0],m);

    cout<0]][m]<0],m);

    for(int i=1;i<=n;i++)
        if(ans[i])    cout<return 0;
}

你可能感兴趣的:(动态规划,树形动态规划,算法总结,动态规划问题)