动态规划DP 合辑

--------本文将梳理一下我见过的DP(动态规划)常见的类型

【目录】:

①简单递推||线性型dp

②背包

③区间dp

④状态压缩dp

⑤字符串dp

⑥数位dp

⑦树形dp

⑧概率dp

 

----------总概

  动态规划dynamic programming (DP) 是一种经常用的算法,她满足以下两个性质:

A重叠子问题

B最优子结构

  她的本质思想就是把问题划分为更小的子问题,然后把子问题的结果记录下来以后可以利用;或者说,以空间换时间。

一般有两种结构来做DP,分别为顺推(递推)和逆推,后者也叫记忆化搜索。

 

-----------①简单递推||线性型dp

此种dp较为简单,如斐波那契数,递归版本:

//递归
int fib(int n){
    if(n==0||n==1)return 1;
    else return fib(n-1)+fib(n-2);
}
 

       很明显,递归版本重复计算了很多子问题,那么我们就可以用到dp的思想,开一个表,把每次计算的值存下来,然后下次要计算一个值时,先在表里面找有没有对应的值,如果有的话直接用,没有的话则计算存表.如下:

//自顶向下(记忆化搜索)
int dp[N]={0};//表用来存计算过的值
int fib(int n){
    if(dp[n]!=0)return dp[n];
    else return dp[n]=fib(n-1)+fib(n-2);
}

也可以写成更紧凑的递推形式:

int dp[N]={0};
int fib(int n){
    dp[0]=d[1]=1;//初始化
     for(int i=2;i<=n;i++){
        dp[i]=dp[i-1]+dp[i-2];
    }
    return dp[n];
}

另外,dp问题有时候也可以用滚动数组来优化空间复杂度,因为有些决策只用到前面几个状态,而再之前的决策不会再用到,假设当前每一个决策最多只用到前面某K个状态,那么我们开一个大小为K的表,然后在计算出当前状态i后,迭代更新存下来的表,一般用取模来或异或来解决下标.斐波那契的例子每一个状态只用到前面两个状态,那么可以开一个大小为3的表,如下:

//滚动优化
int dp[3]={0};
int fib(int n){
    dp[0%3]=dp[1%3]=1;
    for(int i=2;i<=n;i++){
        dp[i%3]=dp[(i-1)%3]+dp[(i-2)%3];
    }
    return dp[n%3];
}
 
