关于动态规划的一些个人学习和看法

以前一直只是接触过动态规划相关的内容,但是从未系统学习过,所以这一次来做一个系统的总结。

 

一、概述

动态规划和递归,分治思想,贪心类似,但是又不同。

动态规划之所以特殊不同,是因为所有可以使用动态规划解决的问题都会涉及到两个特点:

1.具有重叠子问题;

2.具有最优子结构;

所谓重叠子问题,就是指在能够将整个大问题进行整体性分割之后,小问题可以互相联系和互相解决。这是和分治思想最不同的地方。对于分治算法来说,例如简单的分治排序,所有子序列互不相关,并且有独立性。但是动态规划中分割的子问题往往会出现重复,产生冗余计算。

例如书上的例子:对于一个斐波那契数列,往往的通用方法是通过递归来进行自上而下的计算处理,用递归方法来进行计算的情况下,会出现多次计算,例如F(4)=F(3)+F(2),F(5)=F(4)+F(3),可以看到F(4)被用来计算了两次。这显然是多余的计算,对于初始方法来说,计算复杂度会达到O(2^n)。如果用动态规划进行改进,使用标志数组来进行标记计算,则每一次F(n)只需要计算一次,通过查询标志数组的内容,而不去计算已经计算过的内容,时间复杂度可以到达线性程度。这个操作也就是记忆化搜索。

所谓最优子结构,个人认为可能多用在树状搜索的例子中。例如权值二叉树的最长路径问题,就可以用动态规划思想来解决。对于这个问题,先前进行二叉树构造学习的时候多用递推来进行解决,也就是遍历每一条可能路径,从而一直比较,得到最长路径。也就是二叉树的深度遍历,且构造方式多为自顶而下。

但是对于动态规划来说,又有不同。如果说递推是从上至下,则动态规划为从底至上。例如最长路径搜索路径的问题来说,可以理解为找某个节点当前的最长路径,从而可以构造递归公式:dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j]),也成为状态转移方程。所以最后会从最底层的位置开始,逐层求出该节点下属的最长路径。

这里也要注意一下贪心算法的问题。贪心算法一定是单链的求解问题,也就是从根节点逐步来进行最大值求解,所以不会遍历到所有的可能路径,所以正确性还需要解决,具有盲目性和断然性。但是动态规划一定是全面且正确的。

 

 

二、最大连续子序列和:

动态规划的经典问题之一,能够完整的体现动态规划的思想。

大概题意:给定一个确定数字序列,要求从中找到一个子序列,使得序列内数字运算和最大。

例如:-2 11 -4 13 -5 -2

最大和为: 20

对于此类问题,通常解法大致为通过枚举左右端点来进行加和枚举,此方法计算复杂度较大。对于动态规划解法,时间复杂度仅有O(n)。

大致思想如下:

由于序列确定,所以可以将问题细分,设置一个dp数组,其中要求的最大值就为dp数组中的最大值,dp数组中的下标和给定数组的下标一一对应。

对于dp数组中的计算,核心想法如下:

对于dp数组中的每一位,都包括了A[i]之前所有子序列的最大值(注意,是所有情况下的最大值)。但是该问题下,有一个细则,就是对于dp[n]可能性的考虑。

对于dp[n]有两种情况:

1.dp[n]为A[n]

2.dp[n]为A[n]+dp[n-1]

对于第一种情况来说,原因也有两种,其一是可能元素确定之后有一个是最大的。其二为进行dp[n-1]和A[n]的加和之后,发现dp[n-1]+A[n]<A[n],对于这种情况,则将要重新确立左端点,将A[n]来作为左端点,如若不这样,则子序列的和要小。

对于第二种情况来说,A[n]作为待定序列中的成员,不作为左端点。

综上所述,则有状态转移方程:

dp[i]=max{A[i],dp[i-1]+A[i]},边界为dp[0]=A[0]

最后,遍历dp中的所有元素,得到最大的元素,即为给定序列中最大子序列和。

从这里可以看出,dp[i]的计算并不会管前面的待定序列是什么样,而是只关心dp[i-1]的值,并且确定后不会改变,也就是具有状态的无后效性。也就是当前状态记录了历史信息,一旦之前的状态确定,就不会再改变,并且所有的后续状态也会在这些已确定状态上进行生成。在设计状态和状态转移方程的时候,要设计拥有无后效性的状态以及状态转移方程,才能得到正确的结果。

代码如下:

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn=10010;
int A[maxn],dp[maxn];
void Maximum_continuous_subsequence_sum(){
    int n;
    scanf("%d",&n);
    for(int i=0;i<n;i++){
        scanf("%d",&A[i]);
    }
    dp[0]=A[0];
    for(int i=1;i<n;i++){
        dp[i]=max(A[i],dp[i-1]+A[i]);
    }
    int k=0;
    for(int i=0;i<n;i++){
        if(dp[i]>dp[k]){
            k=i;
        }
    }
    printf("%d\n",dp[k]);
}
int main()
{
    Maximum_continuous_subsequence_sum();
    system("pause");
    return 0;
}

三、最长公共子序列(LCS)

这算是第一个在动态规划问题里遇到的一个经典的难以理解的问题。随着个人对于动态规划问题的了解深入,逐渐领会了两个重要的点:

1.最小问题的分解和求解,并且一定要按照特定次序来进行求解;

2.对于状态转移方程的确立,这是最大的难点。

所谓最长公共子序列,就是对于两个给定的字符串或者数字序列A、B,求一个字符串,使得这个字符串是A、B的最长公共部分,并且可以不连续。例如“sadstory”,“adminsorry”,这两个字符串的最长公共子序列为"adsory"。

