LeetCode刷题(动态规划篇)

1.爬楼梯

1.确定dp数组以及下标的含义

dp[i]: 爬到第i层楼梯,有dp[i]种方法

2.确定递推公式

如果可以推出dp[i]呢?

从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。

首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。

还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。

那么dp[i]就是 dp[i - 1]与dp[i - 2]之和。

因为到dp就两种了。dp[i-1]再走一步。dp[i-2],再走两步。

3.dp数组如何初始化

不初始化第0步,从第1和第2步开始。

4.确定遍历顺序

从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的

class Solution {
    public int climbStairs(int n) {
        int dp[]=new int[n+1];
        if(n==1){
            return 1;
        }
        if(n==2){
            return 2;
        }
   int i=3;
   dp[1]=1;
   dp[2]=2;
        while(i<n+1){
            dp[i]=dp[i-1]+dp[i-2];
       i++;
        }
         return dp[n];
    }
    
}

2.使用最小花费爬楼梯

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int[] dp=new int[cost.length+1];
        if(cost.length==1){
            return cost[0];
        }
        if(cost.length==2){
            return Math.min(cost[0],cost[1]);
        }
        
        
        dp[0]=cost[0];
        dp[1]=cost[1];
       
        
        int i=2;
        while(i<cost.length){
             dp[i]=Math.min(dp[i-1],dp[i-2])+cost[i];//dp[i],记录i之前最小的与i位置继续跳的花费。
             i++;

        }
        //最后一步需要考虑一下。最后两步应该跳哪个。
        return Math.min(dp[cost.length-1],dp[cost.length-2]);
       

    }
}

3.不同路径

 

class Solution {
    public int uniquePaths(int m, int n) {
        //设dp路径的个数。
        int[][] dp=new int[m][n];
        //dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条。同理。到dp[1][0],的路径也只有一条。因为不可能再改道了,是一直往下走到那的。如果换方向就到不了,因为不能往左走。
        for (int i = 0; i < m; i++) dp[i][0] = 1;
     for (int j = 0; j < n; j++) dp[0][j] = 1;
        
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                 dp[i][j]=dp[i-1][j]+dp[i][j-1];//这俩没地方可走了。

            }
        }
        return dp[m-1][n-1];

    }
}

4.不同路径二

class Solution {
    //[i][j]上如果有石头了,dp[i][j]就等于0吧。
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m=obstacleGrid.length;
        int n=obstacleGrid[0].length;
        int [][] dp=new int[m][n];
   if(obstacleGrid[0][0]==1){
       return 0;
   }


        for (int i = 0; i < m; i++){//如果横的碰到个障碍物,那剩下的都走不了了。
            if(obstacleGrid[i][0]!=1){
                dp[i][0] = 1;

            }else{
                break;

            }
            


        }
       for (int j = 0; j < n; j++){
             if(obstacleGrid[0][j]!=1){//如果横的碰到个障碍物,从这点往后的都走不了了。
                 dp[0][j] = 1;

            }else{
                break;
            }

        } 

        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(obstacleGrid[i][j]!=1){
             dp[i][j]=dp[i-1][j]+dp[i][j-1];//如果[i][j-1]上有石头,那这个时候的路径就是只有dp[i-1][j]的。

        }

            }
        }
        return dp[m-1][n-1];


    }
}

5.整数拆分成两个以上的整数,让乘积最大

class Solution {
    //错误思路:直接开根号,然后取余直接完事。乘积最大化,肯定是从开根号那里乘。
    public int integerBreak(int n) {
//dp[0],dp[1]怎么设置呢?不设置d[0],d[1],这俩没有什么意义。也不会输入0和1。
      int [] dp=new int[n+1];

    dp[2]=1;
//i-j,最低能到2。dp[2],拆成两项以上的,所以dp[2]=1。
//同时j的上限也是i-2。保证i-j,最低是2,最低dp[2]。
if(n>=3)
{
    for(int i=3;i<=n;i++){
    for(int j=1;j<i-1;j++){
          dp[i]=Math.max(dp[i],Math.max(j*(i-j),j*dp[i-j]));//从前到后吧dp[i],求出来,因为dp[n],得用它推导。
          //得拆成两项以上。如果直接弄成dp[i-j]*dp[j],就要拆成4项以上了。

       }

}

}
     return dp[n];

    }
}

6.不同的二叉搜索树。

二叉搜索树的不同,就是根的左右子树结点不同。只看结点的布局,不用看值的差异。

class Solution {
    public int numTrees(int n) {
       // 从1到n数的先后输入顺序,也会导致建树不同。
       //大佬的思路:假设n个节点存在二叉排序树的个数是G(n),1为根节点,2为根节点,...,n为根节点,当1为根节点时,其左子树节点个数为0,右子树节点个数为n-1,同理当2为根节点时,其左子树节点个数为1,右子树节点为n-2,所以可得G(n) = G(0)*G(n-1)+G(1)*G(n-2)+...+G(n-1)*G(0)
       int [] G=new int[n+1];//得用到下标G(n)
       G[0]=1;
       G[1]=1;
       for(int i=2;i<n+1;i++){
           for(int j=1;j<=i;j++){
               G[i]+=G[j-1]*G[i-j];
           }
       }

      return G[n];

    }
}

7. 0-1背包问题

有n件物品和一个最多能背重量为w的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。

1.设置dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。

2.确定递推公式,

那么可以有两个方向推出来dp[i][j],

  • 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以被背包内的价值依然和前面相同。)
  • 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

所以递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);

3.确定初始值。

当背包剩余容量为0时,dp[i][0]=0;

当放0号物品的时候,如果d[0][j],j的容量大于0号物品的重量。就让d[0][j]=weight[0]。

所以,如此初始化。

在java里,数组的初始值就是0。所以只需要设置d[0][j]。

