leetcode动态规划被虐

1.最长回文子串

思路解析:

他要输出的是 真实的子串,不是他的子串长度。所以普通的递归解决不了问题。

只能暴力列举法+递归

递归 用 left和right 左右两边开弓 ,且 返回值是 判断他是否是回文串 boolean

字符串左右两端是否一样 


        public String longestPalindrome(String s) {
            String ans = "";
            for (int i = 0; i < s.length(); i++) {
                for (int j = i; j < s.length(); j++) {
                    if (helper(s, i, j) && j - i + 1 > ans.length()) {
                        ans = s.substring(i, j + 1);
                    }
                }
            }
            return ans;
        }


        private boolean helper(String s, int start, int end) {
            if (start == end) return true;
            if (start + 1 == end) return s.charAt(start) == s.charAt(end);
            boolean ans = false;
            if (s.charAt(start) == s.charAt(end)) {
                ans = helper(s, start + 1, end - 1);
            }
            return ans;
        }

作者:a-fei-8
链接:https://leetcode.cn/problems/longest-palindromic-substring/solution/chang-you-mian-shi-zhong-de-dong-tai-gui-kvv1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 记忆化搜索

  Boolean[][] memo;

public String longestPalindrome(String s) {
    memo = new Boolean[s.length()][s.length()];
    String ans = "";
    for (int i = 0; i < s.length(); i++) {
        for (int j = i; j < s.length(); j++) {
            if (helper(s, i, j) && j - i + 1 > ans.length()) {
                ans = s.substring(i, j + 1);
            }
        }
    }
    return ans;
}


private boolean helper(String s, int start, int end) {
    if (start == end) return true;
    if (start + 1 == end) return s.charAt(start) == s.charAt(end);
    if (memo[start][end] != null) return memo[start][end];
    boolean ans = false;
    if (s.charAt(start) == s.charAt(end)) {
        ans = helper(s, start + 1, end - 1);
    }
    return memo[start][end] = ans;
}

作者:a-fei-8
链接:https://leetcode.cn/problems/longest-palindromic-substring/solution/chang-you-mian-shi-zhong-de-dong-tai-gui-kvv1/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

dp 按对角线一条一条填数据

class Solution {
    public String longestPalindrome(String s) {
        int start=0,maxLen=1;
        boolean[][] bol= new boolean[s.length()][s.length()];
        for(int i=0;i<s.length();i++){
            bol[i][i]=true;
        }
        for(int j=0;j<s.length();j++){
            for(int i=0;i<j;i++){
                if(j-i==1){bol[i][j]= s.charAt(i)==s.charAt(j)? true:false; }
                else{
                    bol[i][j]= s.charAt(i)==s.charAt(j) && bol[i+1][j-1];
                }
                 if (bol[i][j] && j - i + 1 > maxLen) {
                        maxLen = j - i + 1;
                        start = i;
                    }


            }
        }
    return s.substring(start,start+maxLen);

    }
}

学到的:

  1.  左右开工 left和right
  2.  暴力法 for( i=0  for(j=i) )
  3.  暴力和递归一起 用递归解决每一个问题 暴力法罗列出所有的情况   
  4.  二维数组的对角线填法 就是 用列作为大循环 行作为内部小循环for( j=0  for(i=j) )


2.  括号生成

 思路解析

首先他是从左到右模型,就和简单的机器人走路一样,走一个地方走左还是走右。

可以说他是树形图!

但是 由规则 开括号不能大于剩余括号数 (剪支)

所以用递归解决

dfs(int 开括号数量,int 剩余括号数量, String 当前合成的括号字符串)

class Solution {
    List<String> list = new ArrayList<String>();
    String a="(";
    String b=")";
    public List<String> generateParenthesis(int n) {
        dfs(1,n,a);
        return list;
    }

    public void dfs(int open,int num,String s){
        if(open>num) return;
        if(num==0) {list.add(s);return;}

        if(open>0){
        dfs(open-1,num-1,s+b);
        dfs(open+1,num,s+a);
        }
        else{
             dfs(open+1,num,s+a);
        }
        return;
    }
}

学到的

辅助dfs函数返回值为空 且有两种情况你都想走 那你直接内部的东西 dfs();dfs();

题目要的是具体的东西 而不是长度或数量这种,可以把具体的东西放入函数参数,最后出口再返回他们


3. 机器人走路

