动态规划(再也不用担心动态规划了)

目录

 动态规划基础

力扣 509.斐波那契数

力扣 70.爬楼梯

力扣 746.使用最小花费爬楼梯

力扣 62.不同路径

力扣 63.不同路径Ⅱ

力扣 343.整数拆分

力扣 96.不同的二叉搜索树​

背包问题

01背包和完全背包

01背包

力扣 416分割等和子集

力扣 1049最后一块石头重量Ⅱ

力扣 494目标和

力扣 474.一和零

完全背包

力扣 518.零钱兑换

力扣 377.组合总和Ⅳ

力扣 70.爬楼梯(再爬一次)

力扣 322.零钱兑换

力扣 279.完全平方数

力扣 139.单词拆分

 打家劫舍

力扣 198.打家劫舍

力扣 213.打家劫舍Ⅱ

力扣 377.打家劫舍Ⅲ

股票系列

力扣 121.买卖股票的最佳时机(买卖一次)

力扣 122.买卖股票的最佳时机Ⅱ(多次买卖)

力扣 123.买卖股票的最佳时机Ⅲ

力扣 188.买卖股票的最佳时机

力扣 309.买卖股票最佳时机(含冷冻期)

力扣 714.买卖股票的最佳时机(含手续费)

子序列问题

子序列(不连续)

力扣 300.最长上升子序列

力扣 1143.最长公共子序列

子序列(连续) 

力扣 674.最长连续递增序列

力扣 718.最长重复数组

力扣 53.最大子序和

编辑距离

力扣 392.判断子序列

力扣 115.不同的子序列

力扣 72.编辑距离

 回文

力扣 647.回文子串

力扣 516.最长回文子序列


动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。
在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。

动态规划具备了以下三个特点

  1.  把原来的问题分解成了几个相似的子问题。
  2.  所有的子问题都只需要解决一次。
  3.  储存子问题的解。

动规五部曲分别为:

  1. 确定dp数组(dp table)以及下标的含义
  2. 确定递推公式
  3. dp数组如何初始化
  4. 确定遍历顺序

 动态规划基础

力扣 509.斐波那契数

  1. dp[i]代表第i个数字斐波那契的值是dp[i] 
  2. dp[i]=dp[i-1]+dp[i-2]
  3. 第一个数字是0,第二个数字是1,dp[0]=0,dp[1]=1
  4. 从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的

(注意初始化数组时,初始化了dp[0]和dp[1],所有定义数组长度时要+2(int[] dp=new int[n+2]),以免下标越界)

public class DpTest
{
    public int fib(int n)
    {
        int[] dp=new int[n+2];
        dp[0]=0;
        dp[1]=1;
        for (int i=2;i<=n;i++)
        {
            dp[i]=dp[i-1]+dp[i-2];
        }
        return dp[n];
    }
}

力扣 70.爬楼梯

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

力扣 746.使用最小花费爬楼梯

  1. dp[i]到达第i个台阶的最低花费dp[i]
  2. 有两个方式得到dp[i] ,一种是dp[i-1]和dp[i-2],选择这两个中最小的Math.min(dp[i-1],dp[i-2]),向上爬要支付费用,所以dp[i]=Math.min(dp[i-1),dp[i-2])+cost[i]注意这里为什么是加cost[i],而不是cost[i-1],cost[i-2]之类的,因为题目中说了:每当你爬上一个阶梯你都要花费对应的体力值.
  3. 不需要全都初始化,根据递推公式只需要初始化dp[0]和dp[1]即可, dp[0]=cost[0],dp[1]=cost[1]
  4.  dp[i]又dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了.
public class DpTest
{
    public int minCostClimbingStairs(int[] cost)
    {
        int[] dp=new int[cost.length];
        dp[0]=cost[0];
        dp[1]=cost[1];
        for (int i = 2; i <cost.length ; i++) {
            dp[i]=Math.min(dp[i-1],dp[i-2])+cost[i];
        }
        return Math.min(dp[cost.length-1],dp[cost.length-2] );
    }
}

力扣 62.不同路径

1.dp[i][j]:从(0,0)出发到(i,j)有dp[i][j]条不同路径

2.dp[i][j]从两个方向来推导出来, 即dp[i - 1][j] 和 dp[i][j - 1], dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。即dp[i][j]=dp[i-1][j]+dp[i][j-1].

3.如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。

for (int i = 0; i < m; i++) dp[i][0] = 1;
for (int j = 0; j < n;  j++) dp[0][j] = 1;