for (int j = 0 ; j < weight[0]; j++) {  // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。
    dp[0][j] = 0;
}
// 正序遍历
for (int j = weight[0]; j <= bagweight; j++) {
    dp[0][j] = value[0];
}
  public static void testweightbagproblem(int[] weight, int[] value, int bagsize){
        int wlen = weight.length, value0 = 0;
        //定义dp数组:dp[i][j]表示背包容量为j时,前i个物品能获得的最大价值
        int[][] dp = new int[wlen + 1][bagsize + 1];
        //初始化:背包容量为0时,能获得的价值都为0
        for (int i = 0; i <= wlen; i++){
            dp[i][0] = value0;
        }
        //遍历顺序:先遍历物品,再遍历背包容量
        for (int i = 1; i <= wlen; i++){
            for (int j = 1; j <= bagsize; j++){
                if (j < weight[i - 1]){
                    dp[i][j] = dp[i - 1][j];
                }else{
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
                }
            }
        }

一维滚动数组。

 public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
        int wLen = weight.length;
        //定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
        int[] dp = new int[bagWeight + 1];
        //遍历顺序:先遍历物品,再遍历背包容量
        for (int i = 0; i < wLen; i++){
            for (int j = bagWeight; j >= weight[i]; j--){
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }

8. 0-1背包应用于分割等和子集。

把nums[i]的值,看作重量的值和value的值。

  • 背包的体积为sum / 2
  • 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
  • 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
  • 背包中每一个元素是不可重复放入。
class Solution {
    public boolean canPartition(int[] nums) {
        
        int sum=0;
        for(int num:nums){
            sum+=num;
        }
        // 特判:如果是奇数,就不符合要求
        if ((sum %2 ) != 0) {
            return false;
        }
         int rong=sum/2;

        int[][] dp =new int [nums.length][rong+1];

        for(int j=nums[0];j<=rong;j++){
            dp[0][j]=nums[0];
        }
        for(int i=1;i<nums.length;i++){
            for(int j=0;j<=rong;j++){
                if(j<nums[i]) {
                    dp[i][j]=dp[i-1][j];

                }else{
                    dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-nums[i]]+nums[i]);

                }
             
            }
        }
//当走完了i,容量为rong的时候,如果里面这个值,正好是rong,也就是sum/2。说明找到了。
        if(dp[nums.length-1][rong]==rong){
            return true;
        }else{
            return false;
        }


    }
}

一维的解决办法

class Solution {
    public boolean canPartition(int[] nums) {
        int sum=0;
        
        for(int i=0;i<nums.length;i++)
        {
           sum+=nums[i];
        }
        int target=sum/2;
        double a=sum-target;
        if(a!=target) return false;

        int [] dp=new int [target+1];
        //背包容量为 target

        //一维背包,初始值。
        dp[0]=0;

        for(int i=0;i<nums.length;i++)
        {
            //一维背包,j的终点 是nums[i]。
            for(int j=target;j>=nums[i];j--)
            {
                dp[j]=Math.max(dp[j],dp[j-nums[i]] +nums[i]);
            }
        }
        if(dp[target]==target)
        {
            return true;
        }else {
            return false;
        }
        
    }
}

9  0-1背包用来解决最后一块石头的重量

 思路:本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了

然后    结果=sum-2*dp数组最后的长度    即可。

10 用0-1背包问题,解决装成target有多少种方法。

这题的代码正好解决了这一类的问题,背就完事。

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum =0;
        for(int num:nums){
            sum+=num;
        }
        //过程逆过来,留下来的数的和。留下来-减去的=target。留下来+减去的=sum。

        //所以 留下来的=(target+sum)/2。这个数必须得是整数
        if((target+sum)%2==1){
            return 0;
        }
        //如果目标值的绝对值大于sum,也没办法。
        if(Math.abs(target)>sum){
            return 0;
        }
    
        //此时问题就转化为,装满容量为x背包,有几种方法。回溯肯定超时。
        //dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。
        int left=(target+sum)/2;
        int [][] dp=new int [nums.length][left+1];

     //初始化
      dp[0][0]=1;

        //只装第0个物品的目标也是给背包装满。装满了才算一种。
        //dp[0][j],看第一个元素的大小情况,进行赋值1(如果第一个元素为0.则dp[0][0]应该为2)。
        for(int j=0;j<left+1;j++){
            if(j==nums[0]){
                dp[0][j]+=1;
            }
        }
       
        for(int i=1;i<nums.length;i++){
            //这里j从0开始。
            for(int j=0;j<left+1;j++){
                //不拿/拿 第i件物品的方案数
                if(j<nums[i]){
                    dp[i][j]=dp[i-1][j];
                }else{
                    dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i]];
                }
               
            }
        }
        return dp[nums.length-1][left];
        

    




    }
}

一维数组的

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) sum += nums[i];
        if ((target + sum) % 2 != 0) return 0;
        int size = (target + sum) / 2;
        if(size < 0) size = -size;
        int[] dp = new int[size + 1];
        dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = size; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[size];
    }
}

11.0-1背包的二维背包。(记得这样做,就行,不用管过程)

记得这些代码有什么作用就行,不用管每一步怎么来的。

题目简要说明:找到的字符串,里面都会有0.1,

