动态规划算法的学习之路

动态规划算法,可以说已经是各大互联网公司笔试、面试必考的算法之一了,为了抓住金九银十,学习要争分夺秒......

可是如何分辨题目是否可以使用动态规划进行解题呢?别急,动态规划题目的特点可以大致分为以下三类:

1.计数

-有多少种方式走到右下角,比如机器人寻路等

-有多少种方法选出k个数使得和是sum

2.求最大最小值

-从左上角走到右下角路径的最大数字和

-最长上升子序列长度

3.求存在性

-取石子游戏,先手是否必胜

-能不能选出k个数使得和是sum

什么是动态规划?

接下来通过例题进行进一步的分析。

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

直觉:直观分析,用最少的硬币组合—>尽量用面值大的硬币,那么

  • 7+7+7=21
  • 21+5=26
  • emmmmm....

貌似不行,再来,换策略

改算法:尽量用最大的硬币,最后如果可以使用一种硬币付清就行

  • 7+7+7=21
  • 21+2+2+2=27
  • 6枚硬币,应该对了吧...

可是答案居然是,居然是

正确答案:7+5+5+5+5=27,5枚硬币

哎,老老实实根据算法,按部就班来做题吧,不YY了。

首先第一步:动态规划组成部分——确定状态

  • 状态在动态规划中的作用属于定海神针
  • 简答的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么,类似于解数学题中,X,Y,Z代表什么
  • 确定状态需要两个意识:最后一步;子问题

最后一步

  • 虽然我们不知道最优策略是什么,但是最优策略肯定是k枚硬币a1,a2,a3,...,ak,面值加起来是27
  • 所以一定有一枚最后的硬币:ak
  • 除掉这枚硬币,前面硬币的面值加起来是27-ak

关键点1

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

关键点2

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

子问题

  • 所以我们就要求:最少用多少枚硬币可以拼出27-ak
  • 原问题是最少用多少枚硬币拼出27
  • 我们将原问题转化成了一个子问题,而且规模更小:27-ak
  • 为了简化定义,我们设状态f(X)=最少用多少枚硬币拼出X

但是,等等,我们还不知道最后那枚硬币ak是多少

由于题目中给出了三种币值,那么最后那枚硬币ak只可能是2,5或7

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

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

因此需要求最少的硬币数,所以不难推出以下关系式

f(27)=min{f(27-2)+1,f(27-5)+1,f(27-7)+1}

根据上面的推导,我们很容易写出递归的程序,如下

public static void main(String[] args) {
        System.out.println(getMinCoinCount(27));//5
    }

    //写一个方法,获取购买X元书需要的最少硬币
    public static int getMinCoinCount(int X) {
        if (X == 0) return 0;//递归的出口
        int res = Integer.MAX_VALUE;//将初始值设置为最大值

        //如果ak为2
        if (X >= 2 && getMinCoinCount(X - 2) != Integer.MAX_VALUE) {
            res = Math.min(getMinCoinCount(X - 2) + 1, res);
        }

        //如果ak为5
        if (X >= 5 && getMinCoinCount(X - 5) != Integer.MAX_VALUE) {
            res = Math.min(getMinCoinCount(X - 5) + 1, res);
        }

        //如果ak为7
        if (X >= 7 && getMinCoinCount(X - 7) != Integer.MAX_VALUE) {
            res = Math.min(getMinCoinCount(X - 7) + 1, res);
        }
        return res;
    }

虽然递归能解决问题,但并不是最好的解决方法,接下来谈谈递归解法的问题

从图中不难看出,f(20)重复计算了3次,f(15)重复计算了两次,如何解决这个问题呢?主角动态规划又站起来了,“放开那方程,让我来”。动态规划的第二步,登上历史舞台

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

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

现在,转移方程有了,但是还没完,还有东西。

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

  • f(X) = min{f(X-2)+1,f(X-5)+1,f(X-7)+1}
  • 两个问题: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

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

  • 拼出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]都已经得到结果了

f[X]=最少用多少枚硬币拼出X

f[X]=无穷  表示无法用硬币拼出X

  • 每一步尝试三种硬币,一共27步
  • 与递归相比,没有任何重复计算
  • 算法时间复杂度(即需要进行的步数):27*3
  • 递归时间复杂度:>>27*3

小结

求最值型动态规划

动态规划的组成部分:

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],...

消除冗余,加速计算

最后代码实现,完结!

public static void main(String[] args) {
        int[] arr = {2, 5, 7};//币值的种类
        int M = 37;//商品总价
        System.out.println(getMinCoinCount(arr, M));
    }

    public static int getMinCoinCount(int[] arr, int M) {
        int[] f = new int[M + 1];//定义动态规划的一维数组
        f[0] = 0;//初始条件
        
        for (int i = 1; i <= M; i++) {
            f[i] = Integer.MAX_VALUE;//初值设置为无穷大
            for (int j = 0; j < arr.length; j++) {
                if (i >= arr[j] && f[i - arr[j]] != Integer.MAX_VALUE) {
                    f[i] = Math.min(f[i - arr[j]] + 1, f[i]);
                }
            }
        }
        
        if (f[M] == Integer.MAX_VALUE) {//如果拼不出,返回-1
            f[M] = -1;
        }
        
        return f[M];
    }

以上内容均来自B站九章算法课程。

课程地址https://www.bilibili.com/video/BV1xb411e7ww?from=search&seid=15667342454332890058

猜你喜欢

转载自blog.csdn.net/weixin_43419256/article/details/107923888