4.递归公式dp[i][j] =  dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。

public class DpTest
{
    public static int uniquePaths(int m, int n)
    {
        int[][] dp=new int[m][n];
        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];
    }
}

力扣 63.不同路径Ⅱ

此题和上题差不多,只是有个障碍物,遇到障碍物跳过即可,并且在初始化中,遇到障碍物了就终止。

public class DpTest
{
    public int uniquePathsWithObstacles(int[][] obstacleGrid)
    {
        int[][] dp=new int[obstacleGrid.length][obstacleGrid[0].length];
        for (int i=0;i<obstacleGrid.length&&obstacleGrid[i][0]!=1;i++)
        {
            dp[i][0]=1;
        }
        for (int j=0;j<obstacleGrid[0].length&&obstacleGrid[0][j]!=1;j++)
        {
            dp[0][j]=1;
        }
        for (int i=1;i<obstacleGrid.length;i++)
        {
            for (int j=1;j<obstacleGrid[0].length;j++)
            {
                if (obstacleGrid[i][j]==1)
                {
                    continue;
                }
                dp[i][j]=dp[i-1][j]+dp[i][j-1];
            }
        }
        return dp[obstacleGrid.length-1][obstacleGrid[0].length];
    }
}

力扣 343.整数拆分

  1. dp[i] :拆分数字i可以得到的最大乘积为dp[i]
  2. dp[i]有两种方法可以得到 从1到遍历到 j ,一种是直接相乘  i*(i-j) , 另一种是j*dp[i-j] (拆分i-j)j是从1开始遍历,拆分j的情况,在遍历j的过程中都计算过了。dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
  3. p[0] dp[1] 就不应该初始化,也就是没有意义的数值。拆分0和拆分1的最大乘积是多少?这是无解的。所有直接从dp[2]开始初始化。dp[2]=1;
  4. dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。
public class DpTest
{
    public int integerBreak(int n)
    {
        int[] dp=new int[n+1];
        dp[2]=1;
        for (int i=3;i<=n;i++)
        {
            for (int j=1;j<i;j++)
            {
                dp[i]=Math.max(dp[i], Math.max(j*(i-j),dp[i-j]*j));
            }
        }
        return dp[n];
    }
}

力扣 96.不同的二叉搜索树

  1.  dp[i]: 第i个节点有dp[i]种互不相同的二叉搜索树
  2.  dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] ,j相当于是头结点的元素,从1遍历到i为止。
  3. dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量
  4. 空树也能算一棵二叉搜索树,dp[0]=1;从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。所以初始化dp[0] = 1
public class DpTest
{
    public int numTrees(int n)
    {
        int[] dp=new int[n+2];
        dp[0]=1;
        for (int i=1;i<=n;i++)
        {
            for (int j=1;j<=i;j++)
            {
                dp[i]+=dp[j-1]*dp[i-j];
            }
        }
        return dp[n];
    }
}

背包问题

01背包和完全背包

01背包:物品只能放到背包一次,不能重复放

二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。

对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖! 

一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。

(因为一个物品会重复加入多次)

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15

如果正序遍历

dp[1] = dp[1 - weight[0]] + value[0] = 15

dp[2] = dp[2 - weight[0]] + value[0] = 30

此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

倒叙就是先算dp[2]

dp[2] = dp[2 - weight[0]] + value[0] = 15  (dp数组已经都初始化为0)

dp[1] = dp[1 - weight[0]] + value[0] = 15

所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。 

完全背包:单个物品可以多次放到背包里(每种物品有无限件。)

纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。 

但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。

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

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

01背包

01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i])

力扣 416分割等和子集

  1. dp[i]: 背包总容量为i,可以凑成i的子集总和为dp[i]
  2. 01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);本题中:weight[i]和value[i]都是num[i],所以dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
  3. 从dp[j]的定义来看,首先dp[0]一定是0。如果如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。
  4. 01背包问题,先物品后背包。即用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒叙遍历。
public class DpTest
{
    public boolean canPartition(int[] nums)
    {
    //  背包内总和不会大于20000,所以定义一个20000大的数组。
        int[] dp=new int[20001];
        int sum=0;
        for (int i=0;i< nums.length;i++)
        {
            sum+=nums[i];
        }
        if (sum%2!=0)
        {
            return false;
        }
        int target=sum/2;
        for (int i=0;i< nums.length;i++)
        {
//     每一个元素一定是不可重复放入,所以从大到小遍历
            for (int j=target;j>=nums[i];j--)
            {
                dp[j] = Math.max(dp[j], dp[j-nums[i]] + nums[i]);
            }
        }
//      集合中的元素刚好可以凑成target
        if(dp[target]==target)
        {
            return true;
        }
        return false;
    }

}