让找的字符串集合里最多有m个0,n个1。这个字符串集合尽量要大。

 而m 和 n相当于是一个背包,两个维度的背包

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        int len =strs.length;
        int [][]  nums = new int [len][2];
        //首先遍历一遍,把每个字符串0和1的数量存进新数组,然后用双重背包即可。
        for(int i=0;i<strs.length;i++)
        {
            int a=finZero(strs[i]);
            int b=findOne(strs[i]);
            nums[i][0]=a;
            nums[i][1]=b;
        }
        int [][] dp =new int [m+1][n+1];
        for(int i=0;i<len;i++)
        {
            for(int j=m;j>=nums[i][0];j--)
            {
                for(int k=n;k>=nums[i][1];k--)
                {
                    //这个递推公式是求个数,所以是+1
                 dp[j][k]=Math.max(dp[j][k],dp[j-nums[i][0]][k-nums[i][1]]+1);
                }
            }
        }
        return  dp[m][n];
    }

    //这两个函数,发现每个字符串的0,1数量
    int findOne(String s)
    {
        int cnt=0;
        for(int i=0;i<s.length();i++)
        {
            if(s.charAt(i)=='1')   cnt++;  
        }
        return cnt;
    }
    int finZero(String s)
    {
        int cnt=0;
        for(int i=0;i<s.length();i++)
        {
             if(s.charAt(i)=='0')   cnt++;
        }
        return cnt;
    }

}

12.完全背包问题。

有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。

//先遍历物品,再遍历背包
private static void testCompletePack(){
    int[] weight = {1, 3, 4};
    int[] value = {15, 20, 30};
    int bagWeight = 4;
    int[] dp = new int[bagWeight + 1];
    for (int i = 0; i < weight.length; i++){ // 遍历物品
        for (int j = weight[i]; j <= bagWeight; j++){ // 遍历背包容量
            dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
        }
    }
    for (int maxValue : dp){
        System.out.println(maxValue + "   ");
    }
}

//先遍历背包,再遍历物品
private static void testCompletePackAnotherWay(){
    int[] weight = {1, 3, 4};
    int[] value = {15, 20, 30};
    int bagWeight = 4;
    int[] dp = new int[bagWeight + 1];
    for (int i = 1; i <= bagWeight; i++){ // 遍历背包容量
        for (int j = 0; j < weight.length; j++){ // 遍历物品
            if (i - weight[j] >= 0){
                dp[i] = Math.max(dp[i], dp[i - weight[j]] + value[j]);
            }
        }
    }
    for (int maxValue : dp){
        System.out.println(maxValue + "   ");
    }
}

13.(零钱兑换)完全背包问题,装到target有多少种?

一维数组的。太啥比,就是把本章第10题的,内层for的遍历顺序调反即可。

class Solution {
    public int change(int amount, int[] nums) {
        int [] dp=new int [amount+1];
         dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = nums[i]; j <amount+1; j++) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[amount];
        

    }
}

14.组合综合IV(外层遍历背包)

组合不强调顺序,(1,5)和(5,1)是同一个组合。

排列强调顺序,(1,5)和(5,1)是两个不同的排列。

看题目,本题其本质是本题求的是排列总和,而且仅仅是求排列总和的个数

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

 此时当外层for遍历背包时,得让j从0开始。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int[] dp=new int[target+1];
        dp[0]=1;
        for(int i=0;i<target+1;i++){
            for(int j=0;j<nums.length;j++){
                if(i>=nums[j])//外层遍历容量时,里面容量i必须大于nums[j]
                dp[i]+=dp[i-nums[j]];//此时外层遍历背包容量时,写成这样
            }
        }
        return dp[target];

    }
}

15.爬楼梯(外层遍历背包)

比如上4楼,112和221是两种排列。用完全背包。

16.零钱兑换(求零钱组成target的最少硬币数)(求最少的target典范)

这题可以看出,当dp求最多时,除了dp[0],都会初始化成0,来递推比较,在递推公式那里用max函数比较。

当dp求最少的时候,除了除了dp[0],都会初始化成int的MAX_VALUE,在递推公式那里用min函数比较。

class Solution {
    public int coinChange(int[] coins, int amount) {
        int[] dp=new int[amount+1];//最少需要的硬币数
        
        int max=Integer.MAX_VALUE;
        //初始化dp数组为最大值
        for (int j = 0; j < dp.length; j++) {
            dp[j] = max;
        }
        dp[0]=0;
        for(int i=0;i<coins.length;i++){
            for(int j=coins[i];j<amount+1;j++){
                if(dp[j-coins[i]]!=max){
                     //只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
                    dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);

                }
                
            }

        }
        //如果没有通往前面的道路,那dp[amount]的值不会到dp[0],而是保持初始值
        if(dp[amount]==max){
            return -1;
        }else{
            return dp[amount];

        }


    }
}

17.16题的变种,可变的nums[i]数组,我们在循环中构建它

class Solution {
    //我们拿的数得是平方的数。
    public int numSquares(int n) {
        int[] dp=new int[n+1];
        
        int max=Integer.MAX_VALUE;
        //初始化dp数组为最大值
        for (int j = 0; j < dp.length; j++) {
            dp[j] = max;
        }
        dp[0]=0;
        //这里的难点就是把nums[i],转化为i*i
        for(int i=0;i*i<=n;i++){
            for(int j=i*i;j<=n;j++){
                if(dp[j-i*i]!=max){
                     //只有dp[j-i*i]不是初始最大值时,该位才有选择的必要
                    dp[j]=Math.min(dp[j],dp[j-i*i]+1);

                }
                
            }

        }
        return dp[n];

    }
}

18.单词拆分,用字典里所给的字符串    拼出所需要的字符串S

此题类似于分割回文串。

思路:挨个判断,S里的字符串是不是在字典里都出现过。

在for里创造一个[j,i)的子串区间,判断这个子串是否在字典里出现过。

class Solution {
    //思路:挨个判断,S里的字符串是不是在字典里都出现过。
    public boolean wordBreak(String s, List<String> wordDict) {
        boolean [] dp=new boolean[s.length()+1];
        //默认为false了。因此你要把true往后传递呀
        dp[0]=true;
        //得用sbstring方法,i肯定在右侧,i-1得>=0,所以让i从1开始,j从0开始,
        for(int i=1;i<=s.length();i++){
            //做右界,所以范围为1-
            for(int j=0;j<i;j++){
                //之前访问到j这里也得是true,必须得所有的单词都在字典里
                if(wordDict.contains(s.substring(j,i))&&dp[j]==true){
                    dp[i]=true;
                }
            }
        }
        return dp[s.length()];


    }
}

