0-1背包问题:

分割等和子集

给定背包容量,能不能装满背包,装满了返回true,装不满返回false

最后一块石头的重量II

给定背包容量,尽可能装,求背包中能装的最大重量

目标和

给定背包容量,装满背包有多少种方法

这道题的关键点就是将nums数组分成left和right两个部分分别代表加+号的、加-号的,计算出left子集的背包数量是(sum + target)/2是重中之重。

问题转化为:背包容量为left时,装满背包有多少种方法。

初始化dp[0]=1

递推公式dp[j] += dp[j – num[i]];

遍历顺序:先遍历物品,再遍历背包容量,01背包问题内层遍历要倒序遍历。

一和零

01背包的两维问题。给定背包容量,装满这个背包最多有多少个物品

dp数组的创建:dp[i][j]表示装i个0,j个1的背包,最多能背dp[i][j]个物品。

两个维度来形容物品的重量。

常规的01背包问题递推公式为

dp[j] = Math.max(dp[j], dp[j – weights[i]] + values[i]);

这道题的递推公式为

dp[i][j] = Math.max(dp[i][j], dp[i – x][j – y] + 1);

其中x和y分别表示给的字符串数组中每个元素中的0的个数和1的个数。通过x和y两个维度来形如weight[i]

初始化:dp[0][0]=0,其他的值默认初始化为0

01背包遍历数组必须先遍历物品再遍历背包,同时背包要倒序遍历,为的就是物品只能使用一次。

在这道题中,遍历的物品就是字符串数组中每个字符串,每个字符串有x个0和y个1

背包容量有两层,一个0的个数,一个是1的个数。

综上:本题中物品就是strs里的字符串,背包容量就是描述的m和n

动归五部曲

1、dp[j]表示背包容量为j的时候,背包背的最大价值为dp[j]

2.确定递推公式

3.初始化dp[0]

4.遍历顺序

5.打印dp数组

完全背包与01背包最大的区别就是,一个物品能不能无限取,具体体现在代码上就是内层循环需不需要倒序遍历。

01背包需要倒序遍历,完全背包不需要倒序遍历。

完全背包问题:

与01背包的不同是每种物品都有无限件。

完全背包的递推公式一维数组写法

dp[j] = Math.max(dp[j], d[j – weight[i]] + value[i]);与01背包一样

遍历顺序:两个for循环的先后顺序,都不影响计算dp[j]所需要的值

完全背包代码模板:

int[] dp = new int[bagWeight + 1];

//初始化dp数组都为默认0

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]);

}

}

return dp[bagWeight];

题目稍稍有点变化,就会体现在遍历顺序上,所以完全背包题型主要改变在遍历顺序上。

以下两个代码块是解决完全背包问题下的:给定背包容量,装满这个背包的装法数量(分为求组合数和求排列数)主要区别是遍历顺序的不同,求组合数先遍历物品再遍历背包容量,求排列数先遍历背包容量再遍历物品。

//先遍历物品再遍历背包 用来解决求组合数

int[] dp = new int[bagWeight +1];

//初始化,背包容量为0初始化为1,其它初始化为0

dp[0]=1;

for(int i = 0; i < weight.length; i++){//先遍历物品

for(int j = weight[i]; j <= bagWeight; j++){//再遍历背包容量

  dp[j] += dp[j – weight[i]];

}

}

return dp[bagWeight];

//先遍历背包再遍历物品 用来求排列数

int[] dp = new int[bagWeight +1];

//初始化,背包容量为0初始化为1,其它初始化为0

dp[0]=1;

for(int j = 0; j <= bagWeight; j++){//先遍历背包容量

for(int i = 0; i < weight.length; i++){//再遍历物品

  if(j >= weight[i]){ dp[j] += dp[j – weight[i]];

  }

}

}

return dp[bagWeight];

零钱兑换II

完全背包问题+装满背包求组合数装法

给定背包容量,问有多少种装法可以装满背包(要区分装法是求组合数还是排列数)

这里要记住对于装满背包装法初始化时要将dp[0] = 1即背包容量为0时的方法是1

求有多少种装法的动归问题有三个主要的点:

1.初始化时dp[0] = 1,表示背包容量为0的时候有1种装法

2.递推公式dp[j] += dp[j – weight[i]];

3.遍历顺序,对于完全背包问题内存循环不需要倒序遍历,而01背包问题需要倒序遍历