力扣 1049最后一块石头重量Ⅱ

public class DpTest
{
    public int lastStoneWeightII(int[] stones)
    {
//         dp[i] 表示容量为i的背包,最多可以背dp[i]重的石头
        int[] dp=new int[15001];
        int sum=0;
        for (int i=0;i<stones.length;i++)
        {
            sum+=stones[i];
        }
        int target=sum/2;
        for (int i=0;i<stones.length;i++)
        {
            for (int j=target;j>=stones[i];j--)
            {
//              dp[j - stones[i]]为 容量为j - stones[i]的背包最大所背重量。
                dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
//        一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。
//        target=sum/2是向下取整,所有sum-dp[target]一定大于dp[target]
        return (sum-dp[target])-dp[target];
    }
}

力扣 494目标和

dp[i] :装满i这么大的背包,有dp[i]种方法

不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]中方法。那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]] 种方法。(总的方案数为不考虑num[i]的方案数+有num[i]的方案数,即dp[j-num[i]]种方法和num[i]共同凑成了dp[j])。需要把 这些方法累加起来就可以了,dp[i] += dp[j - nums[j]](求组合问题的公式都是类似于这种)

dp[0]要初始化,dp[0]是一切递推公式的起源,装满容量为0的背包,有1种方法,就是装0件物品。的。dp[0]=1;

nums放在外层,target放在里面,里面倒叙遍历。

public class DpTest {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int i = 0; i < nums.length; i++) {
            sum += nums[i];
        }
//        目标和不能总和
        if (sum < target) {
            return 0;
        }
//        sum+taget =取+的数字和,不可能为小数。
        if ((sum + target) % 2 == 1) {
            return 0;
        }
//        目标和加总和不能为负数
        if (sum + target < 0) {
            return 0;
        }
//        bagSize取+的数字和,问题等价于num[i]有多少种方法凑成bagSize
        int bagSize = (sum + target) / 2;
//        dp[i] 含义:填满容量为i的背包,有dp[i]种方法
//        填满j-num[i]的背包,有dp[j-num[i]]种方法
        int[] dp = new int[bagSize + 1];
        dp[0] = 1;
        for (int i = 0; i < nums.length; i++) {
            for (int j = bagSize; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[bagSize];

    }
}

这道题回溯也可以哦:

public class DpTest {
    public int findTargetSumWays(int[] nums, int target) {
    int count=0;
    public int findTargetSumWays(int[] nums, int target)
    {
        dfs(nums,target,0,0);
        return count;
    }

    private void dfs(int[] nums, int target, int index, int sum)
    {
       if (index==nums.length)
       {
           if (sum==target)
           {
               count++;
           }
       }
       else {
           dfs(nums, target, index + 1, sum + nums[index]);
           dfs(nums, target, index + 1, sum - nums[index]);
       }
    }
}

力扣 474.一和零

  1. dp[i][j]:有i个0和j个1的strs的最大子集个数是dp[i][j]
  2. dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。然后我们在遍历的过程中,取dp[i][j]的最大值。所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
  3. 01背包的dp数组初始化为0就可以。 
  4. 我们讲到了01背包为什么一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历
public class DpTest {
    public int findMaxForm(String[] strs, int m, int n)
    {
        int[][] dp=new int[m+1][n+1];
        for (String s:strs)
        {
            int zeroNum=0,oneNum=0;
            for (char c:s.toCharArray())
            {
                if (c=='1')
                {
                    oneNum++;
                }
                else
                {
                    zeroNum++;
                }
            }
            for (int i=m;i>=zeroNum;i--)
            {
                for (int j=n;j>=oneNum;j--)
                {
                    dp[i][j]=Math.max(dp[i][j],dp[i-zeroNum][j-oneNum]+1);
                }
            }
        }
        return dp[m][n];
    }
}

完全背包

力扣 518.零钱兑换

