区间DP&树形DP

目录

 

区间DP

树形DP

例题

A:POJ-2955 Brackets

B:UVA-10003 Cutting Sticks

C:POJ-1651 Multiplication Puzzle

D:HDU-3506 Monkey Party

E:POJ-2342 Anniversary party

F:HDU-1054 Strategic Game

G:ZOJ-3201 Tree of Tree

H:POJ-3659 Cell Phone Network


两个定义(做题要用到):

定义1 对于图G=(V,E)来说, 最小支配集 指的是从V中取尽量少的点组成一个集合,使得对于V中剩余的点都与取出来的点有边相连。也就是说,设V‘是图G的一个支配集,则对于图中的任意一个顶点u,要么属于集合V’,要么与V‘中的顶点相邻。在V’中出去任何元素后V‘不再是支配集,则支配集是极小支配集。称G的所有支配集中顶点个数最少的支配集为最小支配集,最小支配集中顶点的个数称为支配数。

定义2 对于图G=(V,E)来说, 最小点覆盖 指的是从V中取尽量少的点组成一个集合,使得E中所有的边都与取出来的点相连。也就是说,设V‘是图G的一个顶点覆盖,则对于图中的任意一条边(u,v),要么u属于集合V’,要么v属于集合V‘。在V‘中除去任何元素后V’不在是顶点覆盖,则V‘是极小顶点覆盖。称G的所有顶点覆盖中顶点个数最少的覆盖为最小点覆盖。

