算法理论动态规划

Posted littercoder

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法理论动态规划相关的知识,希望对你有一定的参考价值。

一.前言

  周末果然是堕落的根源,原谅我两天没刷题(PS:主要是因为周末搬家去了)。上次在这个题的时候,看到网上很多方法都是用动态规划做的,但是本渣渣实在不知道动态规划具体是怎样的,于是就专门花了花时间去研究了一下。肯定没这么快弄懂,只能说是稍微入门,于是写下这篇文章,帮助自己也帮助别人理解动态规划。

 

二.理论部分

  动态规划是什么呢? 百度百科上的定义是:动态规划(dynamic programming)是运筹学的一个分支,是求解决策过程(decision process)最优化的数学方法。用通俗的话来讲,动态规划是针对与某一类问题的解决方法。只要我们弄清楚了某一类问题是什么,那我们就知道什么是动态规划。

  首先,我们举一个简单的例子,也是dp的入门经典问题:如果我们有面值为1元、5元和11元的纸币若干枚,如何用最少的纸币凑够12元?

  按照人类的正常思维来说,首先我们会尽可能的使用大面值的纸币,所以会得出 12 = 11 x 1 + 1 x 1,最少使用两张纸币。这样一看好像这种方法没什么问题,可是当我们把问题换成凑够15元时,继续按照上面这种思路,我们会得到 15 = 11 x 1 + 1 x 4,也就是说,按照惯性思路,我们会认为最少需要五张纸币才能凑齐15元。但是我们可以轻易的看出,15 = 5 x 3,最少只需要三张纸币就能够凑齐15元。那么是为什么导致了这样的问题呢?因为我们之前的思路是,尽可能使用较大面值的纸币,来减小总额的大小,但是没有考虑到凑齐4元的需要四张纸币,这个代价非常大,这就是因为我们只考虑到了眼前的情况。这种方法其实就是常说的贪心算法,不过不在本篇的重点,以后有机会再补上。

  那么我们继续做这个题,换种思路,首先设使用最少的纸币凑齐15元的问题为F(15),那么如果我们使用一张11元的纸币,剩下的问题就是F(4),如果使用了一张5元的纸币,那么剩下的问题就是F(10),如果使用了一张1元的纸币,那么剩下的问题就是F(14),因此,我们可以得到一个公式F(15) = MinF(4), F(10), F(14) + 1 ,这个时候假设我们已经知道了F(4) = 4 ,F(10) = 2, F(14) = 4(ps:先不用去管是怎么知道的),我们就可以轻易得出F(15) = F(10) + 1 = 2 + 1 = 3。上面的公式可以写成F(15) = MinF(15 - 11), F(15 - 5), F(15 - 1) + 1,如果把15换成N,那么我们就可以得到公式F(N) = MinF(N - 11), F(N - 5), F(N - 1) + 1 。这样,我们就把一个问题,转换成了它的几个子问题,这就是DP(动态规划:将一个大问题,拆为几个子问题,分别求解这些子问题,即可推断出大问题的解)。

  知道了什么是动态规划后,我们来认识几个概念:

    1.状态:什么是状态?状态就是当前我们所研究的问题,也就是上题中的F(N);

    2.状态转移方程:状态转移方程就是我们如何从已知的状态,推导出现在未知状态的一个公式,也就是上题中的F(N) = MinF(N - 11), F(N - 5), F(N - 1) + 1 。

    3.无滞后性:“未来与过去无关”,这句话是什么意思呢?以上题为例,F(4), F(10), F(14)为过去的状态,F(15)为当前到状态,F(N)为未来的状态,我们要计算F(15)的时候,需要使用F(4), F(10), F(14),而当F(15)被确定之后,F(N)的计算如果需要使用到F(15)的值,则会直接使用F(15)的值,而不需要去关系F(15)是怎么计算出来的,也就是与F(4), F(10), F(14)无关了。总结一下,就是某一阶段的状态一旦被确定,那么未来的状态的发展就不会受这一阶段之前状态的影响,这就是未来与过去无关。

    4.最优子结构:F(n)的定义是使用最少到纸币凑齐N元,所以F(n)就是n元时的最优解,而我们求解F(n)的时候,需要使用到F(N - 11), F(N - 5), F(N - 1)的值,而这些状态就代表了自己的最优解,也就是说,大问题的最优解可以由小问题的最优解推出,这个性质叫做“最优子结构性质”。

    5.重叠子问题:这个名词很好理解,我们在计算F(15)的时候利用到了F(10)这个子问题的解,而我们在计算F(21)的时候,也会利用到F(10)这个子问题的解,这就是重叠子问题,在计算的时候,子问题并不是独立的,而是会被重复使用多次。

  了解了这几个概念之后,那么我们就来看看,如何确定一个问题能否使用动态规划来解决,又怎么样使用动态规划来解决。

  dp问题的特点:一个问题具有最优子结构的性质,那么它就能够使用dp来解决,而具有重叠子问题的性质,则表示它用dp解决会更占优势。

  dp问题的解法:1.先确定问题的状态

            2.推导出状态转移方程

          3.选择合适的数据结构保存子问题的解(一般是一维数组和二维数组)

          4.给选定的数据结构赋初始值

 