  1. dp[j]:凑成总金额为j的货币组合数为dp[j]
  2. dp[j] 所有的dp[j-coins[i]](不考虑coins[i])相加,即dp[j] += dp[j - coins[i]]
  3. 凑成金额为0个的货币组合数为1,dp[0]=1
  4. 完全背包中组合问题,先遍历物品后遍历背包,正序遍历
public class DpTest {
  public int change(int amount, int[] coins)
    {
        int[] dp=new int[amount+1];
//        凑成金额为0有一种方法
//        下标非0的dp[j]初始化为0,dp[j-coin[i]]累加不影响dp[j]
        dp[0]=1;
        for (int i = 0; i <coins.length ; i++)
        {
            for (int j=coins[i];j<=amount;j++)
            {
                dp[j]+=dp[j-coins[i]];
            }
        }
        return dp[amount];
    }
}

力扣 377.组合总和Ⅳ

dp[i] :凑成总数为i的元素组合个数为dp[i]

求组合公式:dp[i]=dp[i-num[j]

dp[0]=1

此题不同顺序视为不同组合,所有外层遍历背包,内层遍历物品

public class DpTest {
    public int combinationSum4(int[] nums, int target)
    {
        int[] dp=new int[target+1];
//        dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。
        dp[0]=1;
        for (int i=0;i<=target;i++)
        {
            for (int j=0;j<nums.length;j++)
            {
                if (i>=nums[i])
                {
                    dp[i]+=dp[i-nums[j]];
                }
            }
        }
        return dp[target];
    }
}

力扣 70.爬楼梯(再爬一次)

  1. dp[i] :爬到第i阶台阶有dp[i]种方法
  2. 求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]]; dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j]。递推公式为:dp[i] += dp[i - j]
  3. 递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1
  4. 这是背包里求排列问题,即:1、2 步  和 2、1 步都是上三个台阶,但是这两种方法。外层遍历背包,内层遍历物品
public class DpTest {
    public int climbStairs(int n)
    {
        int[] dp=new int[n+1];
        dp[0]=1;
        for (int i=1;i<=n;i++)
        {
//           m表示最多可以爬m个台阶,代码中把m改成2就是本题70.爬楼梯
//            for (int j=1;j<=m;j++)
            for (int j=1;j<=2;j++)
            {
                if (i>=j)
                {
                    dp[i]+=dp[i-j];
                }
            }
        }
        return dp[n];
    }
}

力扣 322.零钱兑换

1.dp[i] :凑成总金额i的最少硬币个数dp[i]