组合总和IV

完全背包问题+装备背包求排列数装法

给定背包容量,问有多少种装法可以装满背包

零钱兑换

完全背包问题+装满背包最少用多少件物品

动归五部曲

1.dp[j]表示背包容量为j(题目中总金额amount)时,装满这个背包所需要的最少物品数为dp[j](题目中coins硬币的数量,注意这里是硬币数量,不是硬币价值)。

2.递推公式

dp[j] = Math.min(dp[j], dp[j – coins[i]] + 1);

3.初始化

dp[0] = 0; 非0下标初始化,本题取最小值,所以初始为最大值dp[j] = Integer.MAX_VALUE;

4.遍历顺序

因为本题求的是最少装多少件物品,不管是组合装还是排列装,该是几个就是几个,所以不需要考虑物品和背包容量的遍历先后问题。

本题还有一个特殊情况,如果有凑不成的情况出现,必须判断不装coins[i]时,容量为dp[j – coins[i]]的背包装满需要的最少硬币数是不是默认给的Integer.MAX_VALUE,如果是说明不合规,不能用于min赋值;如果不是,那么才可以进行min赋值。

还是标注一下,如果说赋值的时候有MAX_VALUE,那么在MIN求解的时候要判断有没有凑不成的情况。

for(int i = 0; i < weight.length; i++){//先遍历物品 for(int j = weight[i]; j <= bagWeight; j++){//再遍历背包容量

  if(dp[j – weight[i]] != Integer.MAX_VALUE){

    dp[j] = Math.min(dp[j], dp[j – weight[i]] + 1);

  }

}

}

return dp[bagWeight] == Integer.MAX_VALUE ? -1 : dp[bagWeight];

最后判断的dp[amount]是不是Integer.MAX_VALUE,下述实例2的情况,要返回-1。

示例 2:

  • 输入:coins = [2], amount = 3
  • 输出:-1

5.打印

完全平方数

换汤不换药,给定背包容量,装满这个背包所需要的最少物品数量。(只不过不需要判断“凑不出”的情况,因为最坏也是可以通过1来装出结果)

单词拆分

完全背包+给定背包容量,判断能不能装满这个背包,能装满返回true,不能装满返回false

本题重点是要确定背包容量是什么,用字符串s的长度i作为背包容量。

1.dp[i]表示字符串长度为i时,能不能装满这个背包,能装满dp[i]就为true,不能装满dp[i]就为false。

所以dp[i]数组是布尔类型的。

2.递推公式

if([j, i] 这段字符串在字典集合wordDict中存在 && dp[j] == true){

dp[i] = true;

}else{

dp[i] =false;

}

3.初始化

dp[0] = true;因为在递推公式中要用到dp[0]才能向后推导,所以必须设为true

其它的都设置为false。

4.遍历顺序

因为要求的s是对排列的字符串有顺序的,所以这道题是排列装,因此要先遍历背包容量,再遍历物品。

1.dp[j]表示背包容量为j时,是否能装满这个背包

对应这道题,背包容量用字符串长度来形容,所以dp[j]的含义表示为字符串长度为j时,是否能装满这个背包,如果能装满dp[j]为true;如果不能装满dp[j]为false。

2.递推公式

找到dp[j]的前一个状态是在字符串位置j之前的i位置,所以抽象来看是dp[j] = [i, j] + dp[i]。所以递推公式如下:

if([i, j] 范围的字符串属于wordDict集合 && dp[i] == true){

dp[j] = true;

}

3.初始化

dp[0] = true;因为递推公式中dp[j]需要依赖dp[i]的状态,且i < j,所以dp[0]必须为true,才能推导下去

其它dp[]都默认初始化为false。

4.遍历顺序

因为装填的物品组合需要确定的顺序才能真正构成所需要的字符串s,所以是排列装。因此先遍历背包容量,再遍历物品

for(int j = 1; j <= s.length(); j++){//先遍历背包容量,这里j从1开始是因为背包容量非空(字符串s非空)

for(int i = 0; i < j; i++){//再遍历物品,这里这样写为什么是在遍历物品?因为i是小于j的,从i到j截取的这一段字符串是我们要判断的字符串,判断它在不在wordDict列表中

  if(wordDict.contains(s.substring(i, j)) && dp[i] == true){

     dp[j] = true;

  }

}

}

return dp[s.length()];

5.打印dp