这类问题常见的还有:
  eg1: 给你一个二维矩阵,让你从左上角走到左下角,每次只能走规定的方向,求走过的数总和最小(大)是多少.只要开个二维数组dp[i][j],然后每次从可以走的几个方向推过来即可.
  eg2: 最大连续子串和.给你个序列,求出最大的连续字串和. 那么记dp[i]表示第i个数加入目标串时的最大字串和,那么dp[i]=max(sq[i],dp[i-1]+sq[i].类似的可以扩展到矩阵上,求最大子矩阵和
  eg3:硬币组成问题.给你k种币值的硬币,用最小数目的硬币凑出价值n. 此种题有时候可以用贪心来解,不过更普遍的解法是用dp. 我们可以记dp[i]表示凑出价值为i时用到最少数目的硬币,那么dp[i]=min{dp[i-v[j]]+1|1<=j<=k}
  eg4:lis,最长不降子序列问题。就是在一个序列上,找出最长的子序列(不要求连续)满足不降的大小关系。则可以记dp[i]表示以第i个数结尾的最长不降子序列,然后类似于eg3,结果就是max{dp[1..n]}. -----lis是有比较深的数学本质,我见过好几个问题的本质其实都是lis,另外lis可以利用单调队列的思想把复杂度优化到nlogn,有兴趣者自行搜索。
  
第一类dp问题比较简单,状态转移往往不难想。
 
 
 
-----
 
  
 
  
 
 
  
 
 
  
 
  
 
 
  
 
 
 
-----
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
  
 
 
  
 
 
  
 
 
  
 
  
 
  
 
  
 
  
 
 
  
//递推形式
#define INF 0x3f3f3f3f;
int p[N],dp[N][N];
int matrix_chain(int n){
    for(int i=1;i<=n;i++)dp[i][i]=0;//初始化,因为只有一个矩阵时候乘法数为0

    for(int k=2;k<=n;k++){
        for(int i=1;i<=n-k+1;i++){
            int j=i+k-1;
            dp[i][j]=INF;
            for(int t=i;t<=j-1;t++){
              dp[i][j]=min(dp[i][j],dp[i][t]+dp[t+1][j]+p[i-1]*p[t]*p[j]);
            }
        }
    }
    return dp[1][n];
}

也可以写成记忆化搜索形式,更为简洁。

 

------④状态压缩dp

此种dp一般用在集合上,而且集合比较小,那么我们就可以用二进制来表示集合,把对集合里每一个元素的选取与否对应到一个二进制位里,从而把状态压缩到一个整数,并写出相应的转移方程.

最经典的当属TSP旅行商问题。

TSP:给一个n个顶点的带权有向图,d[i][j]表示从i到j的权值,INF表示没有边,要求从顶点0出发,经过每一个顶点恰好一次后回到0顶点。求所经过的路径权值总和最小是多少?(n<16)

-解法:以dp[V][vi]表示访问V集合顶点各一次并且以vi为终点的最短路径和。

则有dp[0][0]=0;

     dp[V][vi]=min(dp[V/vi][vj]+d[j][i] | j ∈V)

结果就是dp[S][0];

写成记忆化搜索就不用想蛋疼的递推顺序啦:

//TSP 
#define INF=0x3f3f3f3f;
int n;//n 表示顶点个数,从0开始
int dp[1<<N][N];//存结果
int d[N][N];//距离矩阵
int dfs(int s,int v){
    if(dp[s][v]!=-1)return dp[s][v];
    int res=INF;
    for(int u=0;u<n;u++){
        if(u!=v && (s>>u & 1)){
            res=min(res,dfs(s^(1<<u),u)+d[u][v]);
        }
    }
    return dp[s][v]=res;
}
void TSP(){
    memset(dp,-1,sizeof(dp));
    dp[0][0]=0;
    int ans=dfs((1<<n)-1,0);
}

 

-------⑤字符串dp

顾名思义,此种dp就是在字符串上进行,其实也没什么区别啦。

有几个经典的如 :LCS最长公共子序列  最长回文字串  最小编辑距离 等等

下面介绍其中的LCS,并介绍其优化方法

LCS:设X=<x1,x2,…,xi>和Y=<y1,y2,…,yj>为两个子序列,并设Z=<z1,z2,…,zk>为X和Y地任意一个LCS.

我们记dp[i][j]为X前i位和Y前j位构成的最长公共子序列,则有

a.xi=yj,则dp[i][j]=dp[i-1][j-1]+1

b.xi!=yj,则dp[i][j]=max(dp[i-1][j],dp[i][j-1])

然后两重循环扫过去最后dp[x_size][y_size]则为结果,代码如下:

//LCS 时间复杂度O(NM),空间复杂度O(NM)
char x[N],y[M];//下标从1开始
int dp[N+1][M+1];
int LCS(){
    memset(dp,0,sizeof(dp));        
    for(int i=1;i<=N;i++){
        for(int j=1;j<=M;j++){
            if(x[i]==y[j])dp[i][j]=dp[i-1][j-1]+1;
            else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
        }
    }
    return dp[N][M];
}

对空间进行优化成O(N),因为每个状态dp[i][j]对i只会用到上一层的值,因此我们可以数组大小开为dp[2][M],然后每次取模2即可。

时间复杂度进行优化成O(nlogn),可以把lcs转化成lis,方法是对x中出现的每个字符,记下所有在y中出现下标,按降序排列,然后把这个下标组合起来,现在最长公共子序列的大小就是这个下标组合的最长不降子序列的大小。为什么这样可以呢?想一想还真的可以……只能说你们太屌了,不过据说这个方法在某些情况下会退化,慎用。

其他几种求字符串各种长度的处理方法也类似。

 

------⑥数位dp

在数的位上进行dp,一般是比较大的数,常见情况是要求出现或不出现某些数字。

比如hdu上那道让你求[a,b]上不出现4和和62的数字有多少个。那么可以先求出[0,n].

问题:那[0,n)不出现4和62的数字个数怎么求呢?

如果n大时候,枚举可能会tle,这里就用到数位dp这个东西了。记dp[i][j]表示i位数,以j开头满足要求的数字个数。然后从高到低枚举每一位,比如n=24,那么

对于十位有 0_ 1_          _表示可以填0~9

然后对于个位,这时候十位已经确定为2了,则有20~23(这里是23,不是24,没错!!!)

并且我们可以先预处理一下对于某个位 是 _ 时候,总共有多少个满足条件的数字,那么求[0,n)满足条件的完整代码如下:

//!!![0,n)!!!满足不含4和62的数字个数 
const int N=10;
int dp[N][N];
void init()//预处理 
{
    memset(dp,0,sizeof(dp));
    dp[0][0] = 1;
    for(int i=1;i<N;i++){
        for(int j=0;j<10;j++){//第i位可能出现的数
            for(int k=0;k<10;k++){//第i-1位可能出现的数
                if(j!=4&&!(j==6&&k==2))
                dp[i][j]+=dp[i-1][k];
            }
        }
    }
}
int solve(int n)
{
    init();
    int digit[10];
    int len = 0;
    while(n){
        digit[++len]=n%10;
        n/=10;
    }
    digit[len+1]=0;//必须加入,否则下面的判断可能出错 
    int ans = 0;
    for(int i=len;i>0;i--){
        for(int j=0;j<digit[i];j++){
            if(j!=4&&!(digit[i+1]==6&&j==2))
            ans+=dp[i][j];
        }
        if(digit[i]==4||(digit[i]==2&&digit[i+1]==6))
        break;
    }
    return ans;
}
 

知道上面为什么不包含n吗?因为我们从高位推到低位时候,对第i位,是假设第i+1位确定为digit[i+1],然后第i位从0枚举到digit[i]-1,并不包含i位,这样到第1位时候,也就不包含n了,仔细想一想吧~   哦不,等等,但是我们要求[0,n]啊,妈蛋那你调用时候就调用solve(n+1)嘛。

 

---------⑦树形dp

                                            (妈蛋,卡在树dp卡挺久了,感觉有些懂了)

树上的dp比较简单的是求树的最大独立集这些,个人感觉和普通dp没啥区别。

感觉另一类类似于分组背包的dp才计较正常的归属于树dp里面。

首先,先来了解一下分组背包(不懂分组背包?->背包九讲<-)的循环结构

//
f[k][v]表示前k组物品花费费用v能取得的最大权值
f[k][v]=max{f[k-1][v],f[k-1][v-c[i]]+w[i]|物品i属于组k}
分组背包一维数组的伪代码:
for 所有的组k
    for v=V..0 //这里用到滚动数组,所以要逆序
        for 所有的i属于组k
            f[v]=max{f[v],f[v-c[i]]+w[i]}

然后呢,你就可以看ural 1018 了,题意是这样的:有一颗二叉苹果树,每段树枝上都有一定数量的苹果,让你从1节点开始,保留q条树枝,求出保留的最大苹果数量。把每条边的苹果数量压到节点上,这样相当于保留Q=q+1个节点,1节点苹果数为0.这样比较容易处理。

A.正常想法:以对于根节点,左子树保留k个节点,则右子树保留Q-k-1个节点。这样记忆化搜索下去就可以了。

B.转化成更一般的分组背包的模型。dp[i][j]表示以i为根节点,保留j个节点可以得到的最大数量苹果。那么对于i的每个儿子v(这里更一般情况,所以儿子数可以不止两个了),相当于一组背包,那么给这个儿子v分配k个节点(相当于分配的背包容量),剩下j-k-1个节点(剩余背包容量)。如此那就可以像分组背包那样推过去了。而树的dp我们一般写成记忆化形式,所以先对子节点各种节点数(背包容量)都求出来,然后在推过去~哦也。来段代码吧

ural 1018 apple binary tree

//ural 1018  可用于一般的树结构

#include<iostream>
#include<iomanip>
#include<cstring>
#include<stdio.h>
#include<map>
#include<cmath>
#include<algorithm>
#include<vector>
#include<stack>
#include<fstream>
#include<queue>
#define rep(i,n) for(int i=0;i<n;i++)
#define fab(i,a,b) for(int i=(a);i<=(b);i++)
#define fba(i,b,a) for(int i=(b);i>=(a);i--)
#define MP make_pair
#define PB push_back

using namespace std;
const int INF=0x7ffffff;
const int N=105;
int n,q,u,v,w;
int dp[N][N]={0};
int a[N][N];
void dfs(int u,int fa){
    fab(v,1,n){
        if(a[u][v]!=-1&&v!=fa){
            dfs(v,u);
            fba(j,q,1){
                fab(k,1,j-1){
                    dp[u][j]=max(dp[u][j],dp[v][k]+dp[u][j-k]+a[u][v]);
                }
            }
        }
    }
}
int main(){
    ios::sync_with_stdio(false);
    rep(i,N)rep(j,N)a[i][j]=-1,dp[i][j]=0;
    cin>>n>>q;
    rep(i,n-1){
        cin>>u>>v>>w;
        a[u][v]=a[v][u]=w;
    }
    q++;
    dfs(1,-1);
    cout<<dp[1][q]<<endl;
    return 0;
}

 

来段复杂点的吧。。

poj 2486  又是苹果树。题意是这样的:一棵节点数为n的树,每个节点都放有一些苹果,从根节点1开始走,每走一条边算一步,每经过一个节点就能吃掉这个节点的苹果,问走m步最多能吃几个苹果?

因为走一步算一步,所以我们应该再开多一维来表示目前的位置。

那么可以dp[i][j][0]表示i节点走j步,最后回到i节点吃到数量最多的苹果有多少。

          dp[i][j][1]表示i节点走j步,不回到i节点吃到数量最多的苹果有多少。

回到i节点的状态转移比较容易想,那么不回到i节点的呢?你自己想吧。。噗

别打我。。核心代码如下:

void dfs(int u,int fa){
    vis[u]=1;
    rep(i,k+1)dp[u][i][0]=dp[u][i][1]=a[u];
    rep(i,g[u].size()){
        int v=g[u][i];
        if(v!=fa&&!vis[v]){
            dfs(v,u);
            fba(i,k,0){//逆序哦
                fab(j,0,i){
                    if(i-j-2>=0)dp[u][i][0]=max(dp[u][i][0],dp[v][j][0]+dp[u][i-j-2][0]);
                    if(i-j-2>=0)dp[u][i][1]=max(dp[u][i][1],dp[v][j][0]+dp[u][i-j-2][1]);
                    if(i-j-1>=0)dp[u][i][1]=max(dp[u][i][1],dp[v][j][1]+dp[u][i-j-1][0]);
                }
            }
        }
    }
}

(终于可以写最后一个类型了冰冻

 

--------⑧概率dp

     

一般有求概率和期望两种。

其中,求概率的话,一般从前往后推,假设dp[i]表示某一点的概率值,从j=x..y个转移而来而且对于的概率分别为px…py那么跟普通的转移方程一样

dp[i]={function(dp[j]*p[i]) }具体什么的再看要求。

然后,求期望的话,一般从后往前推。可以设dp[i]表示从i到目标状态的期望,然后结果就是

dp[start],再由转移方程从目标状态推回来。

 

而同时,概率dp时候,如果转移方程出现环的时候,往往和高斯消元有关了,这个待撸吧。

 

,待续

 

 

                                                                                     --wanggp

转载于:https://www.cnblogs.com/wanggp3/p/3730635.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值