2.dp[j]只有一个来源:dp[j-coins[i]], 即凑成总金额为j-coins[i]的最少硬币数为dp[j-coins[i],那么只要再加上这一个coins[i]就可以凑成dp[j].

  1. 当j<coins[i]时装不下,就只能继承dp[j]的值
  2. 当j>=coins[i]时装的下,可以选择不装或者装

即dp[j]=Math.min(dp[j],dp[j-coins[i]])

3.首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0;递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。所以下标非0的元素都是应该是最大值。

4.求钱币最小个数,那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。所以本题并不强调集合是组合还是排列。

public class DpTest {
    public int coinChange(int[] coins, int amount)
    {
        int[] dp=new int[amount+1];
        Arrays.sort(coins);
        Arrays.fill(dp,Integer.MAX_VALUE);
        dp[0]=0;
        for (int i=0;i<coins.length;i++)
        {
            for (int j=coins[i];j<=amount;j++)
            {
//               前面dp值在有计算过的基础上才能转移,如果不判断这一步,dp[j-coins[i]]=Integer.MAX_VALUE+1,那么最小的永远是dp[j]
                if (dp[j-coins[i]]!=Integer.MAX_VALUE)
                {
                    dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
                }
            }
        }
        return dp[amount]==Integer.MAX_VALUE?-1:dp[amount];
    }
}

力扣 279.完全平方数

  1. dp[j]:和为j的完全平方数的最少数量dp[i]
  2. dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。此时我们要选择最小的dp[j],所以递推公式:dp[j] = min(dp[j - i * i] + 1, dp[j]);
  3. dp[0]表示 和为0的完全平方数的最小数量,那么dp[0]一定是0。从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要选最小的,所以非0下标的dp[i]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖。 
  4. 如果求组合数就是外层for循环遍历物品,内层for遍历背包。如果求排列数就是外层for遍历背包,内层for循环遍历物品。本题求最小数,所以外层for遍历背包,里层for遍历物品,还是外层for遍历物品,内层for遍历背包,都是可以的。
public class DpTest {
    public int numSquares(int n)
    {
        int[] dp=new int[n+1];
        dp[0]=0;
        Arrays.fill(dp,Integer.MAX_VALUE);
        for (int i=1;i<=n;i++)
        {
            for (int j=1;j*j<=n;j++)
            {
                dp[j]=Math.min(dp[j],dp[j-i*i]+1);
            }
        }
        return dp[n];
    }
}

力扣 139.单词拆分

  1. dp[i] : 字符串长度为i,dp[i]=true,表示可以拆分为一个或者多个在字典中出现的单词
  2. 如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true(j < i )。所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。
  3. dp[0]表示如果字符串为空的话,说明出现在字典里。dp[0]=true;
  4. 本题使用求排列的方式,还是求组合的方式都可以。 
public class DpTest {
    public boolean wordBreak(String s, List<String> wordDict)
    {
        Set<String> set=new HashSet<>(wordDict);
        boolean[] dp=new boolean[s.length()+1];
        dp[0]=true;
        for (int i=1;i<=s.length();i++)
        {
            for (int j=0;j<i;j++)
            {
                if (set.contains(s.substring(j,i))&&dp[j])
                {
                    dp[i]=true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

 打家劫舍

力扣 198.打家劫舍

  1. dp[i] :考虑到dp[i]以内的房屋最多可以偷窃的金额是dp[i]
  2. dp[i]可以从两个方面推出,如果i房间不偷,那金额就和前一个一样,dp[i-1]; 另外就是这个房子偷,这个房子偷的话,那偷窃 i-2 房间的金额要加上这个房间的金额nums[i],即递推公式:dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]); 
  3. 第一个房间的偷窃金额就是nums[0],第二个房间以内做多偷窃的金额就是Math.max(nums[0],nums[1])
  4. dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,应该从前向后推
public class DpTest {
    public int rob(int[] nums)
    {
        if (nums.length==1)
        {
            return nums[0];
        }
        int[] dp=new int[nums.length+2];
        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-1],dp[i-2]+nums[i]);
        }
        return dp[nums.length];
    }
}

力扣 213.打家劫舍Ⅱ

和上一个题不一样的是,这个题成环了,考虑首元素就不能考虑尾部元素,考虑尾部元素就不能考虑首部元素。

public class DpTest {
    public int rob(int[] nums)
    {
     if (nums.length==1)
     {
         return nums[0];
     }
     int ans=rob(nums,0,nums.length-2);
     int ans2=rob(nums,1,nums.length-1);
     return Math.max(ans,ans2);
    }

    private int rob(int[] nums, int start, int end)
    {
//        只有两个元素
        if (start==end)
        {
            return nums[start];
        }
        int[] dp=new int[nums.length];
        dp[start]=nums[start];
        dp[start+1]=Math.max(nums[start],nums[start+1]);
        for (int i=start+2;i<nums.length;i++)
        {
            dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
        }
        return dp[end];
    }
}

力扣 377.打家劫舍Ⅲ

 递归(超时)

   public class DpTest
{
    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));
    }
}

记忆化递归

public class DpTest
{
    Map<TreeNode,Integer> map=new HashMap<>();
    public int rob(TreeNode root)
    {
        if (root==null)
        {
            return 0;
        }
        if (root.right==null&&root.left==null)
        {
            return root.val;
        }
//        可以使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。
        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 money2=rob(root.left)+rob(root.right);
        map.put(root,Math.max(money,money2));
        return Math.max(money,money2);
    }
}

动态规划

public class DpTest
{
    public int rob(TreeNode root)
    {
        int[] dp=rootTree(root);
        return Math.max(dp[0],dp[1]);
    }
    private int[] rootTree(TreeNode root)
    {
        if (root==null)
        {
            return new int[2];
        }
        int[] left=rootTree(root.left);
        int[] right=rootTree(root.right);
//      下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。
        int[] dp=new int[2];
//        0 代表不偷,1 代表偷
//       当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
        dp[1]=root.val+left[0]+right[0];
//       当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的钱 + 右孩子能偷到的钱
        dp[0]=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
        return dp;
    }
}

股票系列

力扣 121.买卖股票的最佳时机(买卖一次)

1.dp[i][0]: 表示第i天所持有股票所得的现金,一开始现金是0,那么加入第i天买入股票现金就是 -prices[i], 这是一个负数。 dp[i][1] 表示第i天不持有股票所得现金。

