动态规划


动态规划

动态规划特点

  1. 计数
    • 有多少种方式走到右下角
    • 有多少种方式选出k个数使得和是sum
  2. 求最大最小值
    • 从左上角走到右下角路径的最大数字和
    • 最长上升子序列长度
  3. 求存在性
    • 取石子游戏,先手是否必胜
    • 能不能选出k个数使得和是sum

动态规划组成部分

  1. 确定状态
    • 最后一步(最优策略种使用的最后一枚硬币ak
    • 化为子问题(最少的策略拼出最小的的面值27-ak
  2. 转移方程
    • f[X]=min{f[X-2]+1,f[X-5]+1,f[X-7]+1}
  3. 初始化条件和边界情况
    • f[0]=0,如果不能拼出Y,f[Y]=正无穷
  4. 计算顺序
    • f[0],f[1],f[2]…
  5. 消除冗余,加速计算

动态规划实例

求最大最小值动态规划

  1. 你有三种硬币,分别面值2元,5元和7元,每种硬币都有足够多
  2. 买一本书需要27元
  3. 如何用最少的硬币组合正好付清,不需要对方找钱

动态规划组成部分一:确定状态

确定状态需要两个意识:

  1. 最后一步
    • 关键点1:我们不关心前面的k-1枚硬币是怎么拼出27-ak的,而且我们现在甚至不知道ak和k,但是我们可以确定前面的硬币拼出了27-ak
    • 关键点2:因为是最优策略,所以拼出27-ak的硬币数一定要最少,否则就不是最优策略image-20210516211949412
  2. 子问题
    • 所以我们要求:最少用多少枚硬币可以拼出27-ak
    • 原问题是最少用多少枚硬币拼出27
    • 我们将原问题转化成一个子问题,而且规模最小:27-ak
    • 为了简化定义,我们设定状态f(X)=最少用多少枚硬币拼出X
    • 最后一枚硬币只可能是2,5或者7
    • f(27)=min{f(27-2)+1,f(27-5)+1,f(27-7)+1}image-20210516212817288

动态规划组成部分二:转移方程

  • 设状态f[X]=最少用多少枚硬币拼出X
  • 对于任意X,f[X]=min{f[X-2]+1,f{X-5}+1,f[X-7]+1}

动态规划组成部分三:初始条件和边界情况

  1. f[X]=min{f[X-2]+1,f[X-5]+1,f[X-7]+1}

  2. 两个问题:X-2,X-5或者X-7小于0怎么办?什么时候停下来?

  3. 如果不能拼出Y,就定义f[Y]=正无穷

  4. 所以f[1]=min{f[-1]+1,f[-4]+1,f[-6]+1}=正无穷,表示拼出来1

  5. 初始条件:f[0]=0 (用转移方程算不出来,需要手工定义)

动态规划组成部分四:计算顺序

  1. 拼出X所需要的最少硬币数:f[X]=min{f[X-2]+1,f[X-5]+1,f[X-7]+1}
  2. 初始条件:f[0]=0
  3. 然后计算f[1],f[2]……f[27]
  4. 当我们计算到f[X]时,f[X-2],f[X-5],f[X-7]都已经得到结果了image-20210516215043727

结论

  • 每一步尝试三种硬币,一共27步
  • 与递归算法相比,没有任何重复计算
  • 算法时间复杂度(即需要进行的步数):27*3,大于递归时间复杂度:>> 27*3
// num表示硬币有哪些面值,n表示num数组的大小,cost表示总共需要多少钱
int pingyingbi(int *num,int n,int cost)
{
    int f[cost+1];
    int i=0;
    int j=0;
    f[0]=0;
    // 求出当前操作系统int能取的正数最大值
    int MAXNUM=(~((int)0))&(~(1<<(sizeof(int)*8-1)));
    for(i=1;i<=cost;i++){
        f[i]=MAXNUM;
        for(j=0;j<n;j++){
            if(i-num[j]>=0&&f[i-num[j]]<=MAXNUM-1){
                if(f[i]>f[i-num[j]]+1){
                    f[i]=f[i-num[j]]+1;
                }
            }
        }
    }
    if(f[cost]==MAXNUM){
        return -1;
    }
    return f[cost];
}

计数型动态规划

  1. 给定m行n列的网格,有一个机器人从左上角(0,0)出发,每一步可以向下或者向右走一步
  2. 问有多少种不同的方式走到右下角image-20210517152254213

动态规划组成部分一:确定状态

  1. 最后一步:无论机器人用何种方式到达右下角,总有最后挪动的一一步:向右或者向下
  2. 右下角坐标设为(m-1,n-1)
  3. 那么前一步机器人一定是在(m-2,n-1)或者(m-1,n-2)

动态规划组成部分一:子问题

  1. 那么,如果机器人有X种方式从左上角走到(m-2,n-1),有Y种方式从左上角走到(m-1,n-2),则机器人有X+Y种方式走到(m-1,n-1)
  2. 问题转化为,机器人有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2)
  3. 原题要求有多少种方式从左上角走到(m-1,n-1)
  4. 转化为规模更小的子问题
  5. 状态:设f[i][j]为机器人有多少种方式从左上角走到(i,j)

动态规划组成部分二:转移方程

对于任意一个格子(i,j),都有f[i][j]=f[i-1][j]+f[i][j-1]image-20210517153631895

动态规划组成部分三:初始条件和边界情况

  • 初始条件:f[0][0]=1,因为机器人只有一种方式到左上角
  • 边界情况:i=1或j=0,则前一步只能有一个方向过来f[i][j]=1

动态规划组成部分四:计算顺序

  1. f[0][0]=1
  2. 计算第0行:f[0][0],f[0][1],…,f[0][n-1]
  3. 计算第m-1行:f[m-1][0],…..,f[m-1][n-1]
  4. 时间复杂度(计算步数):O(MN),空间复杂度(数组大小):O(MN)
int findwaynum(int m,int n)
{
    int map[m][n];
    int i=0;
    int j=0;
    for(i=0;i<m;i++){
        for(j=0;j<n;j++){
            if(j==0||i==0){
                map[i][j]=1;
            }else{
                map[i][j]=map[i-1][j]+map[i][j-1];
            }
        }
    }
    return map[m-1][n-1];
}

存在型动态规划

  1. 有n块石头分别在x轴的0,1,…,n-1位置
  2. 一只青蛙在石头0,想跳到石头n-1
  3. 如果青蛙在第i块石头上,它最多可以向右跳距离ai
  4. 问青蛙能否跳到石头n-1
  5. 例子:输入:a=[2,3,1,1,4],输出:true;输入:a=[3,2,1,0,4],输出:false

动态规划组成部分一:确定状态

  1. 最后一步:如果青蛙能跳到最后一块石头n-1,我们考虑它跳的最后一步
  2. 这一步是从石头i跳过来,i<n-1
  3. 这需要满足两个条件:青蛙可以跳到石头i;最后一步不超过条约的最大距离:n-1-i<=ai

动态规划组成部分一:子问题

  1. 那么,我们需要知道青蛙能不能跳到石头i(i<n-1)
  2. 而我们原来要求青蛙能不能跳到石头n-1
  3. 变成一个规模更小的子问题了,状态:设f[j]表示青蛙能不能跳到石头j

动态规划组成部分二:转移方程

  • 设f[j]表示青蛙能不能跳到石头jimage-20210517184731890

动态规划组成部分三:初始条件和边界情况

  1. 设f[j]表示青蛙能不能跳到石头j
  2. 初始条件:f[0]=true,因为青蛙一开始就在石头0

动态规划组成部分四:计算顺序

  1. 设f[j]表示青蛙能不能跳到石头j
  2. f[j]=OR0<=i<j(f[i] AND i+a[i]>=j)
  3. 初始化f[0]=true
  4. 计算f[1],f[2],…,f[n-1]
  5. 答案是f[n-1]
  6. 时间复杂度:O(N2),空间复杂度(数组大小):O(N)
#define true 1
#define false 0
int qingwatiao(int *num,int n)
{
    int pd[n];
    int i;
    int j;
    pd[0]=true;
    for(i=1;i<n;i++){
        pd[i]=false;
        for(j=0;j<i;j++){
            if(pd[j]==true&&i-j<=num[j]){
                pd[i]=true;
                break;
            }
        }
    }
    return pd[n-1];
}
// 问题:给定一个整形数组,求该数组中,连续的子序列的乘积达到最大,该数组可以包含负数和0;例如[2,4,5,-1,3,0],则输出20
// 该函数原理动态规划
// i的值始终大于等于j,于是pd[i][j]表示num[j]*num[j+1]* ... *num[i]
// 所以pd[i][j]=num[j]*num[j+1]* ... *num[i]=pd[i][j+1]*num[j]
// 初始条件当i=j时,pd[i][j]=num[i]=num[j]
#include <stdio.h>
int maxmuti(int *num,int n)
{
    // 当前max为当前操作系统下int最小的负数
    int max=(int)1<<(sizeof(int)*8-1);
    int pd[n][n];
    int i=0;
    int j=0;
    for(i=0;i<n;i++){
        for(j=i;j>=0;j--){
            if(i==j){
                // 假设i和j的值相同时,是本身,也就是当传进来数组为[500,-2,1]时,该函数会返回500
                // 传进来的是[500]时,该函数会放回500
                pd[i][j]=num[i];
                if(pd[i][j]>max){
                    max=pd[i][j];
                }
            }else{
                pd[i][j]=pd[i][j+1]*num[j];
                if(pd[i][j]>max){
                    max=pd[i][j];
                }
            }
        }
    }
    return max;
}
int main()
{
    int num[]={41,0,10,-1,5,6};
    printf("%d\n",maxmuti(num,sizeof(num)/sizeof(int)));
    return 0;
}

文章作者: dyl
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 dyl !
 上一篇
Linux静态库和动态库 Linux静态库和动态库
该文总结了在Linux系统下如何进行静态库与动态库的生成与使用,简言总结了静态库与动态库的优缺点,该文来自博主看B站视频总结
2023-12-10 dyl
本篇 
动态规划 动态规划
动态规划在面试中经常出现,该文章来自于博主看B站视频总结,该文章总结了动态规划问题常见的特点与面对动态规划问题思考方式,对于新手来说简单易懂
2023-12-10 dyl
  目录