算法基础1.3:动态规划系列问题描述及实现

image.png

投资问题描述:

设有5W元钱,4个项目,每个项目投资如profit数组所示
项目投资收益如下
11,12,13,14,15//第一个项目投资1万元的收益11,2W元12,3W元13,4W元14,5W元15
0 ,5 ,10,15,20//第二个项目同上
2 ,10,30,32,40//第三个项目同上
20,21,22,23,24//第四个项目同上

代码实现

#include "stdio.h"    
#include "stdlib.h"   

#define TOTAL_PROJECT 4
#define TOTAL_MONEY 5
int profit[TOTAL_PROJECT+1][TOTAL_MONEY+1]=
{
{0,0,0,0,0,0},
{0,11,12,13,14,15},
{0,0,5,10,15,20},
{0,2,10,30,32,40},
{0,20,21,22,23,24}
};

int main(void)
{    
    int dp[TOTAL_PROJECT+1][TOTAL_MONEY+1] = {0};
    int x[5] = {0};
    int project,invest,k;
    int max = 0;
    int invest_check = 0;
    for(project = 1;project <=TOTAL_PROJECT; project++)
    {
        max = 0;
        for(invest = 1;invest <=TOTAL_MONEY; invest++)
        {
            for(k = 0;k <=invest; k++)
            {
                invest_check = profit[project][k] + dp[project-1][invest - k];
                if(invest_check > max) 
                {
                    max = invest_check;
                }
            }
            dp[project][invest] = max;
        }
    }
    printf("\nprofit %d\n",dp[project-1][invest-1]);
}

下面有网友把大部分的动归的转移方程都总结了,我转载并详细再阐述一下。

状态转移方程

关于动归的转移方程,有两个比较好的解法
解法1:状态转移表法
先画一个二维数组,表示状态转移变化过程。根据决策顺序,从前往后,根据递推关系,分别填写二维数组的值,一般是两个循环,分别完成从0到n行,以及从0列到n列的过程。通常在递推的过程中,就会体会到状态转移方程的递推关系,进而写出状态转移方程。但是这个方法通常会有一个限制,就是由于人脑的思维更擅长于平面分析,所以如果影响状态方程的因为超过2个(行、列)时,需要先将所有因素都汇总成两个因素,类f(x,y,z)=F(t(x,y),z)这种方式。
解法2:回溯
一旦涉及到回溯,必然要扯到递归了。。实际上动归是可以直接用递归去实现,通常用递归去实现也更符合动归的直接思路。
回溯也同样需要引入一个“备忘录”,用以记录整体的状态变化过程。

可以用上述思路来求解一下一下几个动归典型问题的实现。

1、最长公共子串

假设两个字符串为str1和str2,它们的长度分别为n和m。d[i][j]表示str1中前i个字符与str2中前j个字符分别组成的两个前缀字符串的最长公共长度。这样就把长度为n的str1和长度为m的str2划分成长度为i和长度为j的子问题进行求解。


image.png

画外音(其实从这个图我们也看得出,动规依然就是一种特殊形式的遍历或者穷举模式,把i到j所有的情况都按某种形式进行了遍历。)
对整个二维数组进行递推的过程中,我们会发现,因为最长公共子串要求必须在原串中是连续的,所以一但某处出现不匹配的情况,此处的值就重置为0。所以只有当str1[i] == str2[j]的时候,最长公共子序列会加1,其他所有情况下,最长公共子序列都会归结为0。
因此得出,状态转移方程如下:

  1. dp[0][j] = 0; (0<=j<=m)
  2. dp[i][0] = 0; (0<=i<=n)
  3. dp[i][j] = dp[i-1][j-1] +1; (str1[i] == str2[j])
  4. dp[i][j] = 0; (str1[i] != str2[j])
    数组类问题,建议能够预留备忘录的索引0,以便保证状态转移方程中,对数组越界访问的保护。
    详细代码请看最长公共子串。

2、最长公共子序列

区分一下,最长公共子序列不同于最长公共子串,序列是保持子序列字符串的下标在str1和str2中的下标顺序是递增的,该字符串在原串中并不一定是连续的。同样的我们可以假设dp[i][j]表示为字符串str1的前i个字符和字符串str2的前j个字符的最长公共子序列的长度。


image.png

上图可以清晰的表示出最长子序列的遍历模式。
画外音(最长公共子串是自上而下的推演二维数组,而最长公共子序列是自下而上的推导)
状态转移方程如下:

  1. dp[0][j] = 0; (0<=j<=m)
  2. dp[i][0] = 0; (0<=i<=n)
  3. dp[i][j] = dp[i-1][j-1] +1; (str1[i-1] == str2[j-1])
  4. dp[i][j] = max{dp[i][j-1],dp[i-1][j]}; (str1[i-1] != str2[j-1])
    详细代码请看最长公共子序列。

3、最长递增子序列(最长递减子序列)

令 dp[i] 表示以 A[i] 结尾的最长不下降序列长度。这样对 A[i] 来说就会有两种可能:
如果存在 A[i] 之前的元素 A[j] (jdp[i],那么就把 A[i] 跟在以 A[j] 结尾的 LIS 后面,形成一条更长的不下降子序列(令 dp[i]=dp[j]+1)。
如果 A[i] 之前的元素都比 A[i] 大,那么 A[i] 就只好自己形成一条 LIS,但是长度为 1。
  由此可以写出状态转移方程:

dp[i] = max{1, dp[j]+1} (j=1,2,....,i-1&&A[j]

上面的状态转移方程中隐含了边界:dp[i]=1(1≤i≤n)。显然 dp[i] 只与小于 i 的 j 有关,因此只要让 i 从小到大遍历即可求出整个 dp 数组。然后在处理过程中记录整个 dp 数组中的最大值,那个就是要寻求的整个序列的 LIS 长度,整体复杂度为 O(n2)。
详细代码请看最长递增子序列

4、最大子序列和的问题

假设有序列{a1,a2,...,an},求子序列的和最大问题,我们用dp[i]表示以ai结尾的子序列的最大和。
会有一下几种情况
1.初始状态,只能取第一个元素
2.以ai结尾的元素,可能有dp[i-1]+ai和ai两种情况。
再展开,如果ai之前的元素构成的最大子序列加上ai以后小于ai了,则把dp[i]=ai,否则,持续加。
dp[1] = a1;
dp[i] = max{a[i], dp[i-1]+a[i]}
详细代码请看[最大子序列的和]
(http://www.cnblogs.com/tgycoder/p/5038268.html)
(https://www.cnblogs.com/coderJiebao/p/Algorithmofnotes27.html)

总结

动归问题
1)如果是求某个“最”值,其状态方程通常必须具备max等比较条件。
2)如果是求字符串问题,通常都是前i个字符和另外一个字符串的前j个字符的转化问题,都具备一定的相似性。
3)状态转化方程,重点思考i-1,j-1到i,j的过程,不要从0开始推,理清楚逻辑,但是不要纠结与细节。
4)如果真的无从下手,就写一个二维数组,填数字,填完一轮,思路就出来了,这个方法屡试不爽。等于基于实验得出结论。

动归的例子参考:[https://www.cnblogs.com/tgycoder/p/5037559.html]

你可能感兴趣的:(算法基础1.3:动态规划系列问题描述及实现)