19.多重背包问题

有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。

比如说这种题是多重背包。

我们可以把它摊开 ,转化成了01背包的问题。

重新建一个数组,将3维变成二维。

public void testMultiPack1(){
    // 版本一:改变物品数量为01背包格式
    List<Integer> weight = new ArrayList<>(Arrays.asList(1, 3, 4));
    List<Integer> value = new ArrayList<>(Arrays.asList(15, 20, 30));
    List<Integer> nums = new ArrayList<>(Arrays.asList(2, 3, 2));
    int bagWeight = 10;

    for (int i = 0; i < nums.size(); i++) {
        while (nums.get(i) > 1) { // 把物品展开为i
            weight.add(weight.get(i));
            value.add(value.get(i));
            nums.set(i, nums.get(i) - 1);
        }
    }

    int[] dp = new int[bagWeight + 1];
    for(int i = 0; i < weight.size(); i++) { // 遍历物品
        for(int j = bagWeight; j >= weight.get(i); j--) { // 遍历背包容量
            dp[j] = Math.max(dp[j], dp[j - weight.get(i)] + value.get(i));
        }
        System.out.println(Arrays.toString(dp));
    }
}

20.打家劫舍

题目描述:每家都有钱,不能连续偷两家。

分析:

1.如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。

2.如果不偷第i房间,那么dp[i] = dp[i - 1],即考虑i-1房,(注意这里是考虑)

然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);

从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1]

,选好出发点。

从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]);

这里没有背包,不是背包的题,所以只需要遍历数组即可。

// 动态规划
class Solution {
	public int rob(int[] nums) {
		if (nums == null || nums.length == 0) return 0;
		if (nums.length == 1) return nums[0];

		int[] dp = new int[nums.length];
		dp[0] = nums[0];
		dp[1] = Math.max(dp[0], nums[1]);
		for (int i = 2; i < nums.length; i++) {
			dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
		}

		return dp[nums.length - 1];
	}
}

21.打家劫舍||

题目表述:要偷的房子围成了一圈,首位相邻,不能偷相邻的屋子。

这题还是得动态规划,不能省事只算不相邻。

成环的情况可以分为三种处理。

  • 情况一:考虑不包含首尾元素

  • 情况二:考虑包含首元素,不包含尾元素

  • 情况三:考虑包含尾元素,不包含首元素

 成环问题:情况二,三包括情况1。

所以我们按照下标建两个数组,比较它们的结果即可。

class Solution {
    public int rob(int[] nums) {

      if (nums == null || nums.length == 0)
            return 0;

        int len = nums.length;
        if (len == 1)
            return nums[0];
        return Math.max(work(nums, 0, len-2), work(nums, 1, len-1));
     
    }
     int work(int[] p,int start,int end){

        if(start==end){
            return p[start];
        }
        if(end-start==1){
            return Math.max(p[start],p[end]);
        }

        int[] nums=new int[end-start+1];
        int index=0;
        for(int i=start;i<=end;i++){
            nums[index]=p[i];
            index++;
        }


        //新建一个数组nums。此时就可以看成,可以忽略首尾相连的了。
       
        int[] dp=new int[nums.length];
        dp[0]=nums[0];
        dp[1]=Math.max(nums[0],nums[1]);
        for(int i=2;i<nums.length;i++){
            dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[nums.length-1];
        
       
    }
}

22.打家劫舍|||(树形动态规划)

经过仔细分析(手动严肃脸),正确的解题思路大致是这样的:

  • 对于一个以 node 为根节点的二叉树而言,如果尝试偷取 node 节点,那么势必不能偷取其左右子节点,然后继续尝试偷取其左右子节点的左右子节点。

  • 如果不偷取该节点,那么只能尝试偷取其左右子节点。

  • 比较两种方式的结果,谁大取谁。

暴力递归

class Solution {
    // 1.递归去偷,超时
    public int rob(TreeNode root) {
        if (root == null)
            return 0;
        int money = root.val;
        if (root.left != null) {
            money += rob(root.left.left) + rob(root.left.right);
        }
        if (root.right != null) {
            money += rob(root.right.left) + rob(root.right.right);
        }
        return Math.max(money, rob(root.left) + rob(root.right));
    }
}

我们计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况(return 那里又计算了)

所以计算左右孩子的时候其实又把孙子计算了一遍。复杂度为n的平方。

所以我们剪枝,如果这个值已经被计算过了,那就直接拿来用,不用再重新计算。

//后序遍历,用儿子的与父亲比较,如果父亲的大,则拿父亲的钱,则下一步去找父亲的孙子。
class Solution {
    Map<TreeNode,Integer> map=new HashMap<>();

    public int rob(TreeNode root) {
        if(root==null){
            return 0;
        }
        if(map.containsKey(root)){
            return map.get(root);
        }
        int money=root.val;
        if(root.left!=null){
            money+=rob(root.left.left)+rob(root.left.right);
        }
         if(root.right!=null){
            money+=rob(root.right.left)+rob(root.right.right);
        }
        
         int res=Math.max(money,rob(root.left)+rob(root.right));
         map.put(root,res);
        return res;

    }


}

23.买卖股票的最佳时机一

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。

1.贪心算法的做法

class Solution {
    //选左侧最小值,然后一边遍往右走,用右侧最大值减去最小值。
    public int maxProfit(int[] prices) {
        int low=Integer.MAX_VALUE;
        int res=0;
        for(int i=0;i<prices.length;i++){
            low=Math.min(prices[i],low);
            res=Math.max(prices[i]-low,res);
        }
        return res;
         

    }
}

2.动态规划的做法

思路:确定dp,dp[i][0] 表示第i天持有股票所得最多现金,持有的概念就是自己还有股在身上。

