关于动态规划的讲解和力扣习题

动态规划是近年来笔试和面试做常考的题型了。但是很难有一个模板去套所有的题型。我们结合力扣的题型来说说动态规划。

首先动态规划的问题一般都是最值问题,最短距离,最小序列,最长序列之类的。我们人看待问题可以左到统筹全局然后找出最值。但是电脑不行,电脑解决最值问题的唯一方法就是穷举。找出所有的解决方案,然后从中比较得出最优解。

也就是说绝大部分的问题都可以使用穷举得到答案。但是这里存在两个问题:一是很多问题不是那么容易穷举,需要我们找出一个合理的关系(转移方程),不然很难做到穷举出所有的值。而是存在重叠问题,会存在大量的重复计算,这使得算法的时间复杂度和空间复杂度都特别大。

第二个问题常规的做法是使用备忘录,最常用的就是加dp数组记录之前的值,但是这里一个难点就是dp[i]的定义是什么,这个很重要,影响到第一个问题的解决,第一个问题是在明确了dp[i]含义之后,如何写出转移方程。写转移方程最常用的就是数学归纳法,假设我们已经求出了dp[i-1],那么我们如何求出dp[i]呢。写转移方程的时候又需要明确dp[0]、dp[i]和dp[i-1]逻辑关系。

这里总结一下:我们的方法是我们的方法是穷举,但是要解决重叠问题,所以要找到最优子结构。我们要做的工作就是:

1.明确dp[i]的含义

2.写出bese case

3.写出转移方程

下面我介绍几个典型的例题:(这里的例题结构我借鉴了labuladong大佬写的动态规划文章,大佬写的特别好)

最长递增子序列

 

300. 最长上升子序列

注意「子序列」和「子串」这两个名词的区别,子串一定是连续的,而子序列不一定是连续的。

首先明确dp[i]定义,表示以nums[i]为结尾的最长递增子序列的长度。

这里要说明一下,我最开始的想法是表示0-i的最长递增子序列长度。也就是不一定要以nums[i]为结尾。这样的话就存在一个问题,如何得出dp[i]呢,因为nums[i]大于nums[i-1]也不一定得出dp[i]=dp[i-1]+1;所以这样设定的话,很难找到适合的转移方程。

有上面的定义可知base case是将dp[i]初始化为1,因为每一个自身都可以作为一个子序列。

下面就要写转移方程了。举个例子,我们现在已经求出了第6个数组元素的dp了,现在要求第7个。那么问题来了,我们要求的一定是最长的,前面6个元素可能不止一个比它小,就要求要比较一下,保存最大,所以只需要遍历之前的元素,维护最大值即可。

代码如下

class Solution {
    public int lengthOfLIS(int[] nums) {
        int[] dp=new int[nums.length];
        for(int i=0;i<nums.length;i++){
            dp[i]=1;
        }

        for(int i=0;i<nums.length;i++){
            for(int j=0;j<i;j++){
                if(nums[i]>nums[j]){
                    dp[i]=Math.max(dp[i],dp[j]+1);
                }
            }
        }
//需要多做这一步
        int res=0;
        for(int i=0;i<dp.length;i++){
            res=Math.max(res,dp[i]);
        }
        return res;
    }
}

最大子数组

53. 最大子序和

首先dp[i]表示已nums[i]为结尾的最大子数组和,处于同样的原因,如果dp[i]表示0-i的最大子数组和,那么此时的最大子数组和不一定和nums[i-1]有关系,很难找到一个合适的转移方程。

base case就很好写了,直接为0就行

转移方程,对nums[i]来说,要么就加入最大子数组和,要么就自己作为一个子数组。

转移方程为

dp[i]=Math.max(nums[i],nums[i]+dp[i-1])

代码如下:

public int maxSubArray(int[] nums){
    if(nums.length==0) return 0;
    int[] dp=new int[nums.length];
    dp[0]=0;
    for(int i=1;i<nums.length;i++){
        dp[i]=Math.max(nums[i],nums[i]+dp[i-1]);
    }
    
    int res=Integer.MIN_VALUE;
    for(int i-0;i<nums.length;i++){
        res=Math.max(res,dp[i]);
    }
    return res;
}

这里可以看到,其实只用到了dp[i]和dp[i-1],因此可以降低空间复杂度

public int maxSubArray(int[] nums){
    if(nums.length==0) return 0;
	int dp_0=nums[0],dp_1=0;
    int res=dp_0;
    for(int i=1;i<nums.length;i++){
        dp_1=Math.max(nums[i],nums[i]+dp_0);
        dp_0=dp_1;
        res=Math.max(res,dp_1);
    }
    return res;
}

还可以在优化一下

class Solution {
    public int maxSubArray(int[] nums)
    {
        int sum=0;
        int max1=nums[0];
        for(int i=0;i<nums.length;i++)
        {
            if(sum>0)
                sum+=nums[i];
            else
                sum=nums[i];
            max1=Math.max(sum,max1);
        }
        return max1;
    }
}

5. 最长回文子串

这个题目处理可以使用中心扩散法也可以使用动态规划,而且动态规划具有普遍性。

首先确定dp【i】【j】表示s[i]到是s[j]是否是回文子串,是为ture,否为false。

base case则是所有的dp【i】【i】为true

转移方程为:

当s[i]!=s[j]时,dp【i】【j】=false

当s[i]==s[j]时,dp【i】【j】=dp【i+1】【j-1】

这里存在一个问题,如果i=j-1的时候,此时是不构成闭区间的,如果i=j-2的时候,恰好是dp【j-1】【j-1】

因此当s[i]==s[j]时,如果j-i<3,dp【i】【j】=true,否则dp【i】【j】=dp【i+1】【j-1】

代码如下

class Solution {
    public String longestPalindrome(String s) {
       int maxlen=1; //记录最长回文子串长度
        int index=0; //记录最长回文子串的起始位置
        char[] ch=s.toCharArray();
        int len=s.length();
        if(len==0) return "";
        if(len==1) return String.valueOf(ch[0]);
        boolean[][] dp=new boolean[len][len];
        for(int i=0;i<len;i++){
            dp[i][i]=true;     //base case
        }

        for(int j=1;j<len;j++){
            for(int i=0;i<j;i++){
                if(ch[i]!=ch[j]){
                    dp[i][j]=false;
                }else{
                    if(j-i<3){
                        dp[i][j]=true;
                    }else{
                        dp[i][j]=dp[i+1][j-1];
                    }
                }
                if(dp[i][j] && j-i+1>maxlen){
                    maxlen=j-i+1;
                    index=i;
                }
            }
        }

        return s.substring(index,index+maxlen);
    }
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值