打家劫舍问题:

198.打家劫舍

1.确定dp[j]的含义

dp[j]表示考虑下标j以内(包括j)的房屋,最多可以偷窃的金额为dp[j]。

int[] dp = new int[nums.length];

2.确定递推公式

dp[j]状态的得到是:偷第j个房间dp[j – 2] + nums[j]以及不偷第j个房间,转而考虑偷第j-1房间dp[j-1]

所以:dp[j] = Math.max(dp[j-2] + nums[j], dp[j-1]);

3.初始化

dp[0] = nums[0];

dp[1] = Math.max(nums[0], nums[1]);

4.确定遍历顺序

for(int j = 2; j < dp.length; j++){

dp[j] = Math.max(dp[j-2] + nums[j], dp[j-1]);

}

Return dp[nums.length – 1];

213.打家劫舍II

房间成环,考虑三种情况

方案一:首尾都不偷;方案二:只偷首元素,不偷尾元素;方案三:只偷尾元素,不偷首元素

方案二和方案三已经包含了方案一,所以总的来说只有两个方案。

两个方案处理,所以自定义一个函数robRange(int[] nums, int start, int end);

遵从左闭右开原则

337.打家劫舍III

树形dp问题

自定义robTree(TreeNode cur){}表示当前节点偷还是不偷所达到的最大金额,所以这个函数的返回值是dp数组。

这里dp数组定义为:大小为2,dp[0]表示不偷当前节点,所拿到的最大金额;d[1]表示偷当前节点,所拿到的最大金额。

遍历二叉树问题:本题使用后序遍历二叉树

递归三部曲:

1.确定返回值类型和传入参数

2.确定终止条件

3.确定递归逻辑

//确定返回值类型和传入参数

Public int[] robTree(TreeNode cur){

if(cur == null){

  //终止条件,当前节点为空节点,那么返回dp数组,值都初始化为0

  return new int[2];

}

//确定递归逻辑

//后序遍历

两个数组是后一步再考虑的:

int[] leftdp = robTree(cur.left);//左节点

int[] rightdp = robTree(cur.right);//右节点

这一步先考虑 ://当前节点

//偷当前节点所能拿到的最大金额为:

int val1 = cur.value + leftdp[0] + rightdp[0];//偷了当前节点,那么它的左右孩子节点就不能再偷了,所以要加上左右孩子各自的不偷的最大金leftdp[0]、rightdp[0]。

//不偷当前节点,那么就看左孩子节点要不要偷(看leftdp[0]和leftdp[1]谁更大来决定左孩子节点偷不偷),以及右孩子节点要不要偷(看rightdp[0]和rightdp[1]谁更大来决定右孩子节点偷不偷)

int val2 = Math.max(leftdp[0], leftdp[1]) + Math.max(rightdp[0], rightdp[1]);

return new int[]{val2, val1};

}

在主函数中调用robTree()

int[] result = robTree(root);

return Math.max(result[0], result[1]);

买卖股票问题:

121.买卖股票的最佳时机

只能买卖一次股票

1.dp[i][0]表示第i天持有股票的最大金额,dp[i][1]表示第i天不持股的最大金额。

这样定义可以不遗漏状态。dp[i][0]表示第i天持有股票的最大金额,不仅表示第i天把股票买下来,还可以表示前i天的某一天把股票买下来。dp[i][1]表示第i天不持股的最大金额,不仅表示第i天把股票卖出,还可以表示前i天的某一天把股票卖出。

用户初始的金额一定是0,那么第一次买入股票后,总金额一定是负数。

2.递推公式

dp[i][0]由两个状态推导出:一个是第i-1天持有股票的最大金额dp[i-1][0];一个是前i-1天都没有买入股票,是第i天才买入的股票,即第i天买入股票的最大金额-price[i]。

所以dp[i][0] = Math.max(dp[i-1][0], -price[i]);

dp[i][1]由两个状态推导出:一个是第i-1天不持股的最大金额dp[i-1][1];一个是前i-1天都持有股,但是在第i天才卖出,即第i天卖出股票的最大金额为dp[i-1][0]+price[i]。

所以dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + price[i]);

3.初始化

dp[0][0]=-price[0];

dp[0][1]=0;

4.dp[i][0]和dp[i][1]都依赖i-1天的状态,所以正常遍历即可

5.打印dp

122.买卖股票的最佳时机II

可以无限次买卖股票