                          dp[i][1] 表示第i天不持有股票所得最多现金,自己手里没有股票。

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

  • 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
  • 第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i]。只在今天这一天出手

那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]);

如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来

  • 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1]
  • 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0]

同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);

所以我们得初始化俩值。dp[0][0]和dp[0][1],

那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] =  -prices[0];

dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0;

确定遍历顺序

从递推公式可以看出dp[i]都是有dp[i - 1]推导出来的,那么一定是从前向后遍历。

class Solution {
  
    public int maxProfit(int[] prices) {
        int [][] dp=new int[prices.length][2];
        dp[0][0]=-prices[0];
        dp[0][1]=0;
        for(int i=1;i<prices.length;i++){
            dp[i][0]=Math.max(dp[i-1][0],-prices[i]);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
        }
        return dp[prices.length-1][1];
       

    }
}

24.买卖股票的最佳时机二

在这一周里可以你可以尽可能地完成更多的交易(多次买卖一支股票)。一个星期可以交易好几次,与上一题不同,上一次只能买一次。

这里重申一下dp数组的含义:

  • dp[i][0] 表示第i天持有股票所得现金。
  • dp[i][1] 表示第i天不持有股票所得最多现金

如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来

  • 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0]
  • 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i]

买新的时候,必须得之前不持有。在第i天持有时,还可能有之前赚的钱在。

class Solution {
    public int maxProfit(int[] prices) {
            int [][] dp=new int[prices.length][2];
        dp[0][0]=-prices[0];
        dp[0][1]=0;
        for(int i=1;i<prices.length;i++){
            //与第一题不同,这里改了
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
        }
        return dp[prices.length-1][1];

    }
}

25.买卖股票的最佳时机III

这次给了局限,你最多只能完成两次交易。 

1.确定dp数组有以下几种状态。

  

        0没有操作(没有持有)

        1第一次买入(第一次持有)

        2第一次卖出(第一次不持有)

        3第二次买入 (第二次持有)

       4第二次卖出   (第二次不持有)

dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。

需要注意:dp[i][1],表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票(即第一次持有)

2.达到dp[i][1]状态,有两个具体操作:

  • 操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i]
  • 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1]
  • dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]);

同理dp[i][2]也有两个操作:

  • 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i]
  • 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2]

所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2])。

所以

dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]);

dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]);

3.确定初始值。

dp[0][0] = 0;第0天也没操作。

dp[0][1] = -prices[0];

dp[0][2]=0;//今天买今天卖。

dp[0][3]=-prices[0];//又买一遍。

dp[0][4]==0;

4.确定遍历顺序

从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp=new int[prices.length][5];
        dp[0][0]=0;
        dp[0][1]=-prices[0];
        dp[0][2]=0;
        dp[0][3]=-prices[0];
        dp[0][4]=0;

        for(int i=1;i<prices.length;i++){
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
            dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]+prices[i]);
            dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
            dp[i][4]=Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
      
        }
        return dp[prices.length-1][4];

    }
}

26.买卖股票的最佳时机四

上一题的两笔改成k笔。

class Solution {
    public int maxProfit(int k, int[] prices) {
        if (prices.length == 0) return 0;
        int[][] dp=new int[prices.length][2*k+1];
        //初始化数组。
        for(int i=0;i<=2*k;i++){
            if(i%2==0){
                dp[0][i]=0;

            }else{
               dp[0][i]=-prices[0];
            } 
        }
        for(int i=1;i<prices.length;i++){
            //改造了一下,只能交易二次的那一题
            for(int j=1;j<=2*k;j++){
                if(j%2==1){
                    dp[i][j]=Math.max( dp[i - 1][j],dp[i-1][j-1] - prices[i]);
                }else{
                    dp[i][j]=Math.max( dp[i - 1][j],dp[i-1][j-1] +prices[i]);
                }
               
            }
            
        }
        return dp[prices.length-1][2*k];

    }
}

27.最佳买卖股票时机含冷冻期(复杂)

之前多次买卖同一只股票,这里如果今天卖了,那么明天不能买,得隔一天才能买。这次的买入,距离上次的卖出,得中间隔着一天。

思路:分为四种状态。

  • 状态一:持有股票状态,今天买入股票,或者是之前就买入了股票然后没有操作
  • 卖出股票状态,这里就有两种卖出股票状态
    • 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态
    • 状态三:今天卖出了股票.
  • 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

达到持有股票状态(状态一)即:dp[i][0],有两个具体操作:

  • 操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0]
  • 操作二:今天买入了,有两种情况
    • 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i]
    • 前一天已经卖了,保持卖出状态(状态二)再买入,dp[i - 1][1] - prices[i]
    • 所以操作二取最大值,即:max(dp[i - 1][3], dp[i - 1][1]) - prices[i]

达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作:

  • 操作一:前一天就是状态二(即已经卖出)
  • 操作二:前一天是冷冻期(状态四),延续这个卖出状态。

dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);

达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作:

  • 操作一:昨天一定是持有股票状态(状态一),今天卖出

即:dp[i][2] = dp[i - 1][0] + prices[i];

达到冷冻期状态(状态四),即:dp[i][3],只有一个操作:

  • 操作一:昨天卖出了股票(状态三)

dp[i][3] = dp[i - 1][2];

综上所述

dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]);
dp[i][2] = dp[i - 1][0] + prices[i];
dp[i][3] = dp[i - 1][2];

这里主要讨论一下第0天如何初始化。

如果是持有股票状态(状态一)那么:dp[0][0] = -prices[0],买入股票所剩现金为负数。

保持卖出股票状态(状态二),第0天没有卖出dp[0][1]初始化为0就行,

