动态规划常用类型精讲——从入门到入土

文章目录

  • 一、动态规划设计方法一般模式
    • 1、划分阶段
    • 2、确定状态和状态变量
    • 3、确定决策和状态转移方程
    • 4、确定边界条件
    • 5、设计并实现程序
  • 二、线性模型
    • 例题1:*最长单调递增子序列*
  • 三、背包DP
    • *0-1背包*
    • *完全背包*
  • 四、区间DP
    • 例题1:*合并石子*
  • 五、数位DP
    • 例题1:*烦人的数学作业*
  • 六、树状DP
    • 例题1:*树上最长链*
  • 七、状态压缩DP
    • 例题1:*TSP 旅行商问题*

一、动态规划设计方法一般模式

1、划分阶段

将问题所给的过程,划分为若干相互联系的阶段

2、确定状态和状态变量

一个阶段包含若干的状态

3、确定决策和状态转移方程

决策就是每次选择性的行动。即从一个阶段的某一状态转换为另一阶段的某一状态。

4、确定边界条件

5、设计并实现程序

二、线性模型

线性模型即状态的排布是呈线性的。
DP中最常用的模型

例题1:最长单调递增子序列

数组a:给定序列
dp[i]:表示必须以a[i]截止的最长单调递增子序列的长度。
状态转移方程:dp[i]=max(dp[j]+1) (a[i]>a[j],1<=j 边界条件: 以a[1]截止的序列长度就为1 dp[1]=1;
代码实现:

#include 
#include 
#include 
using namespace std;

int main(){
    int n,a[105],m=0;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    //确定状态
    int dp[105]={0};
    //dp[i]:必须以a[i]截止的最长递增子序列
    //边界条件:
    //以a[1]截止的序列长度就为1
    dp[1]=1;
    //dp:
    for(int i=2;i<=n;i++){
            //保存最大值
            int k=0;
        for(int j=1;j<i;j++){
            //如果递增并且长度大于当前的最大值
            //正推(递推) dp[j]都在前面求出来了
            if(a[i]>a[j]&&dp[j]>k)
                //更新当前最大值
                k=dp[j];
        }
        //长度为当前保存最大值+1
        dp[i]=k+1;
        //边求边更新最大长度
        if(m<dp[i])
            m=dp[i];
    }
    printf("%d",m);
    return 0;
}

三、背包DP

经典背包问题:
N件物品,每件物品重量为wi,价值为ci,背包负重为V。求在背包容量一定条件下,使装入背包的物品的价值最大。
背包问题经典模型:0-1背包、完全背包、多重背包。
0-1背包:N件物品中每件物品只有一件要么装,要么都不装。
完全背包:N件物品中每件物品有无限件
多重背包:N件物品中第i件物品有n[i]件

0-1背包

状态:
dp[i][j]:在背包容量为j下,选择前i件物品 获得的最大价值。
状态转移方程:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i])
考虑第i件物品选和不选,哪个产生的价值更大。
代码实现:

#include 
#include 
using namespace std;

int main(){
    int n,w[105],c[105],dp[105][105]={0},v;
    scanf("%d%d",&n,&v);
    for(int i=1;i<=n;i++) scanf("%d%d",&w[i],&c[i]);
    //初始化:
    //如果第一个物品的重量大于背包体积则无法装入,否则就能装
    for(int i=0;i<=v;i++) if(w[1]<=i) dp[1][i]=c[1];
    //dp:
    for(int i=2;i<=n;i++){
        for(int j=0;j<=v;j++){
            if(w[i]<=j){
                //第i件 要么选 要么不选
                dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+c[i]);
            }
        }
    }
    printf("%d",dp[n][v]);
    return 0;
 }

空间优化:将二维数组转化为一维数组
与二维数组类似,就是每次计算的值都覆盖上一次的值。
dp[j]:当前阶段背包体积为j时,所能装的物品的最大价值。
dp[j]=dp[j-w[i]]+c[i]
!!!但是注意,必须要逆推! 即体积:V~0。这样做保证了每个物品只能被取一次。
因为更新j一定要来自于j-w[i],如果正推的话j-w[i]已经被更新过了,就会导致数据错误!!!
代码实现:

#include 
#include 
using namespace std;

int main(){
    int n,w[105],c[105],dp[105]={0},v;
    scanf("%d%d",&n,&v);
    for(int i=1;i<=n;i++) scanf("%d%d",&w[i],&c[i]);
    //dp:
    for(int i=1;i<=n;i++){
        for(int j=v;j>=0;j--){
            if(w[i]<=j){
                //第i件 要么选 要么不选
                dp[j]=max(dp[j],dp[j-w[i]]+c[i]);
            }
        }
    }
    printf("%d",dp[v]);
    return 0;
}

完全背包

每种物品有无穷件。
状态定义与0-1背包一样,但是状态转移方程有所不同
dp[i][j]=max(dp[i-1][j],dp[i-1][j-k*w[i]]+k*c[i]) (0<=k*w[i]<=j)
时间复杂度很高,一样还要枚举k,所以我们可以类比0-1背包空间优化,来优化完全背包。
这里有个很巧妙的方法,当将0-1背包空间优化中的体积正着枚举就是完全背包了!太巧妙了!!!
你有点不信?我们看例子:
f[j]=max(f[j],f[j-w[i]]+v[i])
当计算前i件物品,背包体积为j时,我们计算f[j-w[i]]:此时f[j-w[i]]已经被更新过了,即为 在前i件物品选择,背包体积为j-w[i]。注意:它已经有可能包含了第i件物品!(它也有可能不取)
例如:

重量 价值
1   3
2   5
3   4

计算第二件物品:
dp[2]=5
dp[4]=dp[2]+5=10 选了两件2物品
dp[6]=dp[4]+5=10+5=15 3件2物品
计算第三件物品时:
dp[3]=dp[3-3]+4=4 选了第三件物品
dp[6]=dp[6-3]+4=dp[3]+4=8 选了两件第三件物品。


而我们使用逆推 0-1背包:
第一件物品:dp[1~6]=3
计算第二件物品:
dp[6]=dp[4]+5=3+5=8 这里dp[4] dp[2]都是选前一件物品的价值,
dp[3]=dp[1]+5=8 选了一件1物品 一件2物品
计算第三件物品
dp[6]=dp[6-3]+4=8+4=12 选了一件1物品 一件2物品 一件3物品

看出差别了吗?


例子:看这篇博客的例子,讲的很详细了。
Money System

四、区间DP

区间DP,即在区间中做动态规划。先得到小区间最优解再得到大区间的最优解。
一般的状态都为dp[i][j]:在区间i j上的最优解。

例题1:合并石子

题目简述:n堆石子,第i堆有a[i]个,合并时合并相邻两堆,产生的代价为两堆果子的总数,求合并成一堆后,最小代价。
输入: 3 1 2 3
输出:
9

解释:先合并1 2 代价为1+2=3 ,此时 石子为 3 3,再合并3 3 代价为3+(3+3)=9
如果先合并 2 3 代价为2+3=5 石子为 1 5 代价 5+(1+5)=11*
状态 :dp[i][j]:合并i j 区间所用的最小代价。
由于一堆肯定是由两堆来的 划分:dp[i][k],dp[k+1][j],即合并的代价为 a[i]+a[i+1]+……+a[j] 。所以,我们设sum[i]为a[1]到a[i]的和 即前缀和
因为他只能相邻的两堆合并。
状态转移方程:dp[i][j]=min(dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]) (i<=k<=j)
边界条件:当i等于j时,自己不用合并代价为0.
代码实现:

#include 
#include 
#include 
using namespace std;

