说明
本篇文章所有的动态规划都来自Carl哥的刷题网站,这真的对于系统学习算法来说是一个巨好的网站,题目层层递进,紧密相连,在这里强烈推荐,由于自己在刷题过程过程中一部分题目也有了自己的思路,且动态规划这一章能解决的问题十分多,而且解法往往十分高效、巧妙,代码量少,所以特意将其提取出来写成一篇博客
8.动态规划
8.1理论基础
8.2使用最小花费爬楼梯
-
思路
首先自己无法理解这个题目,综合了下网友的理解才弄懂这道题的思路
-
代码
class Solution {
public int minCostClimbingStairs(int[] cost) {
int[] dp=new int[cost.length+1];//可以理解最后面还有一扇
dp[0]=0;
dp[1]=0;
for(int i=2;i<dp.length;i++){
dp[i]=Math.min(dp[i-2]+cost[i-2],dp[i-1]+cost[i-1]);
}
return dp[dp.length-1];
}
}
8.3不同路径
-
思路
自己最先想到的是深度搜索,但已经忘了模板,以后学习的时候再补回来,由于只有两个方向,作者先是想到这是一个树形结构,这是个很值得学习的思路
- 代码
class Solution {
public int uniquePaths(int m, int n) {
int[][] dp=new int[m][n];
for(int j=0;j<n;j++){
dp[0][j]=1;
}
for(int i=0;i<m;i++){
dp[i][0]=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];
}
}
8.4不同路径2
-
思路
根据8.3不同路径的经验和模拟,自己首先想到的是在递推公式中如果obstacleGrid[i][j]==1,那么dp[i][j]=0,但是用例[[1,0]]的结果为1,但实际结果是0,所以在初始化中,如果第一行的obstacleGrid[0][j]=1,那么第一行中j到n-1的dp[0][j]都应该为0,在第一列中同理
- 代码
首先是自己写的代码,在初始化的过程中,由于没有考虑到数组默认的初始化为0.代码臃肿
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m= obstacleGrid.length;
int n=obstacleGrid[0].length;
int[][] dp=new int[m][n];
for(int j=0;j<n;j++){
if(obstacleGrid[0][j]==1){
for(int count_j=j;count_j<n;count_j++){
dp[0][count_j]=0;
}
break;
}
dp[0][j]=1;
}
for(int i=0;i<m;i++){
if(obstacleGrid[i][0]==1){
for(int count_i=i;count_i<m;count_i++){
dp[count_i][0]=0;
}
break;
}
dp[i][0]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(obstacleGrid[i][j]==1){
dp[i][j]=0;
continue;
}
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
return dp[m-1][n-1];
}
}
作者的代码
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int n = obstacleGrid.length, m = obstacleGrid[0].length;
int[][] dp = new int[n][m];
for (int i = 0; i < m; i++) {
if (obstacleGrid[0][i] == 1) break; //一旦遇到障碍,后续都到不了
dp[0][i] = 1;
}
for (int i = 0; i < n; i++) {
if (obstacleGrid[i][0] == 1) break; 一旦遇到障碍,后续都到不了
dp[i][0] = 1;
}
for (int i = 1; i < n; i++) {
for (int j = 1; j < m; j++) {
if (obstacleGrid[i][j] == 1) continue;
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[n - 1][m - 1];
}
}
8.5 01背包(二维数组)
- 题目
- 思路
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 main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagsize = 4;
int[][] dp=new int[weight.length][bagsize+1];
for(int i=0;i<bagsize;i++){
if(i>=weight[0]) dp[0][i]=value[0];
}
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(j>=weight[i]){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}else {
dp[i][j]=dp[i-1][j];
}
}
}
//打印二维数组
for(int i=0;i<dp.length;i++){
for(int j=0;j<dp[0].length;j++){
System.out.print("dp["+i+"]"+"["+j+"]:"+dp[i][j]+" ");
}
System.out.println();
}
}
8.6 01背包(一维数组)
-
思路
-
代码
先遍历背包容量再遍历物品的代码(结果为错),理解作者说的这样遍历每次只放入了一个物品
package April;
public class day4_14_2 {
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
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]);
// }
for(int j=bagWeight;j>=0;j--){
for(int i=0;i<wLen;i++){
if(j>=weight[i]){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int i = 0; i<= bagWeight; i++){
System.out.print(dp[i] + " ");
}
System.out.println();
}
}
}
此时从遍历的结果上来看,这样遍历dp[j]可以理解为容量为j的背包只放一个物品能获得的最大的价值
- 先遍历物品,再遍历容量,但容量从小到大遍历(结果错误),如果一旦正序遍历了,那么物品0就会被重复加入多次!不过可以用来解决完全背包中同一物品可以被多次放入的计算
举一个例子:物品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 - weight[0]] + value[0] = 15 (dp[1]初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
package April;
public class day4_14_3 {
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
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 = 0; j <=bagWeight; j++){
if(j>=weight[i]){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
System.out.println();
}
}
}
从输出的结果上来看,此时dp[j]可以理解为容量为j的背包,如果物品可以被重复加入多次,求它能获取的最大价值
- 正确代码
package April;
public class day4_14 {
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
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]);
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
System.out.println();
}
}
}
其实模拟这个过程,跟01背包二维数组的结果是一样的,只不过它每次只算出了二维数组的一行,那么它为什么就能表示出这样的结果呢,其实这就跟你求1~100的和,你既可以一个个的加,你也可以用求和公式来计算,一维数组就相当于一个简化的求和公式
8.7分割等和子集
-
思路
自己刚开始看到这道题是怎么也联想不到这跟01背包有关系,对于这种组合问题,自己最先想到的是回溯,但终止条件是什么呢?看来作者的思路,恍然大悟,这道题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等,那么只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。所以终止条件是path里面的元素之和为sum/2。
接下来看动态规划的思路
二刷时对本题的更深理解:
如果该数组能背分割成元素之和相等的两个子集,也就是说从i个物品中任选x个物品,这x个物品的体积是一定能凑成sum/2的,如果w[i]=v=alue[i],那么这x个物品的最大价值叶问sum/2了,,我们以 1 2 3,2 3 5,3 5 8这几个简单的例子就能看出来
- 代码
class Solution {
public boolean canPartition(int[] nums) {
int[] dp=new int[10001];
int sum=0;
int target=0;
for(int i=0;i<nums.length;i++){
sum+=nums[i];
}
if(sum%2==1) { //如果元素之和不为偶数,直接返回false
return false;
}else {
target=sum/2;
}
// System.out.println(target);
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]);
}
// for(int j=0;j<target+1;j++){
// System.out.print(dp[j]+" ");//打印dp数组进行模拟比对
// }
// System.out.println();
}
if(dp[target]==target) return true;
return false;
}
}
8.8最后一块石头的重量II
-
思路
自己最先想到的是贪心,但无法解决所以的问题,这个问题的关键是如何把这这一堆石头分成两堆重量最相近的石头,8.7是把一堆石头分解成两堆重量相等的石头,令人感到神奇的是01背包的动态规划真的能达到这样的效果,且看作者思路
- 代码
class Solution {
public int lastStoneWeightII(int[] stones) {
int[] dp=new int[15000];
int sum=0;
int target=0;
for(int i=0;i<stones.length;i++){
sum+=stones[i];
}
target=sum/2;
for(int i=0;i<stones.length;i++) {
for (int j = target; j >= stones[i]; j--) {
dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return (sum-dp[target])-dp[target];
}
}
8.9目标和
-
思路
自己完全懵逼了,不知道这跟01背包有啥关系
作者思路
这个思路一出来简直惊为天人,由此感觉用回溯也能解决了
为什么dp[2]=10呢?根据01背包的滚动数组概念,dp[j]是任选物品,题目给出的是5个1,要想获取left2的背包,就是C_5^2
- 代码
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int[] dp=new int[10000];
dp[0]=1;
int sum=0,left=0;
for(int i=0;i<nums.length;i++){
sum+=nums[i];
}
left=(sum+target)/2;
if((sum+target)%2==1) {
return 0;
}
if(Math.abs(target)>sum){
return 0;
}
for(int i=0;i<nums.length;i++){
for(int j=left;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
// for(int j=0;j<=left;j++){
// System.out.print(dp[j]+" ");
// }
// System.out.println();
}
return dp[left];
}
}
由此可以见,是可以用01背包的dp数组求组合个数的,其实 dp[j]+=dp[j-nums[i]]可以联想起最基础的爬楼梯
8.10一和零
-
思路
遇到这种既考虑A,又考虑B的题目,如果同时考虑往往会觉得无从下手,如果我们只考虑0,m=5,strs里面的每一个元素都有m_个零,如果m>m_,是不是就可以联想到01背包的问题了,可以转化理解为背包容量为5,可以装下多少个带0的字符串,m_就是物体的weight[i],1就是物品value[i],所以此题可以转化为同时有两个背包的01背包
- 代码
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
int[][] dp=new int[101][101];
for(String str:strs){
int countZero=0;
int countOne=0;
for(char s:str.toCharArray()){
if(s=='0') countZero++;
else countOne++;
}
for(int i=m;i>=countZero;i--){
for(int j=n;j>=countOne;j--){
dp[i][j]=Math.max(dp[i][j],dp[i-countZero][j-countOne]+1);
}
}
}
return dp[m][n];
}
}
8.11完全背包理论基础
虽然纯完全背包问题中,先遍历物品还是先遍历容量不会影响结果,但是在用完全背包求具体的问题中,会涉及到组合数和排列数,先遍历物品再遍历容量求的是组合数(我们可以联想到我们已经做过的一和零那道题),先遍历容量再遍历物品求的是排列数
8.11_2零钱兑换II(完全背包求组合数)
-
思路
-
代码
求组合数的代码
package April;
public class day4_17_2 {
public static void main(String[] args) {
int[] coins={1, 2, 5};
int amount=5;
int[] dp=new int[5001];
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]];
}
for(int j=0;j<=amount;j++){
System.out.print(dp[j]+" ");
}
System.out.println();
}
System.out.println(dp[amount]);
}
}
求排列数的代码
package April;
public class day4_17_3 {
public static void main(String[] args) {
int[] coins={1, 2, 5};
int amount=5;
int[] dp=new int[5001];
dp[0]=1;
for(int j=0;j<=amount;j++){
for(int i=0;i<coins.length;i++){
if(j>=coins[i]){
dp[j]+=dp[j-coins[i]];
}
}
for(int i=0;i<=amount;i++){
System.out.print(dp[i]+" ");
}
System.out.println();
}
}
}
8.12_3组合总数IV(完全背包求排列数)
-
代码
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp=new int[target+1];
dp[0]=1;
for(int j=0;j<=target;j++){
for(int i=0;i<nums.length;i++){
if(j>=nums[i]) dp[j]+=dp[j-nums[i]];
}
}
return dp[target];
}
}
8.12零钱兑换
-
思路
自己解决掉了,但注意下这题中的递推公式和数组初始化
-
代码(主要学习下它最后的返回形式,使代码优美化)
class Solution {
public int coinChange(int[] coins, int amount) {
int max = Integer.MAX_VALUE;
int[] dp = new int[amount + 1];
//初始化dp数组为最大值
for (int j = 0; j < dp.length; j++) {
dp[j] = max;
}
//当金额为0时需要的硬币数目为0
dp[0] = 0;
for (int i = 0; i < coins.length; i++) {
//正序遍历:完全背包每个硬币可以选择多次
for (int j = coins[i]; j <= amount; j++) {
//只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
if (dp[j - coins[i]] != max) {
//选择硬币数目最小的情况
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == max ? -1 : dp[amount];
}
}
8.13单词拆分
-
思路
自己最初想到的是定义一个字符串数组dp,然后dp[s.length()]==就是true,但是实现起来根本不好操作,然后又想到了回溯,但觉得会超时,所以没有细想,作者也想到了回溯,而且还优化了不让它超时,但此题的重点是作者的完全背包思路
- 代码
自己最初并没有看题解,根据作者的思路来写的代码,虽然运行结果对了,但总感觉写的复杂了
注意点,本题跟遍历顺序还是有关系的,如果先遍历物品再遍历容量,会在String s = “applepenapple”;ist wordDict = new ArrayList<>();
wordDict.add(“apple”);
wordDict.add(“pen”)报错
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length()+1;
boolean[] dp = new boolean[n];
dp[0] = true;
for(int j = 0; j < n; j++){
for(int i = 0; i < wordDict.size(); i++){
int tem = wordDict.get(i).length();
if(j >= tem ) {
String str = s.substring(j-tem,j);
if(wordDict.contains(str) && (dp[j-tem] == true)) {
//System.out.println("str:"+str);
dp[j] = true;
}
}
}
}
return dp[n-1];
}
}
然后看了下作者的题解,其实这题跟遍历物品没多大关系,代码确实优化了不少
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] valid = new boolean[s.length() + 1];
valid[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = 0; j < i; j++) {
if (wordDict.contains(s.substring(j,i)) && valid[j]){
//按照二刷时的思路,其实s1.equals(wordDict.get(i))也是可以的
valid[i] = true;
}
}
}
return valid[s.length()];
}
}
8.14 打家劫舍
-
思路
自己由于刚从01背包和完全背包转过来,脑子里就会形成惯性思维,由于每件物品都只能放一件,首先想到的是01背包,但想了好久始终找不出这道题的背包”容量“在哪里,看了题解,才想起动态规划都不一定都是背包问题的呀,只要找到递推公式就行了
- 代码·
class Solution {
public int rob(int[] nums) {
int[] dp=new int[nums.length];
dp[0]=nums[0];
if(nums.length>1){
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];
}
}
8.15 打家劫舍II
-
思路
自己刚开始做的时候,实在是想不出来环形的打家劫舍要怎么解决,但一看作者的题解,恍然大悟,原来环形的打家劫舍II无非是打家劫舍分两种情况考虑
- 代码
package April;
public class day4_20_2 {
public static void main(String[] args) {
int[] nums={1,2,3,1};
int a1=test(nums,1,nums.length-1); //不考虑头元素,考虑尾元素
int a2=test(nums,0,nums.length-2);//考虑头元素,不考虑尾元素
int max_=Math.max(a1,a2);
System.out.println(max_);
}
public static int test(int[] nums,int start,int end){
int length=end-start+1;
int[] nums_=new int[length];
int[] dp=new int[nums_.length];
for(int j=start,i=0;j<=end;j++){
nums_[i++]=nums[j];
}
dp[0]=nums_[0];
// for(int j=0;j<nums_.length;j++){
// System.out.println("nums_["+j+"]:"+nums_[j]);
// }
if(nums_.length>1){
dp[1]=Math.max(nums_[0],nums_[1]);
}
for(int j=2;j<nums_.length;j++){
dp[j]=Math.max(dp[j-2]+nums_[j],dp[j-1]);
}
// for(int j=0;j<nums_.length;j++){
// System.out.print(dp[j]+" ");
// }
// System.out.println();
return dp[nums_.length-1];
}
}
8.16打家劫舍III(二叉树,未做)
8.17买卖股票的最佳时机(只有一次买卖)
-
思路
至于为什么0代表持有,1代表不持有,用生活常识理解可能不太记得住,但我们的惯性思维是先考虑0,再考虑1,先考虑持有,再考虑不持有,这样就对的上了
- 代码
public static void main(String[] args) {
int[] prices={7,1,5,3,6,4};
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]);
}
for(int i=0;i<prices.length;i++){
System.out.print("dp["+i+"][0]:"+dp[i][0]+" ");
}
System.out.println();
for(int i=0;i<prices.length;i++){
System.out.print("dp["+i+"][1]:"+dp[i][1]+" ");
}
System.out.print(dp[prices.length-1][1]);
}
8.18买卖股票的最佳时机II(多次买卖)
-
思路
- 代码
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];
}
}
8.19买卖股票的最佳时机III()
-
思路
自己毫无思路,但看了作者的题解,突然想起了在买卖股票II中,因为可以买卖多次,所以在买入股票的时候,可能会有之前买卖的利润即: dp[i][0]=Math.max(dp[i-1][0],dp[i-1][1]-prices[i]);在这题中由于只交易两次,所以我们可以记录第一次买卖能获取最大利润的状态,从而再推导出第二次买卖能获取的最大的利润,至于在此题中为什么dp[i][0]=dp[i-1][0],我在代码中给出了自己的理解
- 代码
public static void main(String[] args) {
int[] prices={3,3,5,0,0,3,1,4};
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]);
}
for(int i=0;i<prices.length;i++){
for(int j=0;j<5;j++){
System.out.print("dp["+i+"]["+j+"]:"+dp[i][j]+" ");
}
System.out.println();
}
System.out.println(dp[prices.length-1][4]);
8.20买卖股票的最佳时机IIII
-
思路
由于在买卖股票III中自己已经发现了既然求出第二次买卖的最大值需要求出第一次买卖的最大值,依次类推,拿第三次肯定也需要根据第一次来推出,所以自己很快就自己解决啦,很开心,毕竟这是一道困难题呢
- 代码
public static void main(String[] args) {
int[] prices={3,3,5,0,0,3,1,4};
int k=2;
int m=k*2+1;
int[][] dp=new int[prices.length][k*2+1];
for(int i=0;i<k*2+1;i++){
if(i%2==1){
dp[0][i]=-prices[0];
}
}
for(int i=1;i<prices.length;i++){
for(int j=0;j<m;j++){
if(j==0){
dp[i][j]=dp[i-1][j];
}
else if(j%2==1){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]-prices[i]);
}
else if(j%2==0){
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-1]+prices[i]);
}
}
}
for(int i=0;i<prices.length;i++){
for(int j=0;j<m;j++){
System.out.print("dp["+i+"]["+j+"]:"+dp[i][j]+" ");
}
System.out.println();
}
}
8.21最长递增子序列
-
思路
自己想的是dp[i]表示的是在i位置所能获取到的最长子序列,然后遍历<=i的元素
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);
}
}
}
对于数组的初始化,每一个dp[i]初始值的长度都为1,一看题解,跟作者完美契合,但作者的代码更为简
- 代码
自己
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp=new int[nums.length];
int max=0;
for(int i=0;i<nums.length;i++){
dp[i]=1;
}
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);
}
}
}
for(int i=0;i<nums.length;i++){
max=Math.max(max,dp[i]);
}
return max;
}
}
作者
class Solution {
public int lengthOfLIS(int[] nums) {
int[] dp=new int[nums.length];
Arrays.fill(dp, 1);
int res=0;
for (int i = 0; i < dp.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]>res) res=dp[i];
}
}
return res;
}
}
8.22最长重复子数组
-
思路
因为是平行相邻的,所以可以求的是连续子序列
- 代码
public static void main(String[] args) {
int[] nums1={1,2,3,2,1};
int[] nums2={3,2,1,4,7};
int[][] dp=new int[nums1.length+1][nums2.length+1];
int res=0;
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(nums1[i-1]==nums2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}
if(dp[i][j]>res) res=dp[i][j];
}
}
for(int i=0;i<dp.length;i++){
for(int j=0;j<dp[0].length;j++){
System.out.print("dp["+i+"]"+"["+j+"]:"+dp[i][j]+" ");
}
System.out.println();
}
}
8.23最长公共子序列
- 思路
自己刚开始想的是
if(s1[i-1]==s2[j-1]){
for(int m=0;m<i;m++){
for(int n=0;n<j;n++){
dp[i][j]=Math.max(dp[i][j],dp[m][n]+1);
if(dp[i][j]>res) res=dp[i][j];
}
}
}
既然是求子序列,如果s1[i-1]==s2[j-1],dp[i][j]应该是由前面所有可能dp[m][n]+1的最大值求出来的,我们以text1="abcde"和text2="bace"讲解,也就是下面的图解,当s1[2]==s2[2]时,我们求出前面dp[m][n]+1(即所有方向)的最大值,我们是能求出正答案的,但会在第42个用例上超时
其实我们从dp[i][j]的定义上可以看出,其实dp[i][j]只需要从三个方向上就能得出
- 代码
自己:超时
public static void main(String[] args) {
String text1="abcde";
String text2="ace";
char[] s1=text1.toCharArray();
char[] s2=text2.toCharArray();
int[][] dp=new int[s1.length+1][s2.length+1];
int res=0;
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(s1[i-1]==s2[j-1]){
for(int m=0;m<i;m++){
for(int n=0;n<j;n++){
dp[i][j]=Math.max(dp[i][j],dp[m][n]+1);
if(dp[i][j]>res) res=dp[i][j];
}
}
}
}
}
for(int i=0;i<dp.length;i++){
for(int j=0;j<dp[0].length;j++){
System.out.print("dp["+i+"]"+"["+j+"]:"+dp[i][j]+" ");
}
System.out.println();
}
}
作者:
String text1="abcde";
String text2="ace";
char[] s1=text1.toCharArray();
char[] s2=text2.toCharArray();
int[][] dp=new int[s1.length+1][s2.length+1];
int res=0;
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(s1[i-1]==s2[j-1]){
dp[i][j]=dp[i-1][j-1]+1;
}else {
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
if(dp[i][j]>res) res=dp[i][j];
}
}
for(int i=0;i<dp.length;i++){
for(int j=0;j<dp[0].length;j++){
System.out.print("dp["+i+"]"+"["+j+"]:"+dp[i][j]+" ");
}
System.out.println();
}
8.24 最大子数组和
- 思路
虽然这道题已经用贪心解过几次了,但每次再做都是迷迷糊糊,这次用动态规划求连续的子数组和时,自己困惑的点,怎样才能保存前面多个元素的和,但是自己忘了动态规划中一个很重要的点,递推公式准确来说只能求出dp[i]和dp[i-1]这两者的之间的联系,不能直接从公式里看出前面所有元素的关系,但是dp[i-1]的结果是可以根据dp[i-2]递推出来的,所以我们在写递推公式中,只需要考虑dp[i]和dp[i-1]之间的联系,然后再来模拟验证就行了
- 代码
int[] dp=new int[nums.length];
dp[0]=nums[0];
int res=dp[0];
for(int i=1;i<nums.length;i++){
dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
if(dp[i]>res){
res=dp[i];
}
}
return res;
8.25不同的子序列
-
思路
-
代码
char[] s1=text1.toCharArray();
char[] s2=text2.toCharArray();
int[][] dp=new int[s1.length+1][s2.length+1];
for(int j=0;j< dp.length;j++){
dp[j][0]=1;
}
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(s1[i-1]==s2[j-1]){
dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
} else dp[i][j]=dp[i-1][j];
}
}
for(int i=0;i<dp.length;i++){
for(int j=0;j<dp[0].length;j++){
System.out.print("dp["+i+"]"+"["+j+"]:"+dp[i][j]+" ");
}
System.out.println();
}
8.26编辑距离
-
思路
-
代码
class Solution {
public int minDistance(String word1, String word2) {
char[] s1=word1.toCharArray();
char[] s2=word2.toCharArray();
int[][] dp=new int[s1.length+1][s2.length+1];
for(int i=0,j=0;i<dp.length;i++){
dp[i][0]=i;
}
for(int j=0;j<dp[0].length;j++){
dp[0][j]=j;
}
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(s1[i-1]==s2[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];
}
}
8.27回文子串
-
思路
-
代码
public static void main(String[] args) {
String s="aaa";
char[] s_=s.toCharArray();
int res=0;
boolean[][] dp=new boolean[s.length()][s.length()];
for(int i=dp.length-1;i>=0;i--){
for(int j=i;j<dp[0].length;j++){
if(s_[i]==s_[j]){
if(j-i<=1){
dp[i][j]=true;
res++;
}else{
if(dp[i+1][j-1]==true){
dp[i][j]=true;
res++;
}
}
}
}
}
System.out.println("res:"+res);
for(int i=0;i<dp.length;i++){
for(int j=0;j<dp[0].length;j++){
System.out.print("dp["+i+"]"+"["+j+"]:"+dp[i][j]+" ");
}
System.out.println();
}
}
8.28最长回文子串
-
思路
-
代码
class Solution {
public int longestPalindromeSubseq(String s) {
char[] s_=s.toCharArray();
int res=1;
int [][] dp=new int[s.length()][s.length()];
for(int i=0;i<dp.length;i++){
dp[i][i]=1;
}
for(int i= dp.length-1;i>=0;i--){
for(int j=i+1;j< dp.length;j++){ //j=i+1 可以有效防止 dp[i][j]=dp[i+1][j-1]+2发生越界
if(s_[i]==s_[j]){
dp[i][j]=dp[i+1][j-1]+2;
}else{
dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
}
if(dp[i][j]>res){
res=dp[i][j];
}
}
}
return res;
}
}
8.29 不同的二叉搜索树
-
思路
-
代码
class Solution {
public int numTrees(int n) {
int[] dp=new int[n+1];
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];
}
}
动态规划能解决的问题(二刷)
1 递推公式很明显的爬楼梯问题
2 网格的路径总数问题
3.分割等和子集问题
4.求组合数
5.求排列数
for(int j=0;j<=amount;j++){
for(int i=0;i<coins.length;i++){
if(j>=coins[i]){
dp[j]+=dp[j-coins[i]];
}
}
6.求组合最小数
int max = Integer.MAX_VALUE;
int[] dp = new int[amount + 1];
//初始化dp数组为最大值
for (int j = 0; j < dp.length; j++) {
dp[j] = max;
}
//当金额为0时需要的硬币数目为0
dp[0] = 0;
for (int i = 0; i < coins.length; i++) {
//正序遍历:完全背包每个硬币可以选择多次
for (int j = coins[i]; j <= amount; j++) {
//只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要
if (dp[j - coins[i]] != max) {
//选择硬币数目最小的情况
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1);
}
}
}
return dp[amount] == max ? -1 : dp[amount];
7.单词的拆分
boolean[] valid = new boolean[s.length() + 1];
valid[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = 0; j < i; j++) {
if (wordDict.contains(s.substring(j,i)) && valid[j]){
//按照二刷时的思路,其实s1.equals(wordDict.get(i))也是可以的
valid[i] = true;
}
}
}
8.取与不取的最大值
- 题目1
dp[0]=nums[0];
if(nums.length>1) 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]);
}
- 题目二
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]);
}
9.求最长递增子序列(max的取值在区间范围内取,而不是在末尾)
-
题目
-
代码
int[] dp=new int[nums.length];
int max=1;
Arrays.fill(dp,1);
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;
10.求最长重复子数组
-
题目
-
代码
int[][] dp=new int[nums1.length+1][nums2.length+1];
for(int i=0;i<dp.length;i++) dp[i][0]=0;
for(int j=0;j<dp[0].length;j++) dp[0][j]=0;
int res=0;
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(nums1[i-1]==nums2[j-1]){
dp[i][j]=dp[i-1][j-1]+1
if(dp[i][j]>res) res=dp[i][j];
}
}
}
return res;
11最长公共子序列
-
题目
-
代码
int[][] dp=new int[text1.length()+1][text2.length()+1];
for(int i=0;i<dp.length;i++) dp[i][0]=0;
for(int j=0;j<dp[0].length;j++) dp[0][j]=0;
for (int i=1;i< dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(text1.charAt(i-1)==text2.charAt(j-1)){
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()];
12 最大和连续子数组
-
题目
-
代码
int[] dp=new int[nums.length];
dp[0]=nums[0];
int res=dp[0];
for(int i=1;i<dp.length;i++){
dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
if(dp[i]>res) res=dp[i];
}
return res;
13.字符串s有多少种方法通过删除字符得到另一个字符
-
题目
-
代码
int[][] dp = new int[s.length() + 1][t.length() + 1];
for (int i = 0; i < s.length() + 1; i++) {
dp[i][0] = 1;
}
for (int i = 1; i < s.length() + 1; i++) {
for (int j = 1; j < t.length() + 1; 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]; //如果s[i-1]与t[j-1]不想等等,那就相当于s删除掉一个s[i-1]跟t比较,所以就是以s[i-2]结尾的s与t[j-1]结尾的做对比,即dp[i-1[j]
}
}
}
return dp[s.length()][t.length()];
14.字符串word1,word2两个都可以删除自身的字符,求相等的最小步数
-
题目
-
代码
第一种动态规划方法:既然两个字符串都可以删除,那么只要求出每一次是删除word1的字符,还是删除word2的字符,亦或是删除word1,word2都删除的最小步数就行
int[][] dp=new int[word1.length()+1][word2.length()+1];
for(int i=0;i<dp.length;i++){
dp[i][0]=i;
}
for(int j=0;j<dp[0].length;j++){
dp[0][j]=j;
}
for(int i=1;i<dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else{
int tem=Math.min(dp[i-1][j]+1,dp[i][j-1]+1);
dp[i][j]=Math.min(tem,dp[i-1][j-1]+2);
}
}
}
return dp[word1.length()][word2.length()];
第二种:只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
15.编辑距离(一个字符串增、删、改得到另一个字符串)
-
题目
-
代码
int[][] dp=new int[word1.length()+1][word2.length()+1];
for(int i=0;i<dp.length;i++) dp[i][0]=i;
for(int j=0;j<dp[0].length;j++) dp[0][j]=j;
for(int i=1;i< dp.length;i++){
for(int j=1;j<dp[0].length;j++){
if(word1.charAt(i-1)==word2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1];
}else {
int tem=Math.min(dp[i-1][j]+1,dp[i][j-1]+1);
dp[i][j]=Math.min(tem,dp[i-1][j-1]+1);
}
}
}
return dp[word1.length()][word2.length()];
16.回文子串
-
题目
-
代码
class Solution {
public int countSubstrings(String s) {
int n=s.length();
int res=0;
boolean[][] dp=new boolean[n][n];
for(int i=n-1;i>=0;i--){ //从矩阵的左下角推右上角
for(int j=i;j<n;j++){
if(s.charAt(i)==s.charAt(j)){
if(j-i<=1){
dp[i][j]=true;
res++;
}else {
if(dp[i+1][j-1]){
dp[i][j]=true;
res++;
}
}
}
}
}
return res;
}
}
17.最长回文子序列
-
题目
-
代码
class Solution {
public int longestPalindromeSubseq(String s) {
int n=s.length();
int[][] dp=new int[n][n];
for(int i=0;i<n;i++){
for(int j=0;j<n;j++) dp[i][i]=1;
}
for(int i =n-1;i>=0;i--){
for(int j=i+1;j<n;j++){
if(s.charAt(i)==s.charAt(j)) dp[i][j]=dp[i+1][j-1]+2;//如果是相邻的dp[i][j](j=i+1),由于初始化数组的原因dp[i+1][j-1]必定为0,所以dp[i][j]=2
else dp[i][j]=Math.max(dp[i+1][j],dp[i][j-1]);
}
}
return dp[0][n-1];
}
}