区间DP(转自区间DP

例题:石子归并。

  描述 有N堆石子排成一排,每堆石子有一定的数量。现要将N堆石子并成为一堆。合并的过程只能每次将相邻的两堆石子堆成一堆,并将新的一堆石子数记为该次合并的得分。

  给组测试数据 

     输入   4    表示有4堆石子

       4 4 5 9  表示每堆石子的个数

     输出  54         表示最大的得分   

  分析:主要思想就是一个一个累加:4 4 5 9 先看下去是我想知道dp[i][j]的最大值,i表示起始位置,j表示终止位置,所以我肯定是想求出dp[1][4]间的最大值但是我从1到4可是如图这三种截取方法,所以我先从小的开始记录。

dp[1][1]=4;dp[2][2]=4;dp[3][3]=5;dp[4][4]=9。然后我在长度为2的时候记录,dp[1][2]=4+4=8,dp[2][3]=8+5=14;dp[3][4]=14+9=23;这样加起来的值就是8+14+23=45;但是如果我反过来呢?dp[1][2]=5+9=14;dp[2][3]=14+4=18;dp[3][4]=18+4=22;这样加起来的值就是14+18+22=54。很明显,54就是所求的最大值。

如图,如果我相求圈着的这个三个的值,我完全可以有图上这两种分割,并且分割出来的值是已经知道的了。

动态规划的思想:先两两合并,在三三合并,最后再NN合并,在合并过程中,较大的区间可以拆分成已经的小区间进行计算,省时又省力。比如,我可以在三三合并的时候可以拆分成一一还有二三相加。即把当前阶段的合并方法细分成前一阶段已计算出的方法,选择其中的最优方案。

具体来说我们应该定义一个数组dp[i,j]用来表示合并方法,i表示从编号为i的石头开始合并,j表示所求区间的结尾,sum表示的是石头的数量。

对于上面的例子来说,

   第一阶段:dp[1][1],dp[2][2],dp[3][3],dp[4][4] 因为一开始还没有合并,所以这些值应该全部为0。

    第二阶段:两两合并过程如下,其中sum(i,j)表示石头的数量,即从i开始数j个数的和

              dp[1,2]=dp[1,1]+dp[2,2]+sum[1,2];

    dp[2,3]=dp[2,2]+dp[3,3]+sum[2,3];

    dp[3,4]=dp[3,3]+dp[4,4]+sum[4,4];

    第三阶段:三三合并可以拆成两两合并,拆分方法有两种,前两个为一组或后两个为一组

     dp[1,3]=dp[1,2]+dp[3,3]+sum[1,3]或dp[1,3]=dp[1,1]+dp[2,3]+sum[1,3];取其最优

 dp[2,4]=dp[2,2]+dp[3,4]+sun[2,4]或dp[2,4]=dp[2,3]+dp[3,3]+sum[2,4];取其最优

    第四阶段:四四合并的拆分方法用三种,同理求出三种分法的得分,取其最优即可。以后第五阶段、第六阶段依次类推,最后在第六阶段中找出最优答案即可。

  动态方程为dp[i][j]=dp[i][k]+dp[k+1][j]+sum[i][j];

 参考代码。

#include <iostream>
#include <cstdio>
#include <cstring>

using namespace std;

//#define MAX 999999

int main ()
{
    int dp[210][210],sum[210][210],a[210];
    int n;
    while(~scanf("%d",&n))
    {
        //memset(dp,MAX,sizeof(dp));
        for (int i=1; i<=n; i++)
            scanf("%d",&a[i]);
        for (int i=1; i<=n; i++)
        {
            dp[i][i]=0;//初始化为0
            sum[i][i]=a[i];//将每堆石子的个数赋值进来
        }
        for (int len=1; len<n; len++)//按长度从小到大枚举
        {
            for (int i=1; i<=n&&i+len<=n; i++)//i表示开始位置
            {
                int j=len+i;                    //j表示长度为len的一段区间的结束位置
                for (int k=i; k<j; k++)         //用k来表示分割区间
                {
                    sum[i][j]=sum[i][k]+sum[k+1][j];
                    if (dp[i][j]<dp[i][k]+dp[k+1][j]+sum[i][j])
                        dp[i][j]=dp[i][k]+dp[k+1][j]+sum[i][j];
                    //cout<<i<<" "<<j<<" "<<sum[i][j]<<" "<<k<<" "<<dp[i][j]<<endl;
                }
            }
        }
        cout<<dp[1][n]<<endl;
    }
    return 0;
}

另一种:
一条直线上有N堆石子,现在要将所有石子合并成一堆,每次只能合并相邻的两堆,合并花费为新合成的一堆石子的数量,求最小的花费。

1堆,花费为0 
2堆,花费为sum[2] 
3堆,花费为min(a[1] + a[2], a[2] + a[3]) + sum[3] 
如果我们有n堆,合并的最后一次操作一定是从两堆合并成一堆,

第一种模型就是将大区间从任意位置分成两个小区间
规定dp[i][j]为合并第i堆到第j堆的最小花费 
DP方程为: 
dp[i][j] = min(dp[i][k] + dp[k+1][j]) + sum[j] - sum[i-1] (i <= k < j)    sum[i]为前i项和
 

memset(dp, 0x3f, sizeof(dp));
for (int i = 1; i <= n; i++) 
    dp[i][i] = 0;
for (int len = 2; len <= n; len++)
{
    for (int i = 1, j = len; j <= n; i++, j++)
    {
        for (int k = i; k < j; k++)
        {
            if(dp[i][j] > dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1])
                dp[i][j] = dp[i][k] + dp[k+1][j] + sum[j] - sum[i-1];
        }
    }
}
printf("%d
", dp[1][n]);

树形DP

树形 DP,即在树上进行的 DP。树形DP具体还是要根据题来说,就根据E~H题来讲解。

例题

A:POJ-2955 Brackets:给一个括号组成的字符串,问最多能匹配多少个括号 像([)]这样的字符串匹配度为2,但是像()[]、[()]的字符串匹配度为4,也就是说括号具有分隔作用。长度为1的串匹配度 0 。长度为2的串匹配度 0 或 2。

规定dp[i][j]为合并第i到第j个字符的最大匹配度 
长度为n时,我们可以先检测a[i]和a[j]是否匹配, 
匹配dp[i][j] = dp[i+1][j-1] + 2 
不匹配,那我们可以按第一种模型处理,从任意位置分成两个区间

#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
int dp[105][105];
int main()
{
    char str[105];
    while(scanf("%s",str+1)!=EOF){
        memset(dp,0,sizeof(dp));
        if(str[1]=='e')
            break;
        int n=strlen(str+1);
        for(int i=1;i<=n;i++)
            dp[i][i]=0;
        for(int len=1;len<=n;len++){
            for(int i=1,j=len;j<=n;i++,j++){
                if((str[i]=='('&&str[j]==')')||(str[i]=='['&&str[j]==']'))
                    dp[i][j]=dp[i+1][j-1]+2;
                for(int k=i;k<j;k++)
                    dp[i][j]=max(dp[i][j],dp[i][k]+dp[k+1][j]);
            }

        }
        cout<<dp[1][n]<<endl;
    }
}

B:UVA-10003 Cutting Sticks:题目的意思是有个长度为l的木棍,这个木棍需要加工n次,给出n次加工的位置,每次加工的花费是当前木棍的长度,求加工的最小花费。
与矩阵连乘多少有点类似,不过这里不是枚举最后一次切割的位置,这里最后一次切割的花费和之前的切割次序是相关的。所以不能这样枚举,但我们发现第一次切割时的花费是固定的,可以尝试一下枚举第一次切割的位置。用dp[i][j]代表第i次切割到第j次切割的最优值,我们可以得到这一段的总长度是pos[j+1]-pos[i-1],这里注意边界,要特殊处理下,然后枚举这一段第一次切割的位置k,转移方程为dp[i][j]=min(dp[i][k-1]+dp[k+1][j]+pos[j+1]-pos[i-1]),其中k为从i到j。这里也要注意边界的问题,当i大于j的时候dp[i][j]应该为0,其他的情况都应该初始化为一个足够大的值。只要注意号边界就可以了。这里有两种写法,一种是记忆化搜索,另一种是递推。
 

#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int INF=0x3f3f3f3f;
int dp[105][105];
int a[105];
int main()
{
    int s;
    while(scanf("%d",&s)&&s){
        int n;
        scanf("%d",&n);
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        a[0]=0;a[++n]=s;
        for(int len=2;len<=n;len++){
            for(int i=0,j=len;j<=n;i++,j++){
                int tmp=INF;
                for(int k=i+1;k<j;k++)
                    if(tmp>dp[i][k]+dp[k][j]+a[j]-a[i])
                         tmp=dp[i][k]+dp[k][j]+a[j]-a[i]; //a[j]-a[i]即转移要付出的代价
                 dp[i][j]=tmp;
            }
        }
        printf("The minimum cutting is %d.
",dp[0][n]);
    }
}

C:POJ-1651 Multiplication Puzzle:题意:从一堆放好的卡片中取一个卡片(不能取开头和结尾的卡片),每次取出一个卡片的时候,就计算一个值:该卡片上的数字和他左右卡片的数字的乘积。重复这个操作直到只剩下两个卡片,将所有得到的值加起来。求这个值的最小值是多少。 题意:dp[i][j]表示从第i张到第j张的最小的值,状态转移方程dp[i][j] = min(dp[i][j],dp[i][k] + dp[k][j] + a[i] * a[k] * a[j]),k表示最后一个取走的卡牌,i<k<j。

#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
#define maxn 105
#define INF 0x3f3f3f3f
int main()
{
    int dp[maxn][maxn];
    int a[maxn];
    int n;
    while(cin>>n){
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++)
           cin>>a[i];
        for(int l=2;l<=n;l++){
            for(int i=1,j=l+i;j<=n;j++,i++){
                    dp[i][j]=INF;
                for(int k=i+1;k<=j-1;k++){
                    dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+a[i]*a[j]*a[k]);
                }
            }
        }
        cout<<dp[1][n]<<endl;
    }
    return 0;
}