对于这个问题,如果没有动态规划思想,最浅显的解法就是使用暴力穷举,来逐个比较,这样时间复杂度会很大。

对于动态规划来说,可以缩减到O(mn)的级别。

首先就是进行问题分析,对于dp数组,我们可以进行定义,定义dp数组为两个字符串的最长公共子序列的长度,也就是一个int定值。由于是两个序列,所以我们可以将dp建立为二维数组。其中,dp[i][j]代表A和B中前i和前j个字母序列中的最长子序列。此时,就可以对问题进行了一个状态分解,也就是通过每一次ij个元素的最长公共子序列的长度检测时,都可以检测其子序列的所对应的dp值。

对于dp值的判定,我们可以分为两种情况:

其一:当A的第i个和B中的第j个元素相等时,就可以使前个字母的dp值+1,所以dp[i][j]=dp[i-1][j-1]+1;

其二:当A的第i个和B中的第j个元素不相等时,就要向前来遍历其子序列的对应最大公共子序列长度值dp,从而进行dp[i][j]更新。但是由于i-1个和j-1个元素可能不同,所以dp[i][j]=max{dp[i-1][j],dp[i][j-1]}。

此时,状态转移方程就已经确立完成。

接下来就是如何进行子问题的向上递归求解问题,也就是避免在使用前面的dp值时,该值未被求出。对于dp数组来说,他就是一个二维表,通过循环更新就可以解决这个问题。所以只需要进行简单的循环嵌套就可以解决。因为寻找的元素为dp[i-1][j-1],dp[i][j-1],dp[i-1][j],所以可以轻松解决。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=100;
char A[N],B[N];
int dp[N][N];

void LCS(){
    gets(A+1);
    gets(B+1);
    int lenA=strlen(A+1);
    int lenB=strlen(B+1);
    for(int i=0;i<=lenA;i++){
        dp[i][0]=0;
    }
    for(int i=0;i<=lenB;i++){
        dp[0][i]=0;
    }
    //状态转移方程
    for(int i=1;i<=lenA;i++){
        for(int j=1;j<=lenB;j++){
            if(A[i]==B[j]){
                dp[i][j]=dp[i-1][j-1]+1;
            }else{
                dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            }
        }
    }
    printf("%d\n",dp[lenA][lenB]);
}
int main()
{
    LCS();
    system("pause");
    return 0;
}

对于更多的LCS理解

四、最长回文子串(Longest Palindrome)

最长回文子串也是一个经典问题,其题目为:给出一个字符串S,求S的最长回文子串的长度。

对于这个问题,动态规划思想也十分有效,其基本的思想为,对于一个子串,如果其第二位和倒数第二位所构成的子串也是回文子串,并且第一个字符和最后一个字符相等,则该字符串必为回文子串。因此,可以自定义dp数组。由于会从字符串的首部和尾部来进行定位,所以,dp数组为二维数组,其中,dp[i][j]意义为字符串的第i位至字符串的第j位所构成的子串是否是回文子串。

对于初始dp来说,一个字符本身就是回文子串,所以dp[i][i]=1;不是回文子串,则dp[i][j]=0;

因此有状态转移方程:

dp[i][j]=dp[i+1][j-1],S[i]=S[j];

dp[i][j]=0,S[i]!=S[j];

对于老生常谈的初始化问题,也很简单,为了使出现未求解结果不会出现,因此先从单个字符串来开始更新dp数组,并且每次将测量的子串长度加一,从而进行迭代更新。最后去得到最大的长度。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int maxn=1010;
char S[maxn];
int dp[maxn][maxn];

int work_function(){
    gets(S);
    int len=strlen(S),ans=1;
    memset(dp,0,sizeof(dp));
    for(int i=0;i<len;i++){
        dp[i][i]=1;
        if(i<len-1){
            if(S[i]==S[i+1]){
                dp[i][i+1]=1;
                ans=2;
            }
        }
    }
    for(int L=3;L<=len;L++){
        for(int i=0;i+L-1<len;i++){
            int j=i+L-1;
            if(S[i]==S[j]&&dp[i+1][j-1]==1){
                dp[i][j]=1;
                ans=L;
            }
        }
    }
    printf("%d\n",ans);
}

int main()
{
    work_function();
    system("pause");
    return 0;
}

 

五、最长不下降子序列(LIS)

最长不下降子序列问题描述如下:在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列不是下降的。

其基本的核心思想就是如何构建dp数组和状态转换方程。对于该问题,书上所给的经典思路如下:

令dp[i]代表以A[i]结尾的最长不下降子序列长度。因此,对于dp[i],有两种情况:

其一:如果存在A[j],在A[i]之前,并且小于A[i],此时,可以将A[i]拼接到A[j]后面,从而构成一个更长的子序列。但是注意,也要满足dp[j]+1>dp[i],其目的是A[i]拼接后的子序列长度应该大于直接以A[i]结尾的子序列长度(其实无关紧要,因为初始的dp长度各个都为1);

其二:如果A[i]之前的所有元素都比A[j]大,则A[i]形成自己的一条LIS,长度为1;

所以有状态转移方程dp[i]=max{1,dp[j]+1} (j=1,2,...,i-1&&A[j]<A[i])

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
const int N=100;
int A[N],dp[N];

void LIS(){
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&A[i]);
    }
    int ans=-1;
    for(int i=1;i<=n;i++){
        dp[i]=1;
        for(int j=1;j<i;j++){
            if(A[i]>=A[j]&&(dp[j]+1>dp[i])){
                dp[i]=dp[j]+1;
            }
        }
        ans=max(ans,dp[i]);
    }
    printf("%d",ans);
}
int main()
{
    LIS();
    system("pause");
    return 0;
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值