本篇文章所有的动态规划都来自Carl哥的刷题网站,这真的对于系统学习算法来说是一个巨好的网站,题目层层递进,紧密相连,在这里强烈推荐,由于自己在刷题过程过程中一部分题目也有了自己的思路,且动态规划这一章能解决的问题十分多,而且解法往往十分高效、巧妙,代码量少,所以特意将其提取出来写成一篇博客
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];
}
}
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.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];
}
}
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();
}
}
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的重量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的和,你既可以一个个的加,你也可以用求和公式来计算,一维数组就相当于一个简化的求和公式
自己刚开始看到这道题是怎么也联想不到这跟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
// System.out.print(dp[j]+" ");//打印dp数组进行模拟比对
// }
// System.out.println();
}
if(dp[target]==target) return true;
return false;
}
}
自己最先想到的是贪心,但无法解决所以的问题,这个问题的关键是如何把这这一堆石头分成两堆重量最相近的石头,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];
}
}
为什么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]]可以联想起最基础的爬楼梯
遇到这种既考虑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];
}
}
虽然纯完全背包问题中,先遍历物品还是先遍历容量不会影响结果,但是在用完全背包求具体的问题中,会涉及到组合数和排列数,先遍历物品再遍历容量求的是组合数(我们可以联想到我们已经做过的一和零那道题),先遍历容量再遍历物品求的是排列数
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();
}
}
}
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];
}
}
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];
}
}
自己最初想到的是定义一个字符串数组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()];
}
}
自己由于刚从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];
}
}
自己刚开始做的时候,实在是想不出来环形的打家劫舍要怎么解决,但一看作者的题解,恍然大悟,原来环形的打家劫舍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
// 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
// System.out.print(dp[j]+" ");
// }
// System.out.println();
return dp[nums_.length-1];
}
}
至于为什么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]);
}
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];
}
}
自己毫无思路,但看了作者的题解,突然想起了在买卖股票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]);
由于在买卖股票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();
}
}
自己想的是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;
}
}
因为是平行相邻的,所以可以求的是连续子序列
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();
}
}
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();
}
虽然这道题已经用贪心解过几次了,但每次再做都是迷迷糊糊,这次用动态规划求连续的子数组和时,自己困惑的点,怎样才能保存前面多个元素的和,但是自己忘了动态规划中一个很重要的点,递推公式准确来说只能求出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;
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();
}
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];
}
}
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();
}
}
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;
}
}
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];
}
}
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]];
}
}
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];
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;
}
}
}
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]);
}
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;
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;
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()];
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;
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()];
第一种动态规划方法:既然两个字符串都可以删除,那么只要求出每一次是删除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()];
第二种:只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。
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()];
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;
}
}
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];
}
}