2.如果第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], dp[i - 1][0]+prices[i]);

 3.根据递推公式,需要得知dp[0][0]和dp[0][1]的值,dp[0][0]代表第0天持有股票,则dp[0][0]=-prices[0], dp[0][1]代表第零天不持有股票,dp[0][1]=0;

4.dp[i]都是由dp[i-1]推导出来的,是从前向后遍历

public class DpTest
{    
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];
    }
}
public class DpTest
{
public int maxProfit(int[] prices)
{
    int[] dp=new int[2];
    dp[0]=-prices[0];
    dp[1]=0;
    for (int i=1;i<prices.length;i++)
    {
        dp[0]=Math.max(dp[0],-prices[i] );
        dp[1]=Math.max(dp[1],dp[0]+prices[i]);
    }
    return dp[1];
}
}

贪心 

public class DpTest
{
    public int maxProfit(int[] prices)
    {
        int low=Integer.MAX_VALUE;
        int ans=0;
        for (int i=0;i<prices.length;i++)
        {
           low=Math.min(low,prices[i]);
           ans=Math.max(ans,prices[i]-low);
        }
        return low==Integer.MAX_VALUE?0:ans;
    }
}

力扣 122.买卖股票的最佳时机Ⅱ(多次买卖)

和上一个题相比不同的是这个题可以多次买卖,那么在买的时候就要考虑利润的叠加了,第i天持有股票即dp[i][0],如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金 减去 今天的股票价格 即:dp[i - 1][1] - prices[i]。

public class DpTest
{
    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];
    }
}
public class DpTest
{
    public int maxProfit(int[] prices)
    {
        int[] dp=new int[2];
        dp[0]=-prices[0];
        dp[1]=0;
        for (int i=1;i<prices.length;i++)
        {
            dp[0]=Math.max(dp[0],dp[1]-prices[i]);
            dp[1]=Math.max(dp[1],dp[0]+prices[i]);
        }
        return dp[1];
    }
}

力扣 123.买卖股票的最佳时机Ⅲ

一天一共就有五个状态,

     0. 没有操作

  1. 第一次买入

  2. 第一次卖出

  3. 第二次买入

  4. 第二次卖出

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

dp[i][1] 表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,可以是沿用前一天买入的状态或者当天买入 

i = 0天时五种状态 :dp[0][0] 第0天没有操作,dp[0][0]=0;  dp[0][1] 第0天买入股票,dp[0][1]=-prices[0],dp[0][2]为在同一天买入并且卖出,dp[0][2]=0;

p[0][3] 同一天买入并且卖出后再以 -prices[0] 的价格买入股票,dp[0][3]=-prices[0];同理dp[0][4]=0;

public class DpTest
{
    public int maxProfit(int[] prices)
    {
        int[][] dp=new int[prices.length][5];
        dp[0][1]=-prices[0];
        dp[0][3]=-prices[0];
        for (int i=1;i<prices.length;i++)
        {
            dp[i][0]=dp[i-1][0];
            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];
    }
}
public class DpTest
{
    public int maxProfit(int[] prices)
    {
        int[] dp=new int[5];
        dp[1]=-prices[0];
        dp[3]=-prices[0];
        for (int i=1;i<prices.length;i++)
        {
            dp[1]=Math.max(dp[1],-prices[i]);
            dp[2]=Math.max(dp[2],dp[1]+prices[i]);
            dp[3]=Math.max(dp[3],dp[2]-prices[i]);
            dp[4]=Math.max(dp[4],dp[3]+prices[i]);
        }
        return dp[4];
    }
}

力扣 188.买卖股票的最佳时机

 和上一题相比,区别就是本题可以买卖多次,通过规律我们可以发现,奇数为持有股票,偶数为不持有股票

public class DpTest
{
    public int maxProfit(int k, int[] prices)
    {
        if (prices.length==0)
        {
            return 0;
        }
        int[][] dp=new int[prices.length][2*k+1];
//        初始化数组,第0天持有股票的初始值都为-prices[0]
        for (int i=1;i<2*k;i+=2)
        {
            dp[0][i]=-prices[0];
        }
        for (int i=1;i<prices.length;i++)
        {
            for (int j=0;j<2*k-1;j+=2)
            {
                dp[i][j+1]=Math.max(dp[i-1][j+1],dp[i-1][j]-prices[i]);
                dp[i][j+2]=Math.max(dp[i-1][j+2],dp[i-1][j+1]+prices[i]);
            }
        }
        return dp[prices.length-1][2*k];
    }
}

