动态规划

动态规划

动态规划题目类型特点
  • 计数
    • 有多少种方式走到右下角
    • 有多少种方法选出k个数使得和是sum
  • 求最大最小值
    • 从左上角走到右下角路径的最大数字和
    • 最长上升子序列长度
  • 求存在性
    • 去石子游戏,先手是否必胜
    • 能不能选出k个数使得和是sum
类型一:求最大最小值

例题:LintCode 669:Coin Change


1571364726906

动态规划组成部分一:确定状态 (重点)
  • 状态在动态规划中的作用属于定海神针
  • 简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么
    • 类似于解数学题中,X,Y,Z代表什么
    • 一个状态一个未知数,几个未知数就开几维数组
    • 数组的变量就是状态在变化,每个数组的值就是记录我们想要的的结果。
  • 确定状态需要两个意识
    • 最后一步
    • 子问题

最后一步

  • 虽然我们不知道最优策略是什么,但是最优策略肯定是k枚硬币 a 1 a_1 a1, a 2 a_2 a2,…, a 2 a_2 a2面值加起来是27

  • 所以一定有一枚最后的硬币: a k a_k ak

  • 出掉这枚硬币,前面硬币的面值加起来是27 - a k a_k ak

    1571365204908

关键点1

我们不关心前面的k-1枚硬币是怎么拼出27 - a k a_k ak的(可能有一种拼法,可能有100种),而且我们现在甚至还不知道 a k a_k ak和k,但是我们确定前面的硬币拼出了27 - a k a_k ak

关键点2

因为是最优策略,所以拼出27 - a k a_k ak 的硬币数一定要最少,否则这就不是最优策略了

假如五个硬币是最优的策略,那么如果扣掉最后一个 a k a_k ak,剩下的最少也要有四枚硬币拼成,否则其最后的五枚硬币就不是最优的解了

子问题

  • 所以我们就要求:最少用多少枚硬币可以拼出27 - a k a_k ak

  • 原问题是最少用多少枚硬币拼出27

  • 我们将原问题转化成了一个子问题,而且规模更小:27 - a k a_k ak

  • 为了简化定义,我们设状态f(x) = 最少用多少枚硬币拼出X

  • 等等,我们还不知道最后那枚硬币 a k a_k ak是多少

  • 最后那枚硬币 a k a_k ak只可能是2,5,7

  • 如果 a k a_k ak是2,f(27)应该是f(27-2)+1(加上最后这一枚硬币2)

  • 如果 a k a_k ak是5,f(27)应该是f(27-5)+1(加上最后这一枚硬币5)

  • 如果 a k a_k ak是7,f(27)应该是f(27-7)+1(加上最后这一枚硬币7)

  • 除此之外,没有其他可能了

  • 需要求最少的硬币数,所以:

    1571375851766

动态规划组成部分二:转移方程(重点)

1571373254322

动态规划组成部分三:初始条件和边界情况
  • 两个问题:x - 2, x - 5或者 x - 7小于0怎么办?什么时候停下来?

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

    • 例如f [-1]= f [-2] = … =正无穷
  • 所以f[1] = min{f[-1] + 1, f[-4] + 1, f[-6] + 1} = 正无穷,表示拼不出来1

  • 初始条件:f[0] = 0;

    初始条件就是用转移方程算不出来的,需要手工定义例如:f[0]

动态规划组成部分四:计算顺序
  • 拼出x所需要的最少硬币数: f[x] = min{f[x-2]+1,f[x-5]+1,f[x-7]+1}
  • 初始条件:f[0] = 0
  • 然后计算f[1],f[2],…,f[27]
  • 当我们计算到f[x]时,需要用到f[x-2],f[x-5],f[x-7],不过他们都已经得到结果了,这就是我们需要的计算顺序

1571377588727代码:

#include<iostream>
using namespace std;

/*
 * dp[n]表示凑齐n元最少需要凑多少枚硬币
 * 最后一步有三种情况, dp[22] + 1   dp[20] + 1  dp[25] + 1 
 * 对于最后一步都是加一枚硬币区别只是硬币的面值不一样,关键在于dp[22] dp[20] dp[25]  那个更小
 * 然后我们就从求dp[27] -> 求dp[22] dp[20] dp[25] 这就是子问题
 * 我们只要将 27 22 20 25这些数字用未知数,代替就可以了,循环一下就完成了
 */