1.dp[i][0]表示第i天持有股票的最大金额,dp[i][1]表示第i天不持股的最大金额

2.递推公式

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]);

3.初始化

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

dp[0][1] = 0;

4.遍历顺序

5.打印

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

至多两次买卖股票

//一天五个状态

//0.没有操作

//1.第一次持有股票

//2.第一次卖出股票

//3.第二次持有股票

//4.第二次卖出股票

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

//递推公式

//dp[i][1]表示第i天第一次持有股票的总金额

//dp[i][1]状态由以下两个状态得到

//1.i-1天都没有持有股票,在第i天买入了股票 dp[i-1][0] – prices[i]

//2.i-1天的某一天持有股票,在第i天什么都不操作 dp[i-1][1]

//所以dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] – prices[i]);

//dp[i][2]表示第i天第一次卖出股票的总金额

//dp[i][2]状态由以下两个状态得到

//1.i-1天内都持有股票,在第i天卖出股票 dp[i-1][1] + prices[i]

//2.i-1天内的某一天卖出了股票,第i天什么都不做 dp[i-1][2]

//所以dp[i][2] = Math.max(dp[i-1][2], dp[i-1][1] + prices[i]);

//dp[i][3]表示第i天第二次买入股票的总金额

//dp[i][3]状态由以下两个状态得到

//1.i-1天内都没有买入股票(并且进行过一次买卖了),在第i天第二次买入股票

//dp[i][3] = dp[i-1][2] – prices[i]

//2.i-1天内都持有股票,在第i天什么都不做

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

//所以dp[i][3] = Math.max(dp[i-1][3], dp[i-1][2] – prices[i]);

//dp[i][4]表示第i天第二次卖出股票的总金额

//dp[i][4]状态由以下两个状态得到

//1.i-1天内都持有股票(并且进行过一次买卖了),在第i天第二次卖出股票

//dp[i][4] = dp[i-1][3] + prices[i]

//2.i-1天内某一天卖出了股票,第i天什么都不做

//dp[i][4] = dp[i-1][4]

//所以dp[i][4] = Math.max(dp[i-1][4], dp[i-1][3] + prices[i])

//初始化

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];

188.买卖股票的最佳时机IV

至多完成k笔交易,即至多可以k次买卖

在买卖股票III中至多可以2次买卖股票,一天的状态有5 = 2 * 2 + 1种

所以在该题中一天的状态有2 * k + 1种,从0到2*k一共2*k+1种

dp[i][j]表示第i天处于状态j时,最大金额为dp[i][j]

递推公式

j为奇数状态dp[i][1] = Math.max(dp[i-1][0] – prices[i], dp[i-1][1]);

j为偶数状态dp[i][2] = Math.max(dp[i-1][1] + prices[i], dp[i-1][2]);

初始化

偶数状态都初始化为0,奇数状态都初始化为-prices[i]

for(int j = 0; j < 2 * k; j += 2){//偶数状态都初始化为0,奇数状态都初始化为-prices[i]

dp[0][j+1] = -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] – prices[i], dp[i-1][j+1]);

dp[i][j+2] = Math.max(dp[i-1][j+1] + prices[i], dp[i-1][j+2]);

}

}

37.买卖股票的最佳时机含冷冻期

本题在最佳买卖股票II中加入了冷冻期概念,都是无限交易次数

状态0:dp[i][0]表示第i天持有股的金额

//下面三个状态就是不持有股票的状态

状态1:dp[i][1]表示前i天中的某一天卖完股票后,一直到第i天为止保持什么都不做的状态

状态2:dp[i][2]表示第i天卖出股票的状态

状态3:dp[i][3]表示第i天处于冷冻期的状态

dp[i][0]的状态获取分析:

1.前i-1天保持卖完股票后什么都不做的状态,在第i天买入股票

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

2.第i-1天处于冷冻期,在第i天买入股票

dp[i][0] = dp[i-1][3] – prices[i];

3.前i-1天一直持有股票

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

dp[i][1]的状态获取分析:

1.前i-1天也是保持卖完股票后什么都不做的状态

d[i][1] = dp[i-1][1];

2.在第i-1天处于冷冻期

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

dp[i][2]的状态获取分析:

前i-1天内一直持有股票,在第i天卖出

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

dp[i][3]的状态获取分析:

第i-1天卖出股票

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