力扣 309.买卖股票最佳时机(含冷冻期)

dp[i][j]: 第i天状态为j所剩的最多现金为dp[i][j]

出现冷冻期后就有了四种状态:

  • 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作)

  • 卖出股票状态,这里就有两种卖出股票状态

    • 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态

    • 状态三:今天卖出了股票

  • 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天!

状态一: 达到买入股票状态,前一天就是买入股票的状态或者今天买入,今天买入有两种情况:

  • 前一天是冷冻期
  • 前一天是保持卖出股票的状态

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][0]+prices[i])

状态三:今天卖出了股票

dp[i][2]=dp[i-1][0]+prices[i]

状态四: 今天是冷冻期,昨天刚卖了股票

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

public class DpTest
{
    public int maxProfit(int[] prices)
    {
        int[][] dp=new int[prices.length][4];
        dp[0][0]=-prices[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]));
    }
}

力扣 714.买卖股票的最佳时机(含手续费)

 可以多次交易就是每笔交易需要扣除手续费

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

子序列问题

子序列(不连续)

力扣 300.最长上升子序列

  1. dp[i]: i 之前包括 i 的最长上升子序列
  2. if(num[i]>num[j])dp[i]=Math.max(dp[i],dp[j]+1)
  3. 每一个数字都是自己的一个上升子序列,每一个起始值都是1
  4. 从前向后遍历,遍历i的循环里外层,遍历j则在内层,
public class DpTest
{
    public int lengthOfLIS(int[] nums)
    {
        if (nums.length==0)
        {
            return 0;
        }
        int[] dp=new int[nums.length];
        Arrays.fill(dp,1);
        int max=0;
        for (int i=1;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);
                }
            }
            if (dp[i]>max)
            {
                max=dp[i];
            }
        }
        return max;
    }
}

力扣 1143.最长公共子序列

  • dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j]。之所以dp[i][j]的定义不是text1[0,i]和text2[0,j]  是为了方便当i=0或者j=0时,dp[i][j]表示为空字符串和另外一个字符串的匹配
  • 当text1[i-1]=text2[j-1]时 最后一位相等,所以公共最长子序列又增加1 所以dp[i][j]=dp[i-1][j-1]+1
  • 如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
public class DpTest
{
    public int longestCommonSubsequence(String text1, String text2)
    {
        int[][] dp=new int[text1.length()+1][text2.length()+1];
        for (int i=1;i<=text1.length();i++)
        {
            char c1=text1.charAt(i-1);
            for (int j=1;j<=text2.length();j++)
            {
                char c2=text2.charAt(j-1);
                if (c1==c2)
                {
                    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()];
    }
}

子序列(连续) 

力扣 674.最长连续递增序列

public class DpTest
{
    public int findLengthOfLCIS(int[] nums)
    {
        if (nums.length<=1)
        {
            return nums.length;
        }
        int[] dp=new int[nums.length];
        Arrays.fill(dp,1);
        int max=0;
        for (int i=1;i<nums.length;i++)
        {
            if (nums[i]>nums[i-1])
            {
                dp[i]=dp[i-1]+1;
            }
            if (dp[i]>max)
            {
                max=dp[i];
            }
        }
        return max;
    }
}

力扣 718.最长重复数组

  • dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。
  • 根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1;
public class DpTest
{
    public int findLength(int[] nums1, int[] nums2)
    {
        int[][] dp=new int[nums1.length+1][nums2.length+1];
        int max=0;
        for (int i=1;i<=nums1.length;i++)
        {
            for (int j=1;j<=nums2.length;j++)
            {
                if (nums1[i-1]==nums2[j-1])
                {
                    dp[i][j]=dp[i-1][j-1]+1;
                }
                if (dp[i][j]>max)
                {
                    max=dp[i][j];
                }
            }
        }
        return max;
    }
}

力扣 53.最大子序和

 dp[i] : 包括下标i之前的最大连续子序列和为dp[i]。那么我们要找最大的连续子序列,就应该找每一个i为终点的连续最大子序列。所以在递推公式的时候,可以直接选出最大的dp[i]。

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

方法二: 

public class DpTest
{
    public int maxSubArray(int[] nums)
    {
       int max=nums[0];
       int sum=0;
       for (int i:nums)
       {
           sum=Math.max(i,sum+i);
           max=Math.max(sum,max);
       }
       return max;
    }
}

编辑距离

力扣 392.判断子序列

public class DpTest
{
    public boolean isSubsequence(String s, String t) {
        int[][] dp = new int[s.length()+1][t.length()+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] + 1;
                } else {
                    dp[i][j] = dp[i][j - 1];
                }
            }
        }
        if (dp[s.length()][t.length()] == s.length()) {
            return true;
        } else {
            return false;
        }
    }
}
public boolean isSubsequence(String s, String t)
{
    int i=0;
    int j=0;
    while (j<t.length()&&i<s.length())
    {
        if (s.charAt(i)==t.charAt(j))
        {
            i++;
        }
        j++;
    }
    return i==s.length();
}