今天卖出了股票(状态三),同样dp[0][2]初始化为0,因为最少收益就是0,绝不会是负数。

同理dp[0][3]也初始为0。

最后结果是取 状态二,状态三,和状态四的最大值,不少同学会把状态四忘了,状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。

class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp=new int [prices.length][4];
        dp[0][0]=-prices[0];
        dp[0][1]=0;
        dp[0][2]=0;
        dp[0][3]=0;
        for(int i=1;i<prices.length;i++){
            dp[i][0] = Math.max(dp[i - 1][0], Math.max(dp[i - 1][3], dp[i - 1][1]) - prices[i]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][3]);
            dp[i][2] = dp[i - 1][0] + prices[i];
            dp[i][3] = dp[i - 1][2];
        }
        //最后结果是取 状态二,状态三,和状态四的最大值,是冷冻期也可能是最大值。
       return Math.max(dp[prices.length-1][1],Math.max(dp[prices.length-1][2],dp[prices.length-1][3]));


    }
}

28.买卖股票的最佳时机(含手续费)多次买一支,但有手续费。

class Solution {
    public int maxProfit(int[] prices, int fee) {

        int [][] dp=new int[prices.length][2];
        dp[0][0]=-prices[0];
        dp[0][1]=0;//表示第1天不持有股票所得最多现金
        for(int i=1;i<prices.length;i++){
            //的区别就是这里需要多一个减去手续费的操作。
            dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]-fee);
        }
        return dp[prices.length-1][1];

    }
}

30.最长递增子序列(没看懂)

 我没用动态规划的方法做,错了。这题应该用动态规划。

dp[i]表示i之前包括i的以nums[i]结尾最长上升子序列的长度.

位置  i   的最长升序子序列   等于   j从0到i-1各个位置的最长升序子序列 + 1 的最大值。

dp[i],所有的位置上初始值都为1。因为包括它们自身。

class Solution {
    public int lengthOfLIS(int[] nums) {
        if(nums==null||nums.length==0){
            return 0;
        }
        if(nums.length==1){
            return 1;
        }
        int[] dp=new int[nums.length];
        Arrays.fill(dp,1);

        for(int i=1;i<nums.length;i++){
            for(int j=0;j<=i-1;j++){
                //我现在在位置i,如果我比这之前的有个数要大,那么长度就为这个数的最长+1。
                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(dp[i],res);
       }
       return res;
      
    }
}

31.最长连续递增序列

没用动态规划,因为太简单了。

class Solution {
    public int findLengthOfLCIS(int[] nums) {
        if(nums==null||nums.length==0){
            return 0;
        }
        if(nums.length==1){
            return 1;
        }

        int pre=0;
        int maxLength=1;
        //记录一下最长连续增加即可。用pre表示左区间。
        for(int i=0;i<nums.length-1;i++){
            if(nums[i+1]>nums[i]){
                maxLength=Math.max(maxLength,i+1-pre+1);
            }else{
                pre=i+1;
            }

        }
        return maxLength;

    }
}



        

32.最长重复子数组(用我的思路,别看代码随想录)

我用双指针的暴力破解方法失败!

dp[i][j] :以下标i 为结尾的A,和以下标j 为结尾的B,最长重复子数组长度为dp[i][j]。

根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。

即当A[i ] 和B[j ]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;

所以 i和j都要从1开始遍历。

初始化dp[i][0]与dp[0][j],因为dp[i][j]从这里推导出来。如果碰到相等的,让它们为1即可。

 for(int j=0;j<nums2.length;j++ ){
            if(nums1[0]==nums2[j]){
                dp[0][j]=1;
                res=1;
            }
        }
        for(int j=0;j<nums1.length;j++ ){
            if(nums1[j]==nums2[0]){
                dp[j][0]=1;
                res=1;
            }
        }

所以dp[i][0] 和dp[0][j]初始化为0。

并不是dp[i][j]里的i和j都取最大值时,才有最长重复子数组。dp[i][j]那里可能是0。

所以在这个过程需要一个res来记录。

完整代码如下

class Solution {
    public int findLength(int[] nums1, int[] nums2) {

        int[][] dp=new int[nums1.length+1][nums2.length+1];
        int res=0;
        //因为 dp[i][j]=dp[i-1][j-1]+1;,i和j必须从1开始,
        for(int j=0;j<nums2.length;j++ ){
            if(nums1[0]==nums2[j]){
                dp[0][j]=1;
                res=1;
            }
        }
        for(int j=0;j<nums1.length;j++ ){
            if(nums1[j]==nums2[0]){
                dp[j][0]=1;
                res=1;
            }
        }
        for(int i=1;i<nums1.length;i++){
            for(int j=1;j<nums2.length;j++){
                if(nums1[i]==nums2[j]){
                    dp[i][j]=dp[i-1][j-1]+1;
                    res=Math.max(res,dp[i][j]);
                }
            }
        }
        return res;

       
}
}

不用两个for来定义初始值的版本,多给了一个长度。

class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int result = 0;
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];
        
        for (int i = 1; i < nums1.length + 1; i++) {
            for (int j = 1; j < nums2.length + 1; j++) {
                if (nums1[i - 1] == nums2[j - 1]) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    result = Math.max(result, dp[i][j]);
                }
            }
        }
        
        return result;
    }
}

33.最长公共子序列

与最长重复子数组的区别是,里面的元素重复时,可以不是连续的。

处理两种情况,当这俩不相等时,你得取

dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int[][] dp=new int[text1.length()+1][text2.length()+1];
        //dp[i][j] : S1的0到i-1,S2的0到j-1    最长公共子序列长度。
     
        int res=0;
         for(int i=1;i<=text1.length();i++){
             for(int j=1;j<=text2.length();j++){
                 if(text1.charAt(i-1)==text2.charAt(j-1)){
                     dp[i][j]=Math.max(dp[i][j],dp[i-1][j-1]+1);
                     res=Math.max(res,dp[i][j]);

                 }else{
                     dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]);
                     res=Math.max(res,dp[i][j]);
                 }

             }
            
         }
       return res;

    }
}