三.dp经典例题

  俗话说的好,纸上得来终觉浅 绝知此事要躬行。讲了一大堆的理论知识,可能大家看得也是半懂不懂(ps:也可能是我讲的太菜了),还是要从题目中入手,将理论知识运用到实际中,才算真正的理解了。

  3.1 斐波那契数列

    题目:大家都熟悉斐波那契数列,1 1 2 3 5 8 13 21 ...,现在求斐波那契数列第n位数。

    定性:1.最优子结构:本来这个问题是没有包含最优的,但是第n位数的值都是唯一的,也可以看作是最优,并且第n位数可以由n-1和n-2的值推出,所以满足最优子结构。

       2.重叠子问题:当n为3的时候,f(3) = f(2) + f(1) ,当n为4的时候,f(4) = f(3) + f(2) 。由此可以看到,f(2)被重复利用到了,所以存在重叠子问题。

    解题:确定了可以使用dp之后,就可以按照我们的步骤来进行解题。

          1.状态:F(n) 为斐波那契数列的第n位数。

       2. 状态转移方程:F(n) = F(n-1) + F(n - 2) 

       3. 使用一个一维数组arr,保存每一位的值 

       4.初始值arr[0] = 1 , arr[1] = 1;

       代码如下:

 1 class Solution 
 2     public int fib(int N) 
 3         if(N <= 0)
 4             return 0;
 5         
 6         if(N <= 2 )
 7             return 1;
 8         
 9         int[] arr = new int[N];
10         arr[0] = 1;
11         arr[1] = 1;
12         for(int i = 2; i < N; i++)
13             arr[i] = arr[i - 1] + arr[i - 2]; 
14         
15         return arr[N - 1];
16     
17 

 

  3.2  数组最大不连续递增子序列

    题目:给定一个数组,求它的不连续最大递增子序列的长度,arr[] = 3,1,4,1,5,9,2,6,5的最长递增子序列长度为4。即为:1,4,5,9。

    定性:1.最优子结构,假设数组长度为n,如果我们知道了在n-1的所有以自己为结尾的最长递增子序列长度,那么我们就可以推算出以n结尾的最长递增子序列,所以满足最优子结构。

       2.重叠子问题,在数组每增加一个元素后,计算以该元素为结尾的最长递增子序列长度,需要与前面每个元素做对比,并利用了它们的解,所以满足重叠子问题。

    解题:1.状态F(N):这次的F(N)并不是数组长度为n时的最长递增子序列,而是以n为结尾时的最长递增子序列。如果采用前面那个作为状态F(N)的定义,那么则不满足最优子结构的性质,我们并不能由子问题的解推导出大问题的解,所以有时候状态的定义并不是那么直观,还需要转换一下,只有状态定义正确了,我们才能正确的使用dp。

       2.状态转移方程:F(N) = Max arr[n - 1] > arr[ j]  && F(j) + 1 

       3.使用一个一维数组arr,保存每一个以它为结尾的最长递增子序列的值。

       4.整个数组的值都初始化为1,因为每个位置最小的子序列就是它自己。

       代码如下:

 1 class Solution 
 2     public int lengthOfLIS(int[] nums) 
 3         if(nums == null || nums.length == 0)
 4             return 0;
 5         
 6         int[] arr = new int[nums.length];
 7         for(int i = 0; i < arr.length; i++)
 8             arr[i] = 1;
 9         