D:HDU-3506 Monkey Party:(石子合并的升级版)题意: 一群猴子围成一圈,每个猴子有一个权值,然后每次选相邻的两个猴子交友,代价就是两个权值和,a和b交友之后呢,a的朋友和b的朋友就是朋友了,代价是a,b的朋友的总和,让你求最小代价。分析: 这个和经典的取石子问题及其相似,不同的是,这个可以围成一个圈,我们只要预处理下 n*2即可,然后用四边形不等式优化下,不懂的点这里,注意清零即可。(其实没必要明白优化原理,只需要知道使用条件和s数组的初始化和更新即可。

#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=2e3+10;
#define INF 0x3f3f3f3f
int dp[maxn][maxn];
int a[maxn],s[maxn][maxn],sum[maxn];
int n;
int main()
{

    while(cin>>n){
        sum[0]=0;
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++){
           cin>>a[i];
           sum[i]=sum[i-1]+a[i];
        }
        for(int i=0;i<=2*n;i++)
            s[i][i]=i;
        for(int i=n+1;i<=2*n;i++)
            sum[i]=sum[n]+sum[i-n];
        for(int l=2;l<=n;l++){
            for(int i=1,j=l+i-1;j<=2*n;j++,i++){
                    dp[i][j]=INF;
                for(int k=s[i][j-1];k<=s[i+1][j];k++){
                    if(dp[i][j]>dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]){
                        dp[i][j]=dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1];
                        s[i][j]=k;
                    }
                }
            }
        }
        int ans=INF;
        for(int i=1;i<=n;i++)
            ans=min(ans,dp[i][i+n-1]);
        cout<<ans<<endl;
    }
    return 0;
}