力扣 115.不同的子序列

public class Num115
{
    public int numDistinct(String s, String t)
    {
//        以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。
        int[][] dp=new int[s.length()+1][t.length()+1];
        for (int i=0;i<=s.length();i++)
        {
            dp[i][0]=1;
        }
        for (int j=1;j<=t.length();j++)
        {
//            空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。
//            那么dp[0][j]一定都是0,s如论如何也变成不了t。
            dp[0][j]=0;
        }
//        dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。
        dp[0][0]=1;
        for (int i=1;i<=s.length();i++)
        {
            for (int j = 1; j <=t.length() ; j++)
            {
                if (j>i)
                {
                    continue;
                }
                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()];
    }
}

力扣 72.编辑距离

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

if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1];(word1[i - 1] 与 word2[j - 1]相等了,那么就不用编辑了,以下标i-2为结尾的字符串word1和以下标j-2为结尾的字符串word2的最近编辑距离dp[i - 1][j - 1] 就是 dp[i][j]了。)

 if (word1[i - 1] != word2[j - 1])

  • 操作一:word1增加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 加上一个增加元素的操作。即 dp[i][j] = dp[i - 1][j] + 1;
  • 操作二:word2添加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个增加元素的操作。即 dp[i][j] = dp[i][j - 1] + 1;

这里有同学发现了,怎么都是添加元素,删除元素去哪了。word2添加一个元素,相当于word1删除一个元素,例如 word1 = "ad" ,word2 = "a",word2添加一个元素d,也就是相当于word1删除一个元素d,操作数是一样!

  • 操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增加元素,那么以下标i-2为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个替换元素的操作。即 dp[i][j] = dp[i - 1][j - 1] + 1;

综上,当 if (word1[i - 1] != word2[j - 1]) 时取最小的,

即:dp[i][j] = Math.min((dp[i - 1][j - 1], Math.min(dp[i - 1][j], dp[i][j - 1])) + 1;

public class DpTest
{
    public int minDistance(String word1, String word2)
    {
        int[][] dp=new int[word1.length()+1][word2.length()+1];
        for (int i=1;i<=word1.length();i++)
        {
            dp[i][0]=i;
        }
        for (int i=1;i<=word2.length();i++)
        {
            dp[0][i]=i;
        }
        for (int i=1;i<=word1.length();i++)
        {
            for (int j=1;j<=word2.length();j++)
            {
                if (word1.charAt(i-1)==word2.charAt(j-1))
                {
                    dp[i][j]=dp[i-1][j-1];
                }
                else
                {
                    dp[i][j]=Math.min(dp[i-1][j],Math.min(dp[i][j-1],dp[i-1][j-1]))+1;
                }
            }
        }
        return dp[word1.length()][word2.length()];
    }

}

 回文

力扣 647.回文子串

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

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。

public class DpTest
{
    public int countSubstrings(String s)
    {
        boolean[][] dp=new boolean[s.length()][s.length()];
        int count=0;
        for (int i=s.length()-1;i>=0;i++)
        {
            for (int j=i;j<s.length();j++)
            {
                if (s.charAt(i)==s.charAt(j))
                {
                    if (j-i<=1)
                    {
                        count++;
                        dp[i][j]=true;
                    }
                    else if (dp[i-1][j+1])
                    {
                        count++;
                        dp[i][j]=true;
                    }
                }
            }
        }
        return count;
    }
}

力扣 516.最长回文子序列

public class DpTest
{
    public int longestPalindromeSubseq(String s)
    {
//        dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]。
        int[][] dp=new int[s.length()][s.length()];
//        dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。
        for (int i=0;i<s.length();i++)
        {
            dp[i][i]=1;
        }
//        那么分别加入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]);
        for (int i=s.length()-1;i>=0;i--)
        {
            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]);
                }
            }
        }
        return dp[0][s.length()-1];
    }
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值