动态规划(Dynamic Programming)算法与LC实例的理解

本文详细介绍了动态规划(DP)的概念,通过硬币问题、斐波那契数列、钢条切割、最长公共子序列和双调旅行商问题等经典案例阐述了DP的运用。DP适用于存在重叠子问题和最优子结构的问题,通过状态定义和状态转移方程解决复杂问题。文章还提供了LeetCode的LIS和最大和连续子数组问题的解题思路。
摘要由CSDN通过智能技术生成

动态规划(Dynamic Programming)算法与LC实例的理解

作者:Bluemapleman(tomqianmaple@outlook.com)

麻烦不吝star和fork本博文对应的github上的技术博客项目吧!谢谢你们的支持!

知识无价,写作辛苦,欢迎转载,但请注明出处,谢谢!


前言:动态规划(DP)是比较常见的一种设计算法的思想。即对于存在最优子结构的问题,用子问题的解拼凑完整问题的解(状态转移方程),并在过程中利用诸如记忆化(memorization)这样的方法来存储重叠子问题的解,避免无谓的计算,减少时间复杂度。动态规划的思想不是很容易理解,但结合几个例子多加思考,应该也能有个大致的感觉。

DP是什么

基本定义

先来看维基百科的说明

动态规划(英语:Dynamic programming,简称DP)是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

动态规划常常适用于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往往远少于朴素解法。

动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再合并子问题的解以得出原问题的解。

通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量:一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

动态规划在查找有很多重叠子问题的情况的最优解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被保存,从简单的问题直到整个问题都被解决。因此,动态规划保存递归时的结果,因而不会在解决同样的问题时花费时间。

动态规划只能应用于有最优子结构的问题。最优子结构的意思是局部最优解能决定全局最优解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决。

我们首先关注最核心的定义:

通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。

简练来说,DP的关键在于分解原本的复杂问题为相对简单的子问题

我们结合一点具体的实例来感受一下这句话,比如先来看一下DP最经典的问题之一:硬币问题。

帮助理解的经典问题:硬币问题

现在假设你手上有1,2,5这三种面额的硬币,给定任意一个正整数n,求凑齐n这个数额最需要的最少的硬币数目为多少。(假设每个面额的硬币都有无数多枚)

现在我们有了目标问题:用最少硬币数拼出指定数目n。而如果我们不思考,想比较暴力地解决这个问题,那当然只能穷举能够拼成n的所有可能的做法,然后找出所有做法中,用到硬币数目最少的那个。

而之所以要有算法这种东西,就是为了尽量避免暴力穷举这种耗时耗力的做法,转而借助一些技巧,让我们能够相对轻松地多的解决问题。

DP的定义在这个问题上就可以给我们一个启示(假设目标拼出数额为n,算法为一个函数f(),问题的解就是f(n)):

“如果我知道了拼出数额n-1所需要的最少硬币数目f(n-1),那么是不是再加1个面额为1的硬币(f(n-1)+1)就是拼出数额n的解(f(n))了呢…诶,不太对,是不是可能f(n-2)的解再加上一个面额为2的硬币的硬币总数会更少呢(f(n-2)+1 &lt; \lt <f(n-1)+1)…那同理了,f(n-5)+1会不会比f(n-2)+1和f(n-1)+1都小啊???那么总结一下是不是这样:对于我手上所有的各类面额的硬币{c1,c2,c3,…cn},如果我知道了当目标数额为1,2,3,…,n-1时的对应解f(1),f(2),f(3),…,f(n-1),那么我只需要知道Min{f(n-c1)+1,f(n-c2)+1,f(n-c3)+1,…f(n-cn)+1}就可以了!!!”

当你开始想利用相似问题的解(拼出小于n的某个面额所需要的最少硬币数)来帮助解题时,这个思路是很正确的。因为我们发现,乍一看,我们似乎不需要考虑怎么直接去解决源问题本身,而是有点偷懒地想,但凡类似的子问题有解了,我解决这个问题也就很简单了。

于是,我们这样不断地把硬币问题往前、往简单的方向分解,就会发现,我们最终会落脚到:n=1时,f(1)=?;n=2时,f(2)=?;n=3时,f(3)=?..类似这样最原始的问题上。而对于这样的子问题,我们就完全可以最开始的时候拍脑袋简单想一想,直接给出解,比如给出n=1,2,3的解:

f(1)=1,f(2)=1,f(3)=2;

然后,对于n>4的每个情况,我们这样处理:

f ( n ) = M I N   { f ( n − c i ) + 1 }    f(n)=MIN\ \{f(n-c_i)+1\}\ \ f(n)=MIN { f(nci)+1}  
i = 1 , 2 , 3... , n   a n d   n − c i ≥ 0   a n d   c i ∈ { c 1 , c 2 , c 3 , . . . , c n } i=1,2,3...,n\ and\ n-c_i\ge0\ and\ c_i\in\{c_1,c_2,c_3,...,c_n\} i=1,2,3...,n and nci0 and ci{ c1,c2,c3,...,cn}

意思就是对于手上有的所有面额的硬币 c i c_i ci,我们都拿它来试一试 f ( n − c i ) + 1 f(n-c_i)+1 f(nci)+1,即用它搭配合前面已知的一个最优解得到f(n)的一个可能解时,该可能解是否是最优的(所需硬币数目最少的解)。

这样,我们就不用盲目地去拿手里的硬币瞎拼,也不知道啥时候能拼好了。

给个Java代码:

public int coinProblem(int n){
    int[] coins={1,2,5};
    int[] solutions=new int[n+1];
    //为了后续从第一个解开始的初始比较
    Arrays.fill(solutions,Integer.MAX_VALUE);
    //n=0时,一枚硬币都不需要
    solutions[0]=0;
    for(int i=1;i<n;i++){
        if(n-i>=0){
            if(solutions[n-i]+1<solutions[n]){
                solutions[n]=solutions[n-i]+1;
            }
        }
    }
    return solutions[n];
}

当然针对这个问题会有一些边界情况,比如如果硬币面额最小的是3,那么n=1,2时都是没有解的,或者有时候有些n的数额靠仅有的硬币拼不出来,也没有解,这些就依赖于编程时的具体实现,这里我们的核心是DP算法本身,就不过多专注这些细节问题。

第二个经典问题:斐波那契数列

斐波那契数列是头两项为1,后面任意一项均为前两项之和的一个数列。

相应的问题是:如何高效求解斐波那契数列的第n项?

不考虑效率的情况下,最简单直接的做法是递归:f(n)=f(n-1)+f(n-2)。完全不用动脑筋,非常easy。

但是如果你去在OJ上对付斐波那契数列用这种搞法的话,很容易exceeds time limit或者stack overflow,因为这样简单的递归要进行很多次不必要的重复计算。

比如说,如果要算f(5),那么f(5)=f(4)+f(3),而f(4)=f(3)+f(2),f(3)=f(2)+f(1),f(1)和f(2)已知都等于1,但是f(3)在这个过程已经被计算了两遍。而当n一旦大起来,就可能造成很多个这样的f(i)被多次计算,白白浪费时间。

于是,我们想,我们能不能这样想:递归之所以要进行重复计算,根源问题在于它是倒着推的,即f(n-1)依托于f(n-2),但f(n-2)也不知道,需要继续往前推。但是如果我们正着推,即在知道f(1)和f(2)后,我们f(3)=f(2)+f(1),f(4)=f(3)+f(2)这个思路来做,一直求到n,不就完全没有重复的问题了嘛!

其实这个思路就是动态规划的思路,因为我们又把单纯的源问题【求斐波那契数列的第n项】,变成了子问题【知道了斐波那契数列的第n-2和第n-1项,求斐波那契数理的第n项】,那么子问题就是做个加法的事情。而子问题推回到最原始的部分,我们的问题就是求f(3),f(4)了,和之前的硬币问题就很相似了。

同样给个Java代码:

public int Fibonacci(int n){
    if(n<=2)
        return 1;
    int[] solutions=new int[n+1];
    solutions[0]=0;
    solutions[1]=1;
    solutions[2]=1;
    for(int i=3;i<n;i++){
        solutions[n]=solutions[n-1]+solutions[n-2];
    }
    return solutions[n];
}

第三个经典问题:钢条切割(Rod Cutting)

假设你有这么一根长度为n米的材质奇异的钢条,它按不同的长度卖出去时价格都不同:

len(米) 1 2 3 4 5 6 7 8
price(元) 1 5 8 9 10 17 17 20

于是,现在需要你自己来决策,如何切割钢条,可以让你获得最大收益。(可切割任意多段,但每段长度必须为正整数)。

最naive的brute-force方法肯定就是:从钢条的最左端切一条s下来,s遍历所有可能的长度,然后用它的售价分别加上剩余的钢条可以切出的总价的所有可能(递归),然

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值