注意!!!

  1. 首先是 填dp数组的时候,一定要从右向左,从下向上的填。 因为 如果有障碍物,从左向右遇到就为0,那后面的路你不管了???
  2. 一定要小心测试用例! 比如家门口就是障碍物,或者目的地是障碍物,这种测试用例要想到
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        if(obstacleGrid[obstacleGrid.length-1][obstacleGrid[0].length-1]==1) return 0;
        return helper3(obstacleGrid,0,0);
    }

    public int helper1(int[][] ob,int down, int right){
        if(ob[down][right]==1) return 0;
        if(down==ob.length-1 && right==ob[0].length-1) return 1;
        if(down == ob.length-1 ) return helper1(ob, down, right+1);
        if(right == ob[0].length-1) return helper1(ob, down+1, right);
        return helper1(ob, down+1, right) + helper1(ob, down, right+1);
    }

    public int helper2(int[][] ob,int down, int right,int[][] dp){
        if(ob[down][right]==1) return dp[down][right]=0;
        if(dp[down][right]!=0) return dp[down][right];

        if(down==ob.length-1 && right==ob[0].length-1) return dp[down][right]=1;

        if(down == ob.length-1 ) return dp[down][right]=helper1(ob, down, right+1);
        if(right == ob[0].length-1) return dp[down][right]=helper1(ob, down+1, right);
        
        return dp[down][right]=helper1(ob, down+1, right) + helper1(ob, down, right+1);
    }

    public int helper3(int[][] ob,int down, int right){
        int m = ob.length, n = ob[0].length;
        int[][] dp=new int[ob.length][ob[0].length];

        for(int i=n-1;i>=0;i--) {
            if(ob[m-1][i]==1) break;
            else dp[m-1][i] =1;
            }
        for(int i=m-1;i>=0;i--){
            if(ob[i][n-1]==1) break;
            else dp[i][n-1] =1;
            }

        for(int i=m-2;i>=0;i--){
            for(int j=n-2;j>=0;j--){
                if(ob[i][j]==1) dp[i][j]=0;
                else dp[i][j] =dp[i+1][j]+dp[i][j+1];
            }
        }
        
        return dp[0][0];
    }

}

4. 买卖股票的最佳时机   最大子数组和

  超难点: 

有些问题的子问题,并不是题目问的问题,你需要灵活变通。

一般出现这种情况主要是:

        你发现,index后面的东西的最大连续和,你并不知道后面的最大连续和从哪开始从哪结束!!!

        也就是即便你知道了子问题的答案,你现问题想要解决还是要去遍历一遍后面的数组! 但是这样时间复杂度太高了!!!! 又或者 index开头的问题的答案,是你子问题的子问题。比如 我index后面的数组最大和! 是我index+3后面的答案,这种情况即便你知道子问题index+1也还是要操作一下

        那么你可以 重新定义子问题为: 我index这一天必须选,以index开头(或结尾的选法)其最大连续和是多少?   且 真正问题的答案 你要用全局变量max记录下来。

比如:

class Solution {
    public int maxProfit(int[] prices) {
        return maxP(prices,0);
    }

    public int maxP(int[] prices,int index){

        if(index==prices.length-1) return 0;

        int pro=maxP(prices,index+1);
        int now=0;
        int buy=prices[index];
        int max=-10000;
        // 我不知道卖股票是哪天了 我在知道子问题的情况下,还是要去看看我当前问题去哪天卖股票
        while(index<prices.length){
            now=prices[index]-buy;
            max=Math.max(max,now);
            index++;
        }
        return Math.max(pro,max);
    }
}
public int helper(int[] n, int index){
        if(index == n.length-1) return max = 0;
        int max_now;
        //子问题是 我index这天 必须买! 后续我能得到的最大利益

        // pre 是 index这天买 index+1卖股票那天我去卖 我能获得的收益
        int pre = helper(n,index+1)-(n[index]-n[index+1]);
      
        // 如果 我index这天比明天的股价要低 那我肯定今天买
        if(n[index]<=n[index+1])  max_now = pre;
        // 如果我比明天股价要高 那我就要看看 按照上述方法还去买今天会不会亏钱 亏钱谁还买啊
        else{
            max_now = pre > 0? pre : 0;
        }

        max= Math.max(max_now,max);
        return max_now;
    }

注意!!!! index这个点的东西 是必须在你递归或状态转移方程里的


5. 最长递增子序列

有时候你发现子问题的定义,不管是 寻常定义法,还是 index必包括定义法。都还是再次遍历后续子序列,也就是 你当前问题的解不一定是后一个子问题,还可能是后后后后后一个子问题。

那你就别找递归了!