34.不相交的线

这题与最长公共子序列一摸一样。

class Solution {
  public int maxUncrossedLines(int[] A, int[] B) {
  	int [][] dp = new int[A.length+1][B.length+1];
      int res=0;
  	for(int i=1;i<=A.length;i++) {
  		for(int j=1;j<=B.length;j++) {
  			if (A[i-1]==B[j-1]) {
  				dp[i][j]=dp[i-1][j-1]+1;
                  res=Math.max(dp[i][j],res);
  			}
  			else {
  				dp[i][j]=Math.max(dp[i-1][j], dp[i][j-1]);
  			}
  		}
  	}
      //也可以返回dp[A.length][B.length];因为与最长公共子序列那题不同的是,它只走一遍。
  	return res;
  }
}

35.最大子数组之和(连续的)

class Solution {
    public int maxSubArray(int[] nums) {
        if(nums.length==1){
            return nums[0];
        }
            int [] dp=new int[nums.length];
            int res=nums[0];
            dp[0]=nums[0];
        //当dp[i]小于0了,就让它从下一个整数开始。
            for(int i=1;i<nums.length;i++){
                dp[i]=Math.max(nums[i],dp[i-1]+nums[i]);    
                res=Math.max(res,dp[i]);   
            }
           
            return res;

    }
}

36.判断子序列

进阶:

如果有大量输入的 S,称作 S1, S2, ... , Sk 其中 k >= 10亿,你需要依次检查它们是否为 T 的子序列。在这种情况下,你会怎样改变代码?

如果考察的是连续子序列,用KMP算法。

用动态规划吧。最长公共子序列那一套。如果最长公共子序列长度一样,则是。

class Solution {
    public boolean isSubsequence(String s, String t) {
        int[][] dp=new int[s.length()+1][t.length()+1];
        //dp[i][j] : S1的0到i-1,S2的0到j-1    最长公共子序列长度。
     
        int res=0;
         for(int i=1;i<=s.length();i++){
             for(int j=1;j<=t.length();j++){
                 if(s.charAt(i-1)==t.charAt(j-1)){
                     dp[i][j]=Math.max(dp[i][j],dp[i-1][j-1]+1);
                     res=Math.max(res,dp[i][j]);

                 }else{
                     dp[i][j]=Math.max(dp[i][j-1],dp[i-1][j]);
                     res=Math.max(res,dp[i][j]);
                 }

             }
            
         }
       return (res==s.length());
    }
       
}

37.不同的子序列(完全不会)

 错误思路:用最长公共子序列的方法,当dp里长度==子序列的时候+1。

  1. 确定dp数组(dp table)以及下标的含义

dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。

2.这一类问题,基本是要分析两种情况

  • s[i - 1] 与 t[j - 1]相等
  • s[i - 1] 与 t[j - 1] 不相等

当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。

一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。

一部分是不用s[i - 1]来匹配(看作s[i - 1] 与 t[j - 1]不相等),个数为dp[i - 1][j]。因为t是s的子串

例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。

所以当s[i - 1] 与 t[j - 1]相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];

当s[i - 1] 与 t[j - 1]不相等时(真正的不相等),dp[i][j]只有一部分组成,不用s[i - 1]来匹配,即:dp[i - 1][j]

因为已知t只能是s的子串,在前面最长公共子序列里取的是max(dp[i][j-1],dp[i-1][j]);因为不知道谁是谁的子串。

3.初始值怎么设置?

从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j];

当i等于0时,有dp[0][j],当j等0时,有dp[i-1][0]

中可以看出dp[i][0] 和dp[0][j]是一定要初始化的。

dp[0][j]肯定为0。此时S的  0减去1=-1  之前,t肯定匹配不上。

dp[i][0]。此时t之前的 下标为-1的串,匹配结果只有 1个。所以定为1。

class Solution {

    public int numDistinct(String s, String t) {
        //dp[i][j]:以i-1为结尾的s的子序列   中  出现以j-1为结尾的t的子序列的个数为dp[i][j]。
        int[][] dp=new int[s.length()+1][t.length()+1];
        //dp[0][j]肯定为0。此时S的  0减去1=-1  之前,t肯定匹配不上。
        //dp[i][0]。此时t之前的 下标为-1的串,匹配结果只有 1个。所以定为1。
        for(int i=0;i<s.length();i++){
            dp[i][0]=1;
        }
        for(int i=1;i<=s.length();i++){
            for(int j=1;j<=t.length();j++){
                 if(s.charAt(i-1)==t.charAt(j-1)){
                     //左面加的是当作匹配上,右边加的是当作匹配不上
                     dp[i][j]=dp[i-1][j-1]+dp[i-1][j];

                 }else{
                     //确实匹配不上时
                     dp[i][j]=dp[i-1][j];
                 }

             }
        }
        //这个时候递推肯定最大
        return dp[s.length()][t.length()];

    }
}

38.两个字符串的删除操作

class Solution {
    //找到最长公共子序列的长度,然后分别去除两个字符串中除了最长公共子序列的字符数。
    public int minDistance(String S1, String S2) {
        int[][] dp=new int[S1.length()+1][S2.length()+1];
        int res=0;
        for(int i=1;i<=S1.length();i++){
            for(int j=1;j<=S2.length();j++){
                if(S1.charAt(i-1)==S2.charAt(j-1)){
                    dp[i][j]=Math.max(dp[i][j],dp[i-1][j-1]+1);
                    res=Math.max(res,dp[i][j]);
                }else{
                    dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        return S1.length()+S2.length()-2*res;


    }
}

39.编辑距离(不能使用最长公共子序列,部分测试用例无法通过)

1.dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]

2.确定递推公式

if (word1[i - 1] == word2[j - 1])
    不操作
if (word1[i - 1] != word2[j - 1])
    增
    删
    换

if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];

if (word1[i - 1] != word2[j - 1]),此时就需要编辑了,如何编辑呢?