int main(){
    int way[3] = {2, 5, 7};
    int dp[28] = 无穷大;
    dp[0] = 0;
   	
    for(int i = 1; i < 28; i++){
        for(int j = 0, j < 3; j++){
            if(i - way[j] >= 0 && dp[i - way[j]] != 无穷大)
            dp[i] = min(dp[i - way[j]] + 1,dp[i]);
        }
    }
    
    if(dp[27] == 无穷大) cout<<-1<<endl;
    cout<<dp[27]<<endl;
    
}
类型二:计数

例题:LintCode 114: Unique Paths


1571458796362

动态规划组成部分一:确定状态
  • 确定最后一步:无论机器人用何种方式到达右下角,总有最后挪动的一步:向右 或者 向下
  • 右下角坐标设为(m-1,n-1)
  • 那么前一步机器人一定是在(m-2,n-1)或者(m-1,n-2)

子问题

  • 那么,如果机器人有x种方式从左上角走到(m-2,n-1),有Y种方式从左上角走到(m-1,n-2),则机器人有x+y种方式走到(m-1,n-1)

    思考:为什么是x+y

    加法原理: 加法原理是分类计数原理,常用于排列组合中,具体是指:做一件事情,完成它有n类方式,第一类方式有 M 1 M_1 M1种方法,第二类方式有 M 2 M_2 M2种方法,……,第n类方式有 M n Mn Mn种方法,那么完成这件事情共有 M 1 M_1 M1+ M 2 M_2 M2+……+ M n M_n Mn种方法。

    注意点:

    1.无重复

    2.无遗漏

  • 问题转化为,机器人有多少种方式从左上角走到(m-2,n-1)和 (m-1,n-2)

  • 原题要求有多少种方式从左上角走到(m-1,n-1)

  • 子问题

  • 状态:设f[i][j]为机器人有多少种方式从左上角走到(i,j)

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

1571459505788

动态规划组成部分三:初始条件和边界情况
  • 初始条件:f[0][0] = 1,因为机器人只有一种方式到左上角
  • 边界条件:i = 0 或 j = 0 ,则前一步智能有一个方向过来 —>f[i][j] = 1
动态规划组成部分:计算顺序

1571468746586

代码:

#include<iostream>
#include<stdio.h>
using namespace std;

/*
 * map[][] 大小表示地图的大小,而map[i][j]对应的是到达其位置有多少种方法
 *
 * 计数类经常用到加法原理,这题也不例外:因为他行走的方式只能向右和向下
 * 所以:到达一个位置的方法只有两种可能,在他的上方或者在他的左边
 * 加法原理:到达map[i][j]有两种形式 从map[i-1][j]向下走 和 map[i][j-1]想右走
 * 那么到达 map[i][j]的方法便是两种形式的和。所以对于每个位置都可以这样递推
 */

int main()
{
    int map[4][8];  
    
    for(int i = 1; i <= 3; i++){
        for(int j = 1; j <= 7; j++){
            if(i == 0 || j == 0){
                map[i][j] = 1;
            }else{
                map[i][j] = map[i][j-1] + map[i-1][j];
            }
        }
    }
    cout<<map[3][7]<<endl;
    return 0;
}
类型三:求存在性

例题:

LintCode 116 Jump Game


1571469991667

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

1571474966794

子问题

1571475083988

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

1571475132858

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

1571475203925

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

1571475307108

代码:

#include<iostream>
using namespace std;

/*
 * 参数A[]表示,X轴上的石子   A[n]的值表示 n这个石子可以跳的最远距离
 * n 表示这个石子的位置
 * f[] 对应每个石子能不能跳到	f[n]表示第n个石子能不能跳到
 */
int Jump(int A[])
{
    int n = sizeof(A)/sizeof(A[0]);

    int f[n];
    f[0] = 1;
    
    /*判断能不能调到j,首先认为他不能*/
    for( int j = 0; j < n; j++){		
        f[j] = 0;
        
        /*对j前面的每个石子都进行枚举,因为j是由前面的i跳过来的i < j*/
        for( int i = 0;  i < j; i++){	
        /*
        *对j前面的每个石子i都进行判断,先判断能不能调到i,首先要能到i,然后判断本身的位置加上自己最远能		  *跳的距离能到j吗,如果能到,则就说明j可以调到,改为true
        */
            if(f[i] && i + A[i] >= j){
                f[j] = 1;
                break;
            }
        }
    }
    return f[n-1];
}

int main()
{
    
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值