10         int max = 1;
11         //从第二个元素开始,每个元素都需要与之前的元素进行比较,如果比它大,则将它保存的值+1
12         for(int i = 1; i < nums.length; i++)
13             for(int j = 0; j < i; j++)
14                 if(nums[i] > nums[j])
15                     max = max > arr[j] + 1 ? max : arr[j] + 1;
16                 
17             
18             arr[i] = max;
19             max = 1;
20         
21         //找出整个数组的最大值,则为答案
22         max = arr[0];
23         for(int i = 1; i < arr.length; i++)
24             max = max > arr[i] ? max : arr[i];
25          
26         return max;
27     
28 

    

  3.3 数组最大连续子序列和

       题目:给定一个数组,其中元素可正可负,求其中最大连续子序列的和。如arr[] = 6,-1,3,-4,-6,9,2,-2,5的最大连续子序列和为14。即为:9,2,-2,5

       定性:1.最优子结构:假设数组长度是n,我们在得到了以n-1为结尾的最大连续子序列的和,那么我们就可以推出以n为结尾的最大连续子序列的和,所以满足。

        2.重叠子问题:因为我们在计算以n为结尾的最大连续子序列时,只需要利用到n-1的解,所以不满足重叠子问题,可以用dp解,但是不一定非要用dp解

     解题:1.状态:F(N):以n为结尾时最大连续子序列的和

        2.状态转移方程: F(N) = F(N-1) > 0 : F(N-1) + N : N

        3.使用一个一维数组保存每一个值

        4.初始化,无需初始化

        代码如下:(因为这题目没有涉及到重叠子问题,所以用其他方法做也都是可以的,这里就不演示了     

 1 class Solution 
 2     public int maxSubArray(int[] nums) 
 3         if(nums == null || nums.length <= 0)
 4             return 0;
 5         
 6         int[] arr = new int[nums.length];
 7         arr[0] = nums[0];
 8         int max = arr[0];
 9         for(int i = 1; i < nums.length; i++)
10             arr[i] = arr[i - 1] > 0 ? arr[i - 1] + nums[i] : nums[i];
11             max = max > arr[i] ? max : arr[i];
12         
13         return max;
14     
15 

   3.4 两个字符串最大公共子序列

        题目:求两个字符串的最大公共子序列和,比如字符串1:BDCABA;字符串2:ABCBDAB,则这两个字符串的最长公共子序列长度为4,最长公共子序列是:BCBA

     定性: 1.最优子结构:假设我们知道了i,j(1串以i结尾,2串以j结尾)的最大公共子序列,那么我们可以推出i+1,j的最长公共子序列,也可以推出i,j+1的最大公共子序列,所以满足。

          2.重叠子问题:当我们在计算3,2的最大公共子序列时,需要用到2,2的最大公共子序列和3,1的最大公共子序列;当我们在计算4,1的最大公共子序列时,同意也用到了3,1的最大公共子序列,所以满足重叠子问题。

     解题:1状态:F(i,j):字符串1以i结尾,字符串2以j结尾时,最大的公共子序列

        2.状态转移方程:分两种情况,当i 和 j的字符相等时,这个字符一定在最大公共子序列中,所以F(i, j) = F(i -1, j -1) + 1;

                                                          当i和j不相等的时候,F(i,j),i和j这两个字符,肯定不会同时存在在最大公共子序列中,所以F(i,j)=Math(F(i,j-1),F(i - 1,j));

        3.定义一个二维数组,用于存放结果,数组大小时字符串长度+1,多一行用来存储0字符的情况

        4.初始化数组,将i为0或者j为0的都设置成0(当其中一个字符串为空时,两个字符串则不存在公共子序列)

        代码如下:(本题的难点在于弄清楚状态转移方程,为什么分这两种情况

 1     public int maxTwoArraySameOrder(String str1, String str2)
 2         int[][] arr = new int[str1.length() + 1][str2.length() + 1];
 3         for(int i = 0; i <= str1.length(); i++)
 4             arr[i][0] = 0;
 5         
 6         for (int j = 0; j <= str2.length(); j++)
 7             arr[0][j] = 0;
 8         
 9         for (int i = 1; i <= str1.length(); i++)
10             for (int j = 1; j <= str2.length(); j++)
11                 if (str1.charAt(i - 1) == str2.charAt(j -1))
12                     arr[i][j] = arr[i - 1][j - 1] + 1;
13                 else 
14                     arr[i][j] = Math.max(arr[i][j - 1], arr[i - 1][j]);
15                 
16             
17         
18         return arr[str1.length()][str2.length()];
19     

   3.5 经典题目---01背包问题

    在N件物品取出若干件放在容量为C的背包里,每件物品的体积为W1,W2……Wn(Wi为整数),与之相对应的价值为P1,P2……Pn(Pi为整数),求背包能够容纳的最大价值。对于每件物品只有放(1)和不放(0)两种状态,所以叫做01背包问题。

    定性:1.最优子结构:对于第k个物品来说,只有两种情况,一种是放,一种是不放。假设我们得到了不放第k件物品时,前k -1个物品的最大值m1,同时我们也得到了在背包中预留第k个物品的空间后,前面k-1个物品所能得到的最大值m2,根据这两个值,我们就能够推算出第k个物品放还是不放所得到的价值更大。同时m1和m2也是前k-1个物品分别在不同容积下的最大值,这就满足了最优子结构。

       2.重叠子问题:因为后面物体的体积不同,所以子问题存在重叠的情况。

    解题:1.状态:F(K, C): 容量为C时,从前K个物品中选取的最大价值。

          2.状态转移方程:F(K,C) =  F(K - 1,C) (当第K件物品的体积大于C时,最后一件物品放不下,直接从前面K件物品中选择)

                                                        = MaxF(K - 1, C), F(K - 1,C- Wk)  + Pk (对于第K件物品,只有两种选择,要么放,要么不放,所以从两种选择中,选出最大的那一种结果);

       3.定义一个二维数组,用来存放子问题

       4.初始化数组:当没有物品的时候,全部为0,所以arr【0】【】初始化为0。

         代码如下:

 1     public static int knapsack(int[] w, int[] v, int c)
 2         int[][] arr = new int[w.length + 1][c + 1];
 3         for (int i = 0; i <= c; i++)
 4             arr[0][i] = 0;
 5         
 6         for (int i = 1; i <= w.length; i++)
 7             for (int j = 1 ; j <= c ; j++)
 8                 if (j < w[i - 1])
 9                     arr[i][j] = arr[i - 1][j];
10                 else
11                     arr[i][j] = Math.max(arr[i - 1][j], arr[i - 1][j - w[i - 1]] + v[i - 1]);
12                 
13             
14         
15         return arr[w.length][c];
16     

    对于这个01背包问题,我们使用了一个二维数组来记录子问题的解,网上给出了一个优化方案,使用一个一维数组(滑动数组解法)来进行优化。(ps:我一开始看了半天也没看明白这个一维数组的使用,网上的描述也是不尽如人意,最后在纸上画了好几遍才弄明白的)这个优化方案是怎么样的呢?还要从上面的解法说起,大家会发现,当我们在计算前i个物品的解时,根据我们的状态转移方程可以发现,我们只会与前i - 1个物品那一列的解有关,而不会再使用到之前的解。所以我们可以使用一个一维数组表示上一列的值,本次计算的时候再进行替换。

    新的状态转移方程为:F(K) = F(K) (Wk  > K , 第k件物品的体积大于背包容积,所以该物品放不下,与上一列的值相同)

                   = F(K - Wk)

    注意点:有一个要注意的地方,使用一维数组的时候,我们要逆序进行计算,也就是说在一次循环中,我们要先从背包容量最大的情况开始计算,直到背包容量为0,这是因为我们在计算时,使用到了上一列的值,如果从前往后进行计算,一维数组的值会被更新掉,后面计算时使用到的值就是错误的,而逆序则不会出现这种情况,使用二维数组计算时,顺序逆序都可以。

    代码如下:

 1     public static int knapsack(int[] w, int[] v, int c)
 2         int[] arr = new int[c + 1];
 3         for (int i = 0; i <= c; i++)
 4             arr[i] = 0;
 5         
 6         for (int i = 1; i <= w.length; i++)
 7             for (int j = c ; j >= w[i - 1] ; j--)
 8                 arr[j] = Math.max(arr[j], arr[j - w[i - 1]] + v[i - 1]);
 9             
10         
11         return arr[c];
12     

    通过上面这些例子我们可以发现,dp问题最重要的部分就是定义状态,只有定义了正确的状态,才能较好的写出状态转移方程,做题时更多时间是花在分析上,等分析清楚了,代码就是水到渠成的事。大家千万不要以为弄懂上面的例子,就是已经掌握dp大法了,我们还只是刚入门的小白,要想真正的熟练运用dp,还是需要大量的锻炼。

      

以上是关于算法理论动态规划的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法简记--动态规划理论

每日算法-动态规划

动态规划理论

最优化理论与算法第二版陈宝林课后习题答案

最优化理论与算法第二版陈宝林课后习题答案

数学建模算法理论+程序