int main(){
    int n,a[105],sum[105]={0};
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
            sum[i]=sum[i-1]+a[i];
    }
    int dp[105][105]={0};
    //枚举区间长度
    for(int len=2;len<=n;len++){
        //左端点 (右端点长度不能超过n)
        for(int i=1;i+len-1<=n;i++){
            //右端点
            int j=i+len-1;
            //先将代价设置为最大
            dp[i][j]=0x3f3f3f3f;
            for(int k=i;k<j;k++){
            	//取 i-j 代价最小的情况
                dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
            }
        }

    }
    printf("%d",dp[1][n]);
    return 0;
}

解释:我们一定要先从计算小区间,然后再计算大区间! 大区间状态一定要来自于小区间的状态。

五、数位DP

这类问题一般统计某个区间的数字符合某种条件。
例如:在[L,R]区间中,看有多少个数字符合能被7整除,并且数中含有3。
一般L R的规模都非常的大。
数位DP我们要考虑数字中的每一位。我们从高位向低位枚举(高位数范围决定低位数)。
例如 N=1024。
假设我们前两位枚举的是10,那么第三位只能选0~2。
如果枚举前三位是102,那么第四位只能选0~4。

技巧:
1、将区间变换成一个界限 [x,y]=f(y)-f(x-1)
2、从树的角度来考虑

例题1:烦人的数学作业

烦人的数学作业

题解等我研究研究,补上

六、树状DP

树形动态规划。状态图为一棵树。状态转移也发生在树上。父节点的值都通过子节点得到。

例题1:树上最长链

给定一棵树,边有权,计算一条最长链,要求时间复杂度为O(n)

找某个结点的最长链,就找它所有孩子的最长链,然后加上与这个孩子的边权值。
dp[i]:表示以i为根的子树中的最长链。
dp[i]只有两种情况:一种是以i为根结点最长,另一种是i为中间结点
i为根结点
动态规划常用类型精讲——从入门到入土_第1张图片

i为中间结点
动态规划常用类型精讲——从入门到入土_第2张图片
第一种情况即为树高问题很好解决,关键是第二种情况啊。
设状态dp[u][1/0]:0表示包括u的最长链,1表示包括u的次长链。二者应该严格不等。即路径没有重叠
如果为第一种情况,那么dp[u][0]即为最长
第二种情况 最长链 dp[u][0]+dp[u][1]-1(计算了两次u)

dp[u][0]=max(dp[v][0]+1) v∈son(u)
dp[u][1]:如果为叶子则为最小值,否则为secmax(dp[v][0])+1
secmax():取次大。
如何求次大呢?我们在遍历的时候dp[v][0]时,看其与dp[u][0]和dp[u][1]的关系,如果大于dp[u][0] 那么 dp[v][0]为 替换dp[u][0] dp[u][0]替换dp[u][1]
如何仅大于dp[u][1] 那么就替换dp[u][1]。

七、状态压缩DP

DP的核心在于状态的定义。
对于不易表示的状态,我们可以考虑使用状态压缩的思想来优化求解过程。

例题1:TSP 旅行商问题

问题描述:旅行家在n个点m条边的有向图中,从一个点出发,各个城市经历一次且仅一次,然后回到出发城市,求路径最短

我们分析题目可知,我们需要时刻知道 旅行家在哪个点它经过了哪些点
第一个很好维护。
第二个是一个集合,有2^n可能性,不太好表示。
我们可以将一个集合映射成 [0~2^n-1] 的数字, [0~2^n-1]范围的数字二进制表示最多为n位。
假设n=5.则
{1,3,5}=10101
{2}=00010

定义状态:dp[i][j]:表示所在点为i,已经经过点的集合对应下标为j时所走最短路径。
状态转移方程:
对于一条x到k代价为c的边
使用移位运算按位与按位或运算。

//先判断k点是否走过,
//判断j的第k-1位 是否等于0 等于0则没走过 
if(j&(1<<(k-1))==0)
	// | 按位或
	dp[k][j|1<<(k-1))]=dp[i][j]+c

先只会思想吧。

你可能感兴趣的:(每日一道算法题,算法,动态规划,DP)