看出转态转移,直接dp

nlogn 两层循环直接做

class Solution {
    int max = Integer.MIN_VALUE;
    public int lengthOfLIS(int[] nums) {
        // 1.0.  4.6.1.2.3
        return helper(nums);
    }

    public int helper(int[] nums) {
        if(nums.length == 1) return 1;
        int[] dp = new int[nums.length];
        Arrays.fill(dp,1);
        max=1;

        for(int index = nums.length-2; index>=0; index--) {
            for(int j = nums.length-1; j>=index; j--){
                if(nums[index]<nums[j]) dp[index] = Math.max(dp[j]+1,dp[index]);
                max= Math.max(dp[index],max);
            }
        }

        return max;
    }
}

6. 整数拆分 

收获

有时候你用递归,就是那种暴力递归,每一步分类讨论的情况。你要用for循环去找每一种方法。

这时候如果你的外循环是 开区间!!如本题目,3只能拆成 1+2 1+1+1,不能拆成3+0;

而子问题里面  rest被-1拆成2后  2必须得到。也就是内循环你必须是 闭区间。

那你可以把开区间放到你外面呀 ,内循环自成一个函数

class Solution {
    int[] dp;
    public int integerBreak(int n) {
        dp = new int[n];
        helper2(n);
        int max = 0;
        // for(int i = 1;i<n;i++){
        //     max= Math.max(i*helper(n,n-i),max);
        // }
        for(int i = 1; i<n; i++){
            max= Math.max(max,i*dp[n-i]);
        }

        return max;
    }

    public int helper(int n, int rest) {
        if(rest == 0) return 1;
        if(rest == 1) return 1;

        int max = 0;
        for(int i = 1;i<=rest;i++){
            max= Math.max(i*helper(n,rest-i),max);
        }
        return max;
    }

    public int helper2(int rest){
        
        
        dp[0] = 1;dp[1] = 1;
        for(int i = 2; i<rest;i++){
            for(int j = 1;j<=i;j++){
               dp[i] = Math.max(j*dp[i-j],dp[i]); 
            }            
        }
        return dp[rest-1];
    }
}

7. 组合总和

新收获 

这道题的和普通的取零钱组合几乎一样, 但是不同的在于! 这个有序列先后顺序。

我们写取零钱通常用了 index作为可变变量! 这就写死了 我们子问题只能用数组序号index之后的东西。不适用于这道题。

这个时候要考虑

 int helper(int[] nums,rest){
    for(int num : nums) {
            sum += helper(nums,rest-num);
      }
}
    

 如果说 index写法是一颗树,这种写法就是森林。完美的进行了有顺序的排列组合。

class Solution {
    int sum = 0;
    public int combinationSum4(int[] nums, int target) {
        helper(nums,target);
        return sum;
    }

    public void helper(int[] nums, int rest) {
        if(rest == 0) {sum += 1; return;  }
        if(rest < 0) return;

        for(int num : nums) {
            helper(nums,rest-num);
        }
    }
}
class Solution {
    
    public int combinationSum4(int[] nums, int target) {
        // int sum = helper(nums,target);
        // return sum;
        return helper2(nums,target);
    }

    public int helper(int[] nums, int rest) {
        if(rest == 0) {return 1;  }
        if(rest < 0) return 0;
        int sum = 0;
        for(int num : nums) {
            sum += helper(nums,rest-num);
        }
        // 这种循环递归方式, 其实只有第一轮才返回 sum 其他所有子程序都在出口返回1
        return sum;
    }

    public int helper2(int[] nums, int rest) {
        int[] dp = new int[rest+1];
        dp[0] = 1;
        for(int i = 1; i <= rest; i++) {
            int sum = 0;
            for(int num : nums) {
                if(i-num>=0)
                sum += dp[i-num];
            }
            dp[i] = sum;
        }
        return dp[rest];
    }

     
}

秘诀

1.理解不了你就画树状图

2.学会for循环的递归法

3.在递归变dp的时候,递归的可变参数 永远是你填dp数组的外部大循环  2的for循环是你dp的内部循环

4.循环的范围 不用想 肯定是从0 - rest 一个一个的填,你别管某种rest值娶不到怎么办,只管填就行。

5.这种没有index的限制后,填dp由于是一位数组,你直接从前向后填就行。其实平常的填dp从前向后还是从后向前都无所谓。


8. 1和0

新知识

 这种的限制有三种 index m n 所以dp就是三维