  • 操作一删除元素:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。即  dp[i][j] = dp[i - 1][j] + 1;
  • 操作二增加元素:不相等时,word增了一个,意味着此时指针指向的word1与word2的值相等,所以我们应该删掉word2的这一项。word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。即 dp[i][j] = dp[i][j - 1] + 1;
  • 操作三替换元素:,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增加元素,那么以下标i-2为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个替换元素的操作。     即 dp[i][j] = dp[i - 1][j - 1] + 1;//换成一样的了。

3.初始化问题,dp[i][0] 和 dp[0][j]肯定得用到。

那么dp[i][0] 和 dp[0][j] 表示什么呢?

dp[i][0]应该表示word1的子序列   0到i-1    全删光,共i个操作步骤。

dp[0][j]应该表示word1从头增加到word2子序列   0到j-1的长度,共j个步骤。

dp[0][0]=0;这俩都是空了都不用动手。

class Solution {
    //将word1转换成word2所用最少操作数。
    public int minDistance(String S1, String S2) {
       

        //dp表示 S1的 0到i-1子串,与S2的0到j-1的子串的最短编辑距离。
        int[][] dp=new int[S1.length()+1][S2.length()+1];
        for(int i=0;i<=S1.length();i++){
            dp[i][0]=i;
        }
         for(int j=0;j<=S2.length();j++){
            dp[0][j]=j;
        }

        for(int i=1;i<=S1.length();i++){
            for(int j=1;j<=S2.length();j++){
                if(S1.charAt(i-1)==S2.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1];
                   
                }else{
                    //求的是最小编辑距离
                    dp[i][j]= Math.min(Math.min(dp[i-1][j]+1,dp[i][j-1]+1),dp[i-1][j-1]+1);
                }
            }
        }
        return dp[S1.length()][S2.length()];
       
    
        

    }
}


40.回文子串

动态规划做法

布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。

整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。

当s[i]与s[j]不相等,dp[i][j]一定是false。

当s[i]与s[j]相等时,有如下三种情况

  • 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串
  • 情况二:下标i 与 j相差为1,例如aa,也是回文子串
  • 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。

dp[i + 1][j - 1],所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的

class Solution {
    public int countSubstrings(String s) {
        //dp[i][j]表示i和j这个区间的是回文子串。
        boolean [][] dp=new boolean[s.length()][s.length()];
       int count=0;
       for(int i=s.length()-1;i>=0;i--){
           //j得大于等于i
            for(int j=i;j<s.length();j++){
                  if(s.charAt(i)==s.charAt(j)){
                      //当a和aa时,直接可以设置位true。
                      if(j-i<=1){
                     dp[i][j]=true;
                      count++;
                      }else{
                          //j-i>1时,如果它的子是回文,且此时已有s[i]==s[j],则计数加一。
                          if(dp[i+1][j-1]){
                              dp[i][j]=true;
                            count++;
 
                          }
                      }         
            }
        }   
    }
    return count;
}
}

双指针做法:中心扩散法

首先确定回文串,就是找中心然后向两边扩散看是不是对称的就可以了。

在遍历中心点的时候,要注意中心点有两种情况

一个元素可以作为中心点,两个元素也可以作为中心点。

每个中心点标识一个字符串S。

class Solution {
    public int countSubstrings(String s) {
        int result=0;
        for(int i=0;i<s.length();i++){
            result+=work(s,i,i);
            result+=work(s,i,i+1);
        }

        
    return result;
   }
       int work(String S,int index,int index2){
           int count=0;
           //尽量把判断写在一起。不然可能会超时
           while(index2<S.length()&&index>=0&&S.charAt(index)==S.charAt(index2)){
               count++;
               index--;
               index2++;
           }
             return count;
           


    }
}

41.最长回文子序列(回文想到reverse)

序列的区别就是可以不连续。

方法一:动态规划复杂

 如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2;

如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子串的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。

加入s[j]的回文子序列长度为dp[i + 1][j]。

加入s[i]的回文子序列长度为dp[i][j - 1]。

那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);

dp数组如何初始化

首先要考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出 递推公式是计算不到 i 和 j 相同时候的情况。

所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。

class Solution {
    public int longestPalindromeSubseq(String s) {
        int [][] dp=new int [s.length()+1][s.length()+1];
        //i到j之间的最长回文子序列。

        // dp[i][j]=dp[i+1][j-1]+2;
        for(int i=s.length()-1;i>=0;i--){
            dp[i][i]=1;//我们只看两位的。
            for(int j=i+1;j<s.length();j++){
                if(s.charAt(i)==s.charAt(j)){
                    dp[i][j]=dp[i+1][j-1]+2;
                }else{
                    dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
                }
            }
        }
        //返回的区间应该位0-s.length()-1
        return dp[0][s.length()-1];

    }
}

方法二:s与s.reverse()的最长公共子序列即为其最长回文子序列

class Solution {
    public int longestPalindromeSubseq(String text1) {
        
        //用StringBuffer,反转一下。
        String text2=new StringBuffer(text1).reverse().toString();

         int[][] dp = new int[text1.length() + 1][text2.length() + 1]; // 先对dp数组做初始化操作
        for (int i = 1 ; i <= text1.length() ; i++) {
            char char1 = text1.charAt(i - 1);
            for (int j = 1; j <= text2.length(); j++) {
                char char2 = text2.charAt(j - 1);
                if (char1 == char2) { // 开始列出状态转移方程
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }
        return dp[text1.length()][text2.length()];
    }
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值