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);
}
}
学到的:
- 左右开工 left和right
- 暴力法 for( i=0 for(j=i) )
- 暴力和递归一起 用递归解决每一个问题 暴力法罗列出所有的情况
- 二维数组的对角线填法 就是 用列作为大循环 行作为内部小循环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. 机器人走路
注意!!!
- 首先是 填dp数组的时候,一定要从右向左,从下向上的填。 因为 如果有障碍物,从左向右遇到就为0,那后面的路你不管了???
- 一定要小心测试用例! 比如家门口就是障碍物,或者目的地是障碍物,这种测试用例要想到
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)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这个题由于负数的存在,必须保存当前最小值