但是你别怕, 填dp数组永远,先填递归出口的特殊值。 然后根据你的动态转移方程,写外部大循环。其他的可变变量没有限制的话,直接从0-m一个一个填。只要没专门提 就一个一个填 别管这种情况会不会出现。


9. 等差数列划分子集

 注意

这道题和其他问题 有很大的易错点!!!

也给了一种测试的方法。

那就是 本题找的是子集,你用的sum把各种递归子问题结合在一起,很有可能加入了重复解,这和其他的找一共有多少种方法是不一样的。 比如凑零钱问题,你的子问题的答案虽然可能一样,但是你的问题的 是一整个大集合作为基础的,也就是 凑10块 你先凑5块声学的f(5) 和你先凑2块再凑3块的f(5)。尽管都是f(5)值一样,但对题目本身来说,这是两种方法,因为你对他们的解释分别是 5+5 和3+2+5,这是不一样的 。

而本题 问的是子集,你的f(5) 就代表 从5开始的子集,他没有一个大目标,不能在各种树的分叉整合进去。

class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        if(nums.length<2) return 0;
        return helper(nums, 0, 0);
    }

    public int helper(int[] nums, int index, int number) {
        if(index == nums.length ) return 0;
        if(number>1 && (nums[index]-nums[index-1]) != (nums[index-1]-nums[index-2]))  return 0;
        if(number>=2) return 1+helper(nums,index+1,number+1)+helper(nums,index+1, 0);

        int sum = 0;
        sum = helper(nums, index+1, number+1) + helper(nums,index+1, 0);
        return sum;

        
    }
}

这是错的,要分类讨论。 如果我number>1的时候,我就专注看这一条分支能不能变成等差数列,只有number==0的时候,我才去分类讨论 

class Solution {
    public int numberOfArithmeticSlices(int[] nums) {
        if(nums.length<2) return 0;
        return helper(nums, 0, 0);
    }

    public int helper(int[] nums, int index, int number) {
        if(index == nums.length ) return 0;
        if(number>1 && (nums[index]-nums[index-1]) != (nums[index-1]-nums[index-2]))  return 0;
        if(number>=2) return 1+helper(nums,index+1,number+1);

        int sum = 0;
        int cur1 = helper(nums, index+1, number+1);
        int cur2 = helper(nums,index+1, 0);
        sum = cur1 + cur2;
        if(number> 0) return cur1;
        return sum;

        
    }
}

10. dp数组的下标是负数的情况

 rest可能由于你的决策变成了负数,填dp数组的时候就出了问题,

方法一,用转移法,比如 dp[0+转移量] 等价于 dp[0]

方法二, 用哈希表+记忆化搜索


11.最大连续子数组乘积

 

心得

 除了 1.常规子问题    2. 必须index开头的子问题   

还有一种  递归是工具人!!!   把你子问题的那个答案 放在函数作为参数

本题就是明显的 ,如果非要用方法1和2,你的返回值并不能帮助你很好解决问题。

因为本题有两个限制:

全局最大值,和 连续子数组。

方法1不能做到连续,方法2判断太累

class Solution {

int max = Integer.MIN_VALUE;

public int maxProduct(int[] nums) {
    if (nums == null || nums.length == 0) {
        return -1;
    }
    helper(nums, 1, 0);
    return max;
}
private void helper(int[] nums, int product, int i) {
    if (i == nums.length) {
        return;
    }

    int select = nums[i] * product;
    int max_value = Math.max(nums[i], select);
    max = Math.max(max_value, max);

    helper(nums, nums[i], i + 1);
    helper(nums, select, i + 1);

}

}


dp

public int maxProduct(int[] nums) {
    if(nums == null || nums.length == 0) {
        return -1;
    }
    int[][] dp = new int[nums.length][2];
    dp[0][0] = nums[0];
    dp[0][1] = nums[0];
    int ans = nums[0];
    for(int i = 1; i < nums.length; i++) {
        dp[i][1] = Math.max(dp[i-1][1] * nums[i], Math.max(dp[i-1][0] * nums[i], nums[i])); // 取最大值
        dp[i][0] = Math.min(dp[i-1][1] * nums[i], Math.min(dp[i-1][0] * nums[i], nums[i])); // 取最小值
        ans = Math.max(ans, dp[i][1]); // 更新最大值
    }
    return ans;
}

作者:Yangcy-Zheng
链接:https://leetcode.cn/problems/maximum-product-subarray/solution/xiong-di-wo-jin-li-liao-by-yangcy-zheng/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这个题由于负数的存在,必须保存当前最小值

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值