E:POJ-2342 Anniversary party:题意:某公司要举办一次晚会,但是为了使得晚会的气氛更加活跃,每个参加晚会的人都不希望在晚会中见到他的直接上司,现在已知每个人的活跃指数和上司关系(当然不可能存在环),求邀请哪些人(多少人)来能使得晚会的总活跃指数最大。题解:树形dp.dp[node][1] += dp[i][0]; i是node的下属,1表示上司(node)去,0表示下属(i)不去dp[node][0] += max(dp[i][1],dp[i][0]);这个题看代码应该就能看明白。

#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=6e3+10;
int dp[maxn][2];
int father[maxn],vis[maxn];
int n;
void treedp(int node)
{
    vis[node]=1;
    for(int i=1;i<=n;i++){
        if(!vis[i]&&father[i]==node){
            treedp(i);
            dp[node][1]+=dp[i][0];  //上司来,下属不来
            dp[node][0]+=max(dp[i][0],dp[i][1]);   //上司不来,下属来不来都可以
        }
    }
}
int main()
{
    int f,c,root;
    while(scanf("%d",&n)==1){
        memset(dp,0,sizeof(dp));
        memset(vis,0,sizeof(vis));
        memset(father,0,sizeof(father));
        for(int i=1;i<=n;i++)
            scanf("%d",&dp[i][1]);
        root=0;
        while(scanf("%d %d",&c,&f)&&c&&f){
            father[c]=f;
            root=f;
        }
        while(father[root])
            root=father[root];
        treedp(root);
        printf("%d
",max(dp[root][0], dp[root][1]));
    }
}

F:HDU-1054 Strategic Game:(最小顶点覆盖问题)题意:一棵树,要放置哨兵,要求最少放置多少哨兵能监视到所有的结点。

解题思路:放置哨兵无非两种情况,要嘛放,要嘛不放,我们可以用dp[i][1]来表示第i个结点放置哨兵,dp[i][0]来表示第i个结点不放置哨兵,我们可以从上往下,从左往右来遍历树,所以这就用到了树形DP的知识,我们很容易知道,如果父亲结点没放哨兵,那么子结点肯定要放置哨兵,如果父亲放置了哨兵,那么子结点可以考虑放或者不放。所以很容易写出状态转移方程dp[v][1] += min(dp[u][1],dp[u][0]),dp[v][0] += dp[u][1],由于子结点可能有多个,我们要依次从左到右遍历所以我们必须开一个brother来存放它前一个兄弟结点,然后只要DFS递归的来从上往下遍历就可以得解了。(外附二分图匹配做法)

#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<string>
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=6e3+10;
int dp[maxn][2];
int father[maxn],son[maxn],brother[maxn];
int n;
void treedp(int root)
{
    dp[root][0]=0;
    dp[root][1]=1;
    int f=son[root];
    while(f!=-1){
        treedp(f);//纵向遍历
        dp[root][1]+=min(dp[f][0],dp[f][1]);
        dp[root][0]+=dp[f][1];
        f=brother[f];//切换到下一个兄弟结点,横向遍历
    }
}
int main()
{
    int m,root,num,cnt,num1;
    while(scanf("%d",&m)==1){
        memset(son,-1,sizeof(son));
        memset(father,0,sizeof(father));
        for(int i=0;i<m;i++){
            scanf("%d:(%d)",&num,&cnt);
            for(int j=0;j<cnt;j++)
            {
                cin>>num1;
                brother[num1]=son[num];
                son[num]=num1;
                father[num1]=1;
            }
        }
        for(int i=0;i<m;i++){
            if(!father[i]){
                root=i;
                break;
            }
        }
        treedp(root);
        printf("%d
",min(dp[root][0], dp[root][1]));
    }
}

二分图匹配做法

#include <bits/stdc++.h>
using namespace std;
const int maxn=1500+7;
int n;
vector<int> G[maxn];
int nxt[maxn],visited[maxn],cnt;
int find(int x){
	for(int i = 0;i<G[x].size();++i){
		int v=G[x][i];
		if(!visited[v]){
			visited[v]=1;
			if(nxt[v]==-1||find(nxt[v])){
				nxt[v]=x;
				return 1;
			}
		}
	}
	return 0;
}
int match(){
	int ans=0;
	memset(nxt,-1,sizeof(nxt));
	for(int i = 0;i<n;++i){
		memset(visited,0,sizeof(visited));
		if(find(i)) ans++;
	} 
	return ans;
}
int main(int argc, char** argv) {
	while(~scanf("%d",&n)){
		for(int i = 0;i<=1500;++i) G[i].clear();
		for(int i = 0;i<n;++i){
			int x,y,num;
			scanf("%d:(%d)",&x,&num);
			if(!num) continue;
			while(num--){
				scanf("%d",&y);
				G[x].push_back(y);
				G[y].push_back(x);
			} 
		}
		printf("%d
",match()/2);
	}
	return 0;
}

G:ZOJ-3201 Tree of Tree:题意:有一个n个点的树,每个点有个权值,问大小为k的子树的最大权值和为多少。题解:设dp[x][j]为x点大小为j的子树的最大权值和,则dp[x][j] = max(dp[x][j], dp[x][k] + dp[v][j - k]);任选一点为根往下搜索即可!

#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
using namespace std;
const int MAXN=105;
vector<int> tree[MAXN];  
int n,k;
int dp[MAXN][MAXN];  //dp[i][k]表示以i为根节点的最大的k个节点的和 
int vis[MAXN];
int  maxn;
void dfs(int u)
{
	vis[u]=1;
	for(int i=0;i<tree[u].size();i++)
	{
		int v=tree[u][i];
		if(!vis[v])
		{
			dfs(v);
			for(int j=k;j>=1;j--)  //每个根节点不仅要找出k个节点和的最大值,还要算出1到k-1的 对应的节点和的最大值,是为了给后面的根节点用 
			for(int m=1;m<j;m++)
			dp[u][j]=max(dp[u][j],dp[u][m]+dp[v][j-m]); //根节点的m个和 加上 儿子的j-m个和 一共j个和  不断地更新dp[u][j] 
		}
	}
}
int main()
{
  
    while(scanf("%d%d",&n,&k)!=EOF)
    {
    	maxn=0;
    	for(int i=0;i<n;i++)    //每次一定不要忘记将树清空!!! 
    	tree[i].clear();
	    memset(dp,0,sizeof(dp));
	    memset(vis,0,sizeof(vis));
	    for(int i=0;i<n;i++)
	    	scanf("%d",&dp[i][1]);
	    for(int i=0;i<n-1;i++)
	    {
	        int u,v;
	        scanf("%d%d",&u,&v);
	        tree[u].push_back(v);
	        tree[v].push_back(u);
	    }
	    dfs(0);
	    for(int i=0;i<n;i++)
	    {
	    	if(dp[i][k]>maxn) maxn=dp[i][k];
	    }
	    printf("%d
",maxn);
 
    }
   
    return 0;
}

H:POJ-3659 Cell Phone Network:(最小支配集问题)有一棵树,问最少取树中的多少个节点,能使其与树的其他节点都联通。(一个点只与其有边相连的点联通)。具体题解可看代码中的注释。

#include <iostream>
#include <cstdio>
#include <vector>
#include <algorithm>
using namespace std;
#define MAXN 10010
#define INF 0x3f3f3f3f
vector <int> G[MAXN];
int dp[MAXN][MAXN];   //dp[i][0]表示取i结点时的最小结点数
                      //dp[i][1]表示不取i结点时,i被其儿子覆盖时的最小结点数
                      //dp[i][2]表示不选点i,i被其父亲覆盖时的最小结点数
                      //树形DP状态转移从叶子结点开始向根不断转移,所以用深搜。
                      //搜到叶子结点就结束。
                      //转移的时候可以将结点以下给圈起来。
                      //取与不取,树上背包
int vis[MAXN];
void dfs(int u)
{
    vis[u]=1;
    int i;
    int flag=1,tmp=INF;
    for(i=0;i<G[u].size();i++)
    {
        int v=G[u][i];
        if(!vis[v])                             //额,图的最小支配集吧
        {
            dfs(v);
            dp[u][0]+=min(dp[v][0],min(dp[v][1],dp[v][2]));
            dp[u][2]+=min(dp[v][0],dp[v][1]);   //不取的话,肯定没有父亲这个选项啊
                                                //父亲只有一个,所以不必做过多的讨论
                                                //儿子比较多,可以取所有儿子,但必须选一个儿子
                                                //但选了一个儿子就要做标记
            if(dp[v][0]<=dp[v][1])
           {
              dp[u][1]+=dp[v][0];
              flag=0;
           }
            else
           {
              dp[u][1]+=dp[v][1];
              tmp=min(tmp,dp[v][0]-dp[v][1]);
           }
        }
    }
    if(flag)    //还没有选儿子,加上这个差值,补回一个儿子
    dp[u][1]+=tmp;
    return ;
}
int main()
{
    int n;
    int i=0;
    int u,v;
    scanf("%d",&n);
    for(i=1;i<n;i++)
    {
         scanf("%d%d",&u,&v);
         G[u].push_back(v);
         G[v].push_back(u);
    }
    for(i=0;i<=n;i++)         //dp[i][0]的i从0开始,因为存在下标为0的结点
    {                        //再次强调结点的下标一定是0~n-1
        dp[i][0]=1;
        dp[i][1]=0;
        dp[i][2]=0;
    }
    if(n==1)
    printf("1
");
    else
    {
      dfs(1);
      printf("%d
",min(dp[1][0],dp[1][1]));
    }
    return 0;
}
原文地址:https://www.cnblogs.com/shmilky/p/14089043.html