//初始化

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(Math.max(dp[i-1][1] – prices[i], dp[i-1][3] – prices[i]), dp[i-1][0]);

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(Math.max(dp[prices.length – 1][1], dp[prices.length – 1][2]), dp[prices.length – 1][3]);

714.买卖股票的最佳时机含手续费

本题在最佳买卖股票II中加入了手续费概念,都是无限交易次数

只要把卖出股票的时候减去手续费就行了,其它不变

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

//无限次买卖

//每卖出股票金额就减去2

//dp[i][0]表示持有股票 ,dp[i][1]表示不持有股票

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][1] – prices[i], dp[i-1][0]);

dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i] – fee);

}

return dp[prices.length – 1][1];

}

子序列问题:

300.最长递增子序列

1.dp[i]表示以nums[i]为结尾的最长递增子序列的长度为dp[i];

2.递推公式:

dp[i]的状态可以由在nums[i]之前的元素j为结尾的最长递增子序列的长度dp[j]推出

0 < j < i,去遍历nums[j],将其与nums[i]做判断

if(nums[j] < nums[i]){

dp[i] = Math.max(dp[j] + 1, dp[i]);

}

3.初始化

由dp[i]定义可得,不管以哪个元素为结尾,此时最长递增子序列的长度初始化都为1

dp[i]=1;

4.遍历顺序

for(int i = 1; i < nums.length; i++){

for(int j = 0; j < i; j++){

  //递推公式 if(nums[j] < nums[i]){

   dp[i] = Math.max(dp[j] + 1, dp[i]);

    }

    //得到最大值

    result = Math.math(result, dp[i]);

}

}

5.返回结果

由dp[i]定义可得,为得到最长递增子序列的长度,我们需要遍历整个dp[i],找出最大的那个dp[i],就是最长递增子序列的长度。

674.最长连续递增子序列

与最长递增子序列这道题基本一样,因为子序列要求连续,所以只需要比较nums[i-1]与nums[i]大小即可,就不用内套for循环遍历j了

718.最长重复子数组

此题的子数组就是指连续子序列

1.dp[i][j]表示以numsA[i-1]为结尾的numsA数组,和以numsB[j-1]为结尾的numsB数组,两者的最长重复子数组长度为dp[i][j]。

问:为什么不像前两道题一样表示以numsA[i]为结尾和以numsB[j]为结尾呢?

答:如果像这样设置,那么就导致初始化的时候dp[0][j]和dp[i][0]就有了意义,

比如说dp[0][j],表示数组A的numsA[0]元素要和numsB[j]的所有元素遍历一遍,确定哪个相等然后赋值为1,不相等在赋值为0。代码显得很冗余。

而表示以numsA[i-1]为结尾和以numsB[j-1]为结尾,就可以使得初始化的时候dp[0][j]和dp[i][0]变得没有意义,单纯为了递推状态而设计,所以初始化为0就可以了。

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

2.递推公式,在比较完数组A的numsA[i-1]和数组B的numsB[j-1]之后,比较下一个元素时,需要将两个数组同时回退,所以dp[i][j]的状态是由dp[i-1][j-1]得到的

所以递推公式为:

if(numsA[i-1] == numsB[j-1]){

dp[i][j] = dp[i-1][j-1] + 1;

}

3.初始化

因为我们定义的dp[i][j],表示的numsA[i-1]和numsB[j-1],所以对于dp[0][j]和dp[i][0]必须初始化为0,否则没意义。

4.遍历顺序

for(int i = 1; i <= numsA.length; i++){

for(int j = 1; j <= numsB.length; j++){

  //递推公式

    if(numsA[i-1] == numsB[j-1]){

    dp[i][j] = dp[i-1][j-1] + 1;

    }

  //这里直接取最大值,减少后续为了得到结果还要遍历dp数组的冗余操作

  result = Math.max(result, dp[i][j]);

}

}

1143.最长公共子序列

1.dp[i][j]表示长度为[0, i-1]的字符串text1 与 长度为[0, j-1]的字符串text2 的最长公共子序列长度为dp[i][j]

为什么设置为[0, i-1]和[0, j-1],而不是[0, i]和[0, j],在最长重复子数组中解释了,就是因为初始化更简单

int[][] dp = new int[text1.length() + 1][text2.length() + 1];

2.递推公式

dp[i][j]的状态可以从三个方向推出

(1)dp[i-1][j-1]

if(text1[i-1] == text2[j-1]){

dp[i][j] = dp[i-1][j-1] + 1;

}

(2)dp[i-1][j]

(3)dp[i][j-1]

第(2)种和第(3)种状态取最大值赋给dp[i][j]

详解:我们在比较完text1[i-1]与text2[j-1]时,发现两者不同,那么我们就要比较[0, i-2]的ab字符串与[0,j-1]的abe字符串的最长重复子序列(因为是子序列,不是子数组,所以不要求连续),以及比较[0, i-1]的abc字符串与[0,j-2]的ab字符串的最长重复子序列

else{

dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);

}

3.初始化

dp[i][0]=0;

dp[0][j]=0;

4.遍历顺序

for(int i = 1; i <= text1CharArray.length; i++){

for(int j = 1; j <= text2CharArray.length; j++){

  //递推公式

}

}

5.打印

返回结果,因为要返回最长公共子序列,所以肯定在dp数组的最后一个元素

return dp[text1.length()][text2.length()];

1035.不相交的线

思路:与求最长公共子序列是一模一样的题。

直线不能相交,这就是说明在字符串nums1中找到一个与字符串nums2相同的子序列,且这个子序列

不能改变相对顺序,只要相对顺序不改变,连接相同数字的直线就不会相交。

本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!

53.最大子序和

思路:

1.dp[i]表示以nums[i]为结尾的最大连续子序列和为dp[i]

int[] dp = new int[nums.length];

2.递推公式

dp[i]的状态可以由以下两种情况得到:

情况1:dp[i-1] + nums[i],在前面dp[i-1]基础上,再加上nums[i];

情况2:nums[i],舍弃前面的序列和,从nums[i]开始重新计算。

所以递推公式为:

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

3.初始化

dp[0] = nums[0];

4.遍历顺序

5.返回dp的最大值

392.判断子序列

1.dp[i][j]表示长度为[0, i-1]的字符串s 和 长度为[0,j-1]的字符串t ,两者的最长公共子序列的长度为dp[i][j]

然后判断dp[s.length()][t.length()] ?= s.length();

如果相等就返回true;如果不相等就返回false。

整体上与那道最长公共子序列的题目解法一模一样

2.递推公式

dp[i][j]从三个方向获得

3.初始化,全初始为0

4.遍历顺序

5.打印

115.不同的子序列

题目解析:

计算在s的子序列中t出现的次数,就是s如何删除元素可以得到t。

1.dp[i][j]表示长度为[0,i-1]的字符数组 s 中 有 以长度为[0, j-1]的字符数组 t 的个数为dp[i][j]。

2.递推公式

当s[i-1] == t[j-1]时情况如何:

dp[i][j]可以由两种情况得到:

一部分是用s[i-1]来匹配,那么个数是dp[i-1][j-1]。即不考虑当前s字串和t字串的最后一位字母

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

所以dp[i][j] = dp[i-1][j-1] + dp[i-1][j];

当s[i-1] != t[j-1]时情况如何:

dp[i][j] = dp[i-1][j];

3.初始化

dp[i][0]表示长度为[0, i-1]的字符串s 有多少 长度为-1的字符串t(即空字符串),换句话来说就是s与多少种删除方法得到空字符串t。所以dp[i][0] = 1;

dp[0][j]表示长度为-1的字符串s 有多少长度为[0, j-1]的字符串t,所以初始化为0,dp[0][j] = 0;

dp[i][0]和dp[0][j]的交集dp[0][0] = 1;

4.遍历顺序

5.打印

返回return dp[s.length()][t.length()];

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

1.dp[i][j]表示长度为[0, i-1]的字符串word1和长度为[0, j-1]的字符串word2,想要让两者相等,所需要删除元素的最少次数为dp[i][j]

2.递推公式

两个字符串比对之后两种情况:

情况1:word1[i-1] == word2[j-1] 不需要删元素

dp[i][j] = dp[i-1][j-1];

情况2:word1[i-1] != word2[j-1] 需要删元素

需要删元素时又分为三种情况

情况1:删除word1[i-1]元素

dp[i][j] = dp[i-1][j] + 1;

情况2:删除word2[j-1]元素

dp[i][j] = dp[i][j-1] + 1;

情况3:同时删除word1[i-1]元素和word2[j-1]元素

dp[i][j] = dp[i-1][j-1] + 2;

因为dp表示删除元素的最少次数,所以要取最小值

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

小细节:因为dp[i][j-1] + 1 = dp[i-1][j-1] + 2为什么会相等?

答:对于情况2来说,dp[i][j-1] = dp[i-1][j-1] + 1;dp[i][j-1]本来就不考虑word2[j-1]了,那么在此基础上不考虑word1[i-1],就达到了删除两个元素的效果,然后等式两侧在加1,即dp[i][j-1] + 1 = dp[i-1][j-1] + 1;出现情况2和情况3等价的结果。

所以结果可简化为dp[i][j] = Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1);

3.初始化

dp[i][0]:表示长度为[0, i-1]的字符串word1与空字符串word2,想要让两者相等,需要删除i个元素

所以dp[i][0] == i;

dp[0][j]同理

dp[0][j] == j;

4.遍历顺序

正常遍历

5.打印

返回结果

72.编辑距离

1.dp[i][j]表示以i-1为结尾的word1 和 以 j-1为结尾的word2,想要让两者相等,最少需要操作数为dp[i][j]

2.递推公式

当word1[i-1] == word2[j-1]时

此时不需要任何操作,dp[i][j]可由前一个状态得到

dp[i][j] = dp[i-1][j-1];

当word1[i-1] != word2[j-1]时

分为三种情况:

删除

添加

替换

先看删除操作:

(1)只删除word1[i-1],即不考虑word1[i-1]

dp[i][j] = dp[i-1][j] + 1;

(2)只删除word2[j-1],即不考虑word2[j-1]

dp[i][j] = dp[i][j-1] + 1;

(3)删除word1[i-1]也删除word[j-1]

dp[i][j] = dp[i-1][j-1] + 2;

情况2和情况3等价

再看添加操作:

添加操作和删除操作是一样的效果

最后看替换操作:

替换操作下是word1[i-1]和word2[j-1]不相等,然后只改变一个就可以相等

所以dp[i][j] = dp[i-1][j-1] + 1;

综上,最终的dp[i][j] = 取最小值

3.初始化

dp[i][0] = i;

dp[0][j] = j;

4.遍历顺序

5.打印,返回结果

647.回文子串

1.dp[i][j]表示区间范围[i, j](左闭右闭)的子串是否是回文串,如果是,那么dp[i][j]=true;如果不是,那么dp[i][j]=false。且j >= i。

boolean[][] dp = new boolean[s.length()][s.length()];

2.确定递推公式

整体上分两种情况

当s[i] == s[j]时

又分三种情况

情况一:下标i和下标j相同,即只有一个字符例如a,当然是回文串,dp[i][j] = true;

情况二:下标i和下标j相差为1,例如aa,也是回文串,dp[i][j] = true;

情况三:下标i和下标j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,那么看i到j区间是不是回文子串就看aba是不是回文就行了,那么aba的区间就是 i+1 与 j-1 区间,这个区间是不是回文就看dp[i+1][j-1]是否为true。如果是true,那么dp[i][j] = true; 如果是false,那么dp[i][j] = false;

当s[i] != s[j]时

此时不是回文串,即dp[i][j] = false;

递推公式每次为true时,result++用于记录回文子串的个数。

3.初始化

dp[i][j] = false;

4.遍历顺序

从dp[i][j]定义与递推公式上可以看出,情况三中dp[i][j]状态是由dp[i+1][j-1]状态得到的,即dp[i+1][j-1]在dp[i][j]的左下角,所以要从下到上,从左到右的遍历

for(int i = s.length()-1; i >= 0; i–){

for(int j = i; j < s.length(); j++){

  //递推公式

}

}

5.打印,返回结果return result;

516最长回文子序列

1.dp[i][j]表示区间范围[i, j]的字符串的最长回文子序列的长度是dp[i][j]

2.递推公式

分两种情况

s[i] == s[j]

此时dp[i][j] = dp[i+1][j-1] + 2;

s[i] != s[j]

排除s[i],此时dp[i][j] = dp[i+1][j];

排除s[j],此时dp[i][j] = dp[i][j-1];

所以dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]);

3.初始化

i==j的dp初始化为1

4.遍历方向

for(int i = s.length() – 1; i >= 0; i++){

for(int j = i + 1; j < s.length(); j++){

  //递推公式

}

}

5.打印,返回结果return dp[0][s.length()-1];

Categories:

Tags:

2 Responses

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注