本文章中的例题全部来自力扣
参考视频:代码随想录动态规划之完全背包,装满背包最少的物品件数是多少?| LeetCode:322.零钱兑换_哔哩哔哩_bilibili
动态规划五部曲
拿到一个题我怎么知道是不是动态规划呢,动态规划的题通常有这个特点:
对于原本的问题,我们倒推一步之后 仍然跟原本的问题一样。
举个栗子:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
我们分析一下这个题,n阶台阶 ,每次只能爬1或2阶,那么我们可以从n阶的前一阶爬一阶到n阶,也可以从n阶的前两阶爬2阶到n阶。那么这次爬了1阶或者2阶级之后呢,我们是怎么站在n-1阶上的呢,是不是又是从n-1的前1阶或者n-1的前2阶爬上来的。这样原本是爬n阶的问题,变成了爬n-1阶、n-2阶的问题,这就是动态规划。
动态规划五部曲:
1、给dp[i][j]或者dp[i]定义i、j 以及dp值的意义
2、找状态转移方程
3、dp数组初始化
4、遍历顺序
5、打印dp数组(检查dp数组的值是否与自己推断的相同)
接下来依次学习动规基础、背包问题、打家劫舍、股票问题、子序列问题,相信学完之后菜狗我也能顺利做出动规题,提前比耶✌
爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
这个题上面分析过了 我就不再赘述,直接给出dp定义和状态转移方程。
dp[i]:表示爬到i阶的方法总数
状态转移方程:dp[i]=dp[i-1]+dp[i-2];
初始化: dp[1]=1; dp[2]=2;
代码:
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//在此输入您的代码...
int n=scan.nextInt();
int[] dp=new int [n+1];
for(int i=1;i<=n;i++) {
if(i==1) {
dp[i]=1;
}
else if(i==2) {
dp[i]=2;
}
else dp[i]=dp[i-1]+dp[i-2];
}
System.out.println(dp[n]);
scan.close();
}
最小花费爬楼梯
给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。
你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。
请你计算并返回达到楼梯顶部的最低花费。
示例 1:
输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。
示例 2:输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。
与上题其实差不多,区别在于我们此时找的是最小的花费,花费的不同是因为选择爬的方法的不同导致的,那么我们爬到n阶的方法仍然是两种,从n-1爬上来或者从n-2爬上来,这样导致花费也是两种,我们就取小的哪一种。
dp[ i ]:跳到i阶的最小花费
状态转移方程:
dp[i]=Math.min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
初始化:dp[0]=0 ,dp[1]=0
代码:
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//在此输入您的代码...
int n=scan.nextInt();
int [] cost=new int [n];
for( int i=0;i<n;i++) {
cost[i]=scan.nextInt();
}
int[] dp=new int [n+1];
for(int i=1;i<=n;i++) {
if(i==1) {
dp[i]=0;
}
else if(i==0) {
dp[i]=0;
}
else dp[i]=Math.min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2]);
}
System.out.println(dp[n]);
scan.close();
}
不同路径
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
分析题目,走到随机一个方格(m,n) ,都只能从上方格(m-1,n)或者左边方格(m,n-1)过来,那我们走到(m-1,n)也只能从相对于他的上方格或左方格过来。这样本来是(m,n)的问题,变成了(m-1,n)和(m,n-1)的问题。
dp[ i ][ j ]:表示走到方格(i,j)的路径种数。
状态转移方程:
dp[i][j]=dp[i-1][j]+dp[i][j-1];
初始化:dp[0][j]=1;dp[i][0]=1
遍历顺序: i 从0遍历到m,j 从0遍历到n
代码:
void DifferentRoute(int m,int n) {
int[][] dp=new int [m+1][n+1];
for(int i=1;i<=m;i++) {
for(int j=1;j<=n;j++) {
if(i==1||j==1) dp[i][j]=1;
else {
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
System.out.println(dp[m][n]);
}
不同路径II
一个机器人位于一个
m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
网格中的障碍物和空位置分别用
1
和0
来表示。
分析题目:
对于状态转移方程:
一个普通的方格i j,走到i j的不同路径来自上方和左方,也就是说如果上方是障碍物,那么此时到i j的路径只能从左方来;如果左方有障碍物,那么到i j的路径只能从上方来。
有障碍的不同路径题中 需要关注的地方是对于dp数组的初始化:
1. 当dp数组首行、首列有障碍时,说明障碍之后的方格都不可走,那么dp[i][j]=0,障碍之前的方格初始化为1.
2. 当起点、终点有障碍时,那么无路可走,直接return0;
dp[ i ][ j ]:表示走到方格(i,j)的路径种数。
状态转移方程:
dp[i][j]=dp[i][j-1]+dp[i-1][j]; (Barrier[i-1][j]==0&&Barrier[i][j-1]==0)
dp[i][j]=dp[i-1][j]; (Barrier[i][j-1]==1)
dp[i][j]=dp[i][j-1]; (Barrier[i-1][j]==1)
遍历顺序: i 从0遍历到m,j 从0遍历到n
代码:
int DifferentRouteBarrier(int[][]Barrier) {
int m=Barrier.length;
int n=Barrier[0].length;
System.out.println("m "+m+" n"+n);
int[][] dp=new int [m+1][n+1];
// 初始化
for(int j=0;j<n;j++) {
if(Barrier[0][j]==0) {
dp[0][j]=1;
}
else break;
// System.out.println("i="+0+"j="+j+" dp="+dp[0][j]); //打表
}
for(int j=0;j<m;j++) {
if(Barrier[j][0]==0) {
dp[j][0]=1;
}
else break;
// System.out.println("i="+j+"j="+0+" dp="+dp[j][0]); //打表
}
for(int i=1;i<m;i++) {
for(int j=1;j<n;j++) {
if(Barrier[i-1][j]==1&&Barrier[i][j-1]==1) dp[i][j]=0;
else if(Barrier[i-1][j]==1) {
dp[i][j]=dp[i][j-1];
}
else if(Barrier[i][j-1]==1) {
dp[i][j]=dp[i-1][j];
}
else {
dp[i][j]=dp[i][j-1]+dp[i-1][j];
}
// System.out.println("i="+i+"j="+j+" dp="+dp[i][j]); //打表
}
}
if(Barrier[m-1][n-1]==1||Barrier[0][0]==1) dp[m-1][n-1]=0;
return dp[m-1][n-1];
}
343-整数拆分
给定一个正整数
n
,将其拆分为k
个 正整数 的和(k >= 2
),并使这些整数的乘积最大化。返回 你可以获得的最大乘积 。
示例 1:
输入: n = 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。
题目分析:整数n拆分成2个以上的正整数,考虑特殊情况,如果拆分成包含1的数,那么乘积一定不是最大的。所以当拆到3时,就不需要往下拆了,因为3拆成1 2 乘积为2,不如本身大。拆成2以后也不拆了,2拆成1 1 乘积为1 。设定dp[i]表示i拆分后的最大乘积,那么dp[i]可以拆成什么,dp[ i ]可以是拆成 j i-j 的乘积(i-j不再拆了),也可以是 拆成j i-j 的乘积 i-j 还要拆;dp【i】若要拆,那么其最大乘积就是j*(i-j) 和j*dp【i-j】的最大值,还有一种情况就是dp【i】不拆,如果不拆的值要比拆开后的值还大,那么就可以不拆。
状态转移方程:
dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]))
初始化: 当输入为2 直接返回1 ;输入为3直接返回2;(这两种一定要拆)
输入为大于3的值则初始化dp[2]=2;dp[3]=3;(n大于3时,遇到2、3就不拆)
代码:
static int intApart(int n) {
int [] dp=new int [n+1];
// 当n为0、1,dp【i】=0
if(n==0||n==1) return 0;
if(n==2) return 1;
if(n==3) return 2;
//初始化 遇到2和3就不拆
dp[1]=1;
dp[2]=2;
dp[3]=3;
int result=0;
// 对于n先拆分成i n-i,再对i拆分
for(int i=4;i<=n;i++) {
dp[i]=0;
for(int j=2;j<i;j++) {
result=Math.max(j*(i-j),j*dp[i-j]);
dp[i]=Math.max(dp[i],result);
System.out.println(j+"*"+"("+i+"-"+j+")= "+j*(i-j)+" ---- "+j+"*"+"dp["+i+"-"+j+"]= "+j*dp[i-j]);
}
System.out.println("i ="+i+" "+dp[i]);
}
return dp[n];
}
96-不同的二叉树
给你一个整数
n
,求恰由n
个节点组成且节点值从1
到n
互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。示例 2:
输入:n = 3 输出:5示例 2:
输入:n = 1 输出:1
一个结点为n的二叉树,除去顶点后 左右子树一共n-1个结点,而左右子树又是一个二叉树。
dp[ i ]:表示结点为i的二叉树的种数,设左子树结点有j个,那么右子树结点有i-1-j个;左子树的种数乘以右子树的种数就是n个结点的二叉树的种数。
状态转移方程:
sum+=dp[j]*dp[i-1-j];
dp[i]=sum
初始化:i=1时,二叉树只有1种情况 dp[1]=1; i=0时,由于状态转移方程中有乘法,所以dp【0】不能等于0,否则后面的值会被覆盖成0,要将dp【0】=1;
代码:
int numTrees(int n) {
if(n==0) return 0;
if(n==1) return 1;
int [] dp=new int [n+1];
// 初始化
dp[0]=1;
dp[1]=1;
for(int i=2;i<=n;i++) {
int sum=0;
for(int j=0;j<i;j++) {
sum+=dp[j]*dp[i-1-j];
System.out.println("i: "+i+" j: "+j+" sum: " +sum);
}
dp[i]=sum;
System.out.println("i: "+i+" " +dp[i]);
}
// System.out.println(dp[n]);
return dp[n];
}
01背包--dp
参考视频:动态规划DP0-1背包_哔哩哔哩_bilibili
题目描述
小明有一个容量为V的背包
这天他去商场购物,商场一共有件物品,第件物品的体积为wi,价值为 vi。
小明想知道在购买的物品总体积不超过V的情况下所能获得的最大价值为多少,请你帮他算算
输入描述
输入第1行包含两个正整数 N,,表示商场物品的数量和小明的背包容量。
第2~N+行包含2个正整数w,,表示物品的体积和价值1<N <102,1<V < 103,1 wi,vi < 103
输出描述
输出一行整数表示小明所能获得的最大价值
输入输出样例输入:
4 8
2 3
3 4
4 5
5 8
输出12
01背包问题,特点是每个商品只能拿一次,每个商品都会选择拿或者不拿。
求背包容量为V,可选商品数为N时能获得的最大价值,在小明拿商品的过程中,背包的容量是随拿不拿某个商品而变化的。我们假定第N个商品能被小明拿,那么之后小明背包的容量是V-wArr[N],可选商品数变成了N-1,这样又变成了一个背包容量为V-wArr[N]、可选商品数为N-1的背包问题;假定第N个商品不能被小明拿(由于背包容量不够或者拿了第N个商品比没拿的价值低),那么问题就转变成了背包容量变成V、可选商品数变成N-1的背包问题。
经过分析,每个商品的选择使得背包的容量不断变化,商品可选数的减少让最开始的背包问题变成一个个新的背包问题。是典型的动态规划问题。
定义 : dp[i][w]表示当背包容量为w、可选商品数为i时的最大价值。
我们模拟一下小明不断往背包中装入商品的过程:
然后再将dp放入表格中 找出状态转移方程:(其实在画上面模拟商品被拿的过程中 已经看出了状态转移方程)
状态转移方程:
dp[i][w]=dp[i-1][w]; (wArr[i]>w)
dp[i][w]=Math.max(dp[i-1][w], dp[i-1][w-wArr[i]]+vArr[i]); (wArr[i]<w)
代码如下:
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//在此输入您的代码...
int n=scan.nextInt();//商品个数
int V=scan.nextInt();//包容量
int[] wArr=new int [n+1];//体积
int[] vArr=new int [n+1];//价值
for(int i=1;i<=n;i++) {
wArr[i]=scan.nextInt();
vArr[i]=scan.nextInt();
}
int[][]dp=new int [n+1][V+1];
// dp[i][w]表示背包容量为w 有i件商品可选时的最大价值
for( int i=0;i<=V;i++) {
dp[0][i]=0;
}
for(int i=1;i<=n;i++) {
dp[i][0]=0;//书包容量为0时的价值为0
for(int w=0;w<=V;w++){
if(wArr[i]>w) {
dp[i][w]=dp[i-1][w];
}
else {
dp[i][w]=Math.max(dp[i-1][w], dp[i-1][w-wArr[i]]+vArr[i]);
}
}
}
System.out.println(dp[n][V]);
scan.close();
}
滚动数组优化01背包
如果题目是个不需要考虑过程的背包问题,只需要考虑容量为V的背包可以装多少价值的商品这个结果,那么二维数组中只有一行值是对结果有用的,我们可以对二维数组压缩,用一维数组代替二维数组。原本的dp[ i ][ j ]表示 前i个商品可选,背包容量为j时的最大价值。还是用上面的例题
打表:
二维数组的状态转移方程:
dp[i][j]=dp[i-1][j]; (wArr[i]>j)
dp[i][j]=Math.max(dp[i-1][j], dp[i-1][j-wArr[i]]+vArr[i]); (wArr[i]<j)
也就是说dp[i][j]的值只与i-1时刻的值有关,与i-2、i-3无关,那么我们只需要一行数组来保留i-1时刻的值,然后利用i-1时刻的值得到i时刻的值,并更新在数组中。说来很抽象我们用上述例子来演示一下:
dp[ j ]定义为背包容量为j时的最大价值。
先遍历商品,再遍历背包。我们在有前i个商品可选的情况下,状态转移方程为
dp[ j ]=max(dp[ j ] , dp[ j-wArr[i]] + vArr[i]);
方程中的两个dp[j]的意义是不一样的
更加易懂:
i时刻的dp[j] = max{ (i-1)时刻的dp[j] , (i-1)时刻的dp[j-wArr[i]]+vArr[i] }
如果我们不选第i个商品,背包的容量不变 仍然是j,此时可选商品为前i-1个;如果我们要选第i个商品,背包就会装入第i个商品,背包容量变为j-wArr[i] ,此时可选商品为前i-1个。
初始化:开始遍历商品之前dp【j】不论j为何值,都没有商品可选,则dp【j】=0;
遍历顺序:先遍历商品,内循环遍历背包(背包逆序遍历)
为什么要逆序遍历呢?这是因为i时刻的dp[j]与i-1时刻的dp[j]有关,当我们刚开始遍历i时刻时,这时已经结束了i-1时刻的遍历,此时的dp数组中存放的是 前i-1个商品可选,容量为j的背包的最大价值,那么进行i时刻遍历时,如果对背包容量(j)顺序遍历的话:
i时刻的dp[j] = max{ (i-1)时刻的dp[j] , i时刻的dp[j-wArr[i]]+vArr[i] }
因为顺序遍历,要遍历到 i 时刻的 j 肯定要先遍历到 i时刻的 j-wArr[i],这样的话我们在dp状态转移方程中本来需要的 i-1时刻的 dp[j-wArr[i]]就被 i 时刻的dp[j-wArr[i]]覆盖了,那么后面的答案自然就错了。
将错就错,如果顺序遍历求dp【j】时有会将商品取多次,这就是完全背包问题了。
一维滚动数组优化01背包问题代码:
public class Main {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
//在此输入您的代码...
int n=scan.nextInt();//商品个数
int V=scan.nextInt();//包容量
int[] wArr=new int [n+1];//体积
int[] vArr=new int [n+1];//价值
for(int i=1;i<=n;i++) {
wArr[i]=scan.nextInt();
vArr[i]=scan.nextInt();
}
System.out.println("01背包:");
pack01(wArr, vArr, n, V);
System.out.println("滚动数组");
packGunDong(wArr, vArr, n, V);
scan.close();
}
// 滚动数组优化
static int packGunDong(int[]wArr,int[] vArr,int n,int V) {
int[] dp=new int [V+1] ;
// 初始化
for(int i=0;i<=V;i++) {
dp[i]=0;
}
for(int i=1;i<=n;i++) {
for(int j=V;j>=wArr[i];j--) {
dp[j]=Math.max(dp[j], dp[j-wArr[i]]+vArr[i]);
}
}
System.out.println(dp[V]);
return dp[V];
}
}
416-分割等和子集
给你一个 只包含正整数 的 非空 数组
nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。示例 1:
输入:nums = [1,5,11,5] 输出:true 解释:数组可以分割成 [1, 5, 5] 和 [11]
典型的01背包问题:每个元素只取1次。
我们假设有一个背包,将数组中的元素放进背包中,使得背包容量为sum/2 正好装满,则该数组可以分割。dp[i]:表示可取下标为前i个元素,背包容量为sum/2时的最大价值。正好装满的的判断条件是:dp[i]=sum/2;
状态转移方程与纯01背包一样。
代码:
static boolean canPartition(int[] nums) {
int sum=0;
for(int i=0;i<nums.length;i++) {
sum+=nums[i];
}
// System.out.println("sum: "+sum);
if(sum%2==1) return false;
if(nums.length==2) {
if(nums[0]==nums[1]) return true;
else return false;
}
int[][] dp=new int [nums.length][sum/2+1];
// 初始化 因为nums数组第一个元素下标为0,所以dp[i][j]需要初始化,
//当i=0时,只有下标为0的数可以选,那么背包容量大于nums【0】时,dp[i][j]=nums[0]
for(int j=nums[0];j<=sum/2;j++) {
dp[0][j]=nums[0];
}
for(int i=1;i<nums.length;i++) {
for(int j=1;j<=sum/2;j++) {
if(j<nums[i]) dp[i][j]=dp[i-1][j];
else
dp[i][j]=Math.max(dp[i-1][j], dp[i-1][j-nums[i]]+nums[i]);
}
}
// System.out.println(dp[nums.length-1][sum/2]);
if(dp[nums.length-1][sum/2]==sum/2) return true;
return false;
}
一维滚动数组优化:内层循环倒序遍历,保证每个元素最多拿1次
static boolean canPartition1(int[] nums) {
int sum=0;
for(int i=0;i<nums.length;i++) {
sum+=nums[i];
}
System.out.println("sum: "+sum);
if(sum%2==1) return false;
if(nums.length==2) {
if(nums[0]==nums[1]) return true;
else return false;
}
int [] dp=new int [sum/2+1];
for(int i=0;i<nums.length;i++) {
for(int j=sum/2;j>=nums[i];j--) {
dp[j]=Math.max(dp[j], dp[j-nums[i]]+nums[i]);
}
}
System.out.println(dp[sum/2]);
if(dp[sum/2]==sum/2) return true;
return false;
}
1049-最后一块石头的重量 II
有一堆石头,用整数数组
stones
表示。其中stones[i]
表示第i
块石头的重量。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为
x
和y
,且x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎;- 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回
0
。示例 1:
输入:stones = [2,7,4,1,8,1] 输出:1 解释: 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], 组合 2 和 1,得到 1,所以数组转化为 [1,1,1], 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
读懂题意,其实是袋子中的石头两两相撞,相消,最后剩下的重量。我们将所有石头分成两堆,这两堆的石头总和之差其实等于两堆石头两两相消最后剩下的重量。读懂题意后就知道了这也是一个背包问题,要让两堆之差最小,那么当背包重量为sum/2时若石头也能装满sum/2,说明两堆石头总和都是sum/2,两堆之差为0 此时最小,若两堆石头总和不相等,只能尽量让两堆石头的差变小。设定一个背包容量为sum/2的背包,让石头尽量装满背包,这时两堆石头的差最小。
代码:
static int lastStoneWeightII(int[] stones) {
int sum=0;
for(int i=0;i<stones.length;i++) {
sum+=stones[i];
}
// dp[j] 定义为背包容量j时,最大价值
int[] dp=new int [sum/2+1];
// 初始化:
for(int j=0;j>=stones[0];j++) {
dp[j]=stones[0];
}
for(int i=0;i<stones.length;i++) {
for(int j=sum/2;j>=stones[i];j--) {
dp[j]=Math.max(dp[j], dp[j-stones[i]]+stones[i]);
}
}
int min=sum-dp[sum/2]*2;
System.out.println(min);
return min;
}
494- 目标和
给你一个非负整数数组
nums
和一个整数target
。向数组中的每个整数前添加
'+'
或'-'
,然后串联起所有整数,可以构造一个 表达式 :
- 例如,
nums = [2, 1]
,可以在2
之前添加'+'
,在1
之前添加'-'
,然后串联起来得到表达式"+2-1"
。返回可以通过上述方法构造的、运算结果等于
target
的不同 表达式 的数目。示例 1:
输入:nums = [1,1,1,1,1], target = 3 输出:5 解释:一共有 5 种方法让最终目标和为 3 。 -1 + 1 + 1 + 1 + 1 = 3 +1 - 1 + 1 + 1 + 1 = 3 +1 + 1 - 1 + 1 + 1 = 3 +1 + 1 + 1 - 1 + 1 = 3 +1 + 1 + 1 + 1 - 1 = 3
将元素分成两堆,一堆放 + 符号的数,一堆放- 符号的数,设加法总和为left,减法总和为right ,那么left-right=result,又因为left+right=sum,所以left=(result+sum)/2;问题就变成了设定背包容量为left,在nums数组中选元素,将left背包凑满的方法有多少种。
代码:
static int findTargetSumWays(int[] nums, int target) {
int sum=0;
for(int i=0;i<nums.length;i++) {
sum+=nums[i];
}
int packLeft=(sum+target)/2;
// 若sum+target是一个奇数说明packLeft不是一个整数,说明凑不出packLeft
if((sum+target)%2==1||packLeft<0) return 0;
int[][]dp=new int [nums.length+1][packLeft+1];
// 初始化 背包容量为0时,也是一种装法
for(int j=0;j<=packLeft;j++) {
if(j==0) dp[0][j]=1;
else {
dp[0][j]=0;
}
}
// for(int i=0;i<=nums.length;i++) {
// dp[i][0]=1;
// }
// 不可这样初始化,因为会有【0,0,0,0,0,1】 1的情况,这时dp[2][0]=4,不等于0
for(int i=1;i<nums.length+1;i++) {
int num=nums[i-1];
for(int j=0;j<packLeft+1;j++) {
if(j<num) {
dp[i][j]=dp[i-1][j];
}
else {
dp[i][j]=dp[i-1][j]+dp[i-1][j-num];
}
System.out.println("i "+i+"j "+j+" "+dp[i][j]);
}
}
System.out.println(dp[nums.length][packLeft]);
return dp[nums.length][packLeft];
}
一维滚动数组优化
static int findTargetSumWays1(int[] nums, int target) {
int sum=0;
for(int i=0;i<nums.length;i++) {
sum+=nums[i];
}
int packLeft=(sum+target)/2;
// 若sum+target是一个奇数说明packLeft不是一个整数,说明凑不出packLeft
if((sum+target)%2==1||packLeft<0) return 0;
int[]dp=new int [packLeft+1];
// 初始化 背包容量为0时,也是一种装法
dp[0]=1;
// for(int i=0;i<=nums.length;i++) {
// dp[i][0]=1;
// }
// 不可这样初始化,因为会有【0,0,0,0,0,1】 1的情况,这时dp[2][0]=4,不等于0
for(int i=0;i<nums.length;i++) {
int num=nums[i];
for(int j=packLeft;j>=num;j--) {
dp[j]=dp[j]+dp[j-num];
System.out.println("i "+i+"j "+j+" "+dp[j]);
}
}
System.out.println(dp[packLeft]);
return dp[packLeft];
}
474-一和零
给你一个二进制字符串数组
strs
和两个整数m
和n
。请你找出并返回
strs
的最大子集的长度,该子集中 最多 有m
个0
和n
个1
。如果
x
的所有元素也是y
的元素,集合x
是集合y
的 子集 。示例 1:
输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 输出:4 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3
选取数组中的元素,元素中0总和不超过m,1总和不超过n的情况下,最多可取多少个元素。设定现在两个背包,一个是装0的背包,容量为m,一个是装1的背包,容量为n,在背包可装范围内,可取的最多元素个数。
代码:
static int findMaxForm(String[] strs, int m, int n) {
// 计算每个字符串中包含的0和1 的个数用nums0[] nums1[]存储
int [] nums0=new int [strs.length];
int [] nums1=new int [strs.length];
for(int i=0;i<strs.length;i++) {
int num0=0;
int num1=0;
String str=strs[i];
char[] stri=str.toCharArray();
for(int j=0;j<stri.length;j++) {
if(stri[j]=='0') num0++;
else if(stri[j]=='1') num1++;
}
nums0[i]=num0;
nums1[i]=num1;
}
// 打印字符串中的01个数
for(int i=0;i<strs.length;i++) {
System.out.println("第"+i+"个 0:"+nums0[i]+" 1:"+nums1[i]);
}
int[][] dp=new int[m+1][n+1];
// dp[i][j]表示有最多i个0 j个1的字符串最大个数
// 初始化
dp[0][0]=0;
// 用k去遍历字符串个数,模拟遍历商品
for(int k=0;k<strs.length;k++) {
for(int i=m;i>=nums0[k];i--) {
for(int j=n;j>=nums1[k];j--) {
dp[i][j]=Math.max(dp[i-nums0[k]][j-nums1[k]]+1, dp[i][j]);
}
}
}
System.out.println(dp[m][n]);
return dp[m][n];
}
完全背包理论
背包容量为sum,有n个元素可选,元素可重复选的问题。
完全背包的特点就是元素可重复选。
元素可重复选,那么在代码中如何体现呢,在状态转移方程中,选了第i个元素后,还是应该剩余i个元素可选,而不是像01背包中,剩余i-1个元素可选。
若是1维滚动数组,遍历背包时,不需要逆序遍历了,逆序遍历用于一维滚动数组优化01背包时,使得每个元素保证最多取一次;一维滚动数组优化完全背包时,正序遍历保证每个元素不止取一次。
若是选取所有元素中的某些元素(不看排序),那么是组合数问题,组合数问题不考虑元素拿进背包的顺序,所以先遍历元素,再遍历背包,这样考虑了第i个元素是否进入背包后,就考虑第i+1个元素是否进背包,那么i后面的元素永远在i后面进背包,不会有i后面的元素在i之前进背包的情况,刚好排除了元素的排序情况。
若是选取所有元素中的某些元素(看排序),那么是排列数问题,排列数问题考虑元素拿进背包的顺序,所以在遍历时先遍历背包,在遍历元素,这样考虑了第i个元素是否进入背包后,遍历完一遍元素,下次再遍历元素时,还会考虑第i-1个元素是否进入背包,那么就会出现i之前的元素进背包的情况。
下面我们用例子来学习
518-零钱兑换 II
给你一个整数数组
coins
表示不同面额的硬币,另给一个整数amount
表示总金额。请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回
0
。假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
示例 1:
输入:amount = 5, coins = [1, 2, 5] 输出:4 解释:有四种方式可以凑成总金额: 5=5 5=2+2+1 5=2+1+1+1 5=1+1+1+1+1
这就是一个完全背包的组合问题。
设定dp[ i ][ j ]:表示背包容量为j,前i个元素可选时可以凑成总金额的方式总数
状态转移方程:
我们来考虑dp[ i ][ j ]怎么来呢,在背包容量不够时,我们只能不取第i个元素;在背包容量够时,我们就会考虑取不取第i元素呢,若取 则就还剩前 i 个元素可取,背包容量变成了j-nums[ i ],此时的状态跟dp[ i ][ j ]时的状态的关系是:dp[ i ][ j ]=dp[ i ][ j -nums[ i ]];若不取第i个元素,则就还剩前i-1个元素可取,背包容量不变,此时的状态跟dp[ i ][ j ]时的关系是:dp[ i ][ j ]=dp[ i -1][ j ];求的是背包容量为j,前i个元素可选时可以凑成总金额的方式总数,那么dp[ i ][ j ]可以由取第i个得来,也可以由不取第i个得来,这是两种情况,求其和。
代码:
static int change(int amount, int[] coins) {
int[][]dp=new int [coins.length+1][amount+1];
// 初始化
dp[0][0]=1;//0元钱0的背包也是一种装法
for(int i=1;i<=coins.length;i++) {
int num=coins[i-1];
for(int j=0;j<=amount;j++) {
dp[i][j]=dp[i-1][j];
if(j>=num) dp[i][j]=dp[i-1][j]+ dp[i][j-num];
}
}
System.out.println(dp[coins.length][amount]);
return dp[coins.length][amount];
}
一维滚动数组优化:
static int change2(int amount, int[] coins) {
int[]dp=new int [amount+1];
// 初始化
dp[0]=1;//0元钱0的背包也是一种装法
for(int i=0;i<coins.length;i++) {
for(int j=0;j<=amount;j++) {
if(j>=coins[i]) dp[j]=dp[j]+ dp[j-coins[i]];
}
}
// System.out.println(dp[amount]);
return dp[amount];
}
377-组合总和 Ⅳ
给你一个由 不同 整数组成的数组
nums
,和一个目标整数target
。请你从nums
中找出并返回总和为target
的元素组合的个数。题目数据保证答案符合 32 位整数范围。
示例 1:
输入:nums = [1,2,3], target = 4 输出:7 解释: 所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1) 请注意,顺序不同的序列被视作不同的组合。
排序问题跟组合问题的不同之处就是元素是否排序,我们只需要在组合问题的基础上改变一下遍历顺序,具体原因请看上述完全背包基础。
代码:
static int combinationSum4_2(int[] nums, int target) {
int[] dp = new int[target + 1];
// 初始化
dp[0] = 1;
for (int i = 0; i <= target; i++) {
for (int j = 1; j <= nums.length; j++) {
int num = nums[j - 1];
if (i >= num) {
dp[i] = dp[i - num]+dp[i];
}
System.out.println("i:"+i+" j:"+j+" "+dp[i]);
}
}
int result = dp[target];
System.out.println(result);
return result;
}
322-零钱兑换
给你一个整数数组
coins
,表示不同面额的硬币;以及一个整数amount
,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回
-1
。你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5] , amount = 11 输出: 解释:11 = 5 + 5 + 1
每种硬币可以取多次,完全背包问题特点。题目相当于装满一个容量amount的背包,取得元素的最少个数。
设dp【i】是背包容量是i 装满的最少所需硬币数
若j当前物品的体积大于背包容量,那么不可以装入背包,背包容量不变,那么dp【i】不变。
若j当前物品的体积小于背包容量,那么考虑当前物品可否装进背包,决定当前物品是否装进背包的条件是:若装入当前物品后的硬币数(dp[i-coins[j]])小于不装当前物品的硬币数(dp[i]),则装入当前物品;若大于,则不装如当前物品。
状态转移方程:
dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);
注意:由于状态转移方程中用到了求dp数组中某两个数的最小值,那么需要将dp数组初始化为一个最大定值,不能初始化为0.若初始化为0,在min判断时,0将会覆盖所有的值。最后得到的结果dp【amount】若为初始的最大定值,说明dp【amount】不合法,返回-1。
代码:
static int coinChange(int[] coins, int amount) {
int []dp=new int[amount+1];
// dp[][]表示背包容量 i 时的最少所需硬币个数
// 1.不取当前硬币,2.取当前硬币 那么个数+1
Arrays.fill(dp, amount+1);
dp[0]=0;
for(int i=1;i<amount+1;i++) {
for(int j=0;j<coins.length;j++) {
if(i>=coins[j])
dp[i]=Math.min(dp[i],dp[i-coins[j]]+1);
}
}
int result=dp[amount]==amount+1?-1:dp[amount];
System.out.println(result);
return result;
}
279-完全平方数
给你一个整数
n
,返回 和为n
的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,
1
、4
、9
和16
都是完全平方数,而3
和11
不是。示例 1:
输入:n =12
输出:3 解释:12 = 4 + 4 + 4
分析题目,所有的小于n的完全平方数相当于可选的物品,可以重复选,完全背包问题。n由完全平方数凑成,求完全平方数的最少数量;相当于背包容量为n,小于n的完全平方数都可重复取得情况下,求装满背包的所需最少元素数。
这样一看,与上一题并无差别,只是自己把完全平方数找出来作为放进背包的元素。
代码:
int[] nums=new int [n/2+1];
// nums数组存储完全平方数字
int index=0;
for(int i=1;i<=n;i++) {
for(int j=1;j<=n/2+1;j++) {
if(i==j*j)
{
nums[index]=i;
index++;
}
}
}
int [] dp=new int [n+1];
Arrays.fill(dp, n);
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++) {
for(int j=0;j<=index;j++) {
if(i>=nums[j]) dp[i]=Math.min(dp[i], dp[i-nums[j]]+1);
}
}
System.out.println(dp[n]);
return dp[n];
上述方法是将所需元素先全部找出来放进数组,还原完全背包问题的模型,这样需要进行两次 两层for循环。如果我们一边找出完全平方数,一遍进行背包填充的循环这样可以只进行一次 两层for循环。
代码:
static int numSquares_2(int n) {
int[] nums=new int [n/2+1];
// nums数组存储完全平方数字
int index=0;
int [] dp=new int [n+1];
Arrays.fill(dp, n);
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++) {
for(int j=1;j*j<=i;j++) {
dp[i]=Math.min(dp[i], dp[i-j*j]+1);
}
}
System.out.println(dp[n]);
return dp[n];
}
139-单词拆分
给你一个字符串
s
和一个字符串列表wordDict
作为字典。请你判断是否可以利用字典中出现的单词拼接出s
。注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接
dp【i】表示长度是i的字符串是否可以被凑成 。完全背包问题,s作为背包,s的长度作为背包容量。比较难想的是如何实现让单词作为元素放进背包。我们用下标来解决,外层for循环遍历背包容量(字符串s长度),内层for循环遍历字符串s的每一个下标j,如果遇到s字符串的 j 到 i 段的单词在单词数组中,并且s字符串0到 j 段的字符串也可以被拼接,说明s字符串可以被拼接。s字符串的0到 j 段是否可拼接,又相当于长为j的字符串,是否可以拼接。
状态转移方程:
if(dp[j]==true&&wordDict.contains(s.substring(j, i)) ) {
dp[i]=true;
}
注意 :java中截取字符串:subString()方法来进行字符串截取 ,左闭右开;arr.contains(str),方法用于查找arr数组中是否有str。
代码:
static boolean wordBreak(String s, List<String> wordDict) {
// 抽象成一个完全背包的排列问题
int length=s.length();
boolean [] dp=new boolean [length+1];
// dp【i】表示长度是i的字符串是否可以被凑成
// 用dp[0]来表示空字符串时
// subString()方法来进行字符串截取 ,左闭右开
dp[0]=true;
for(int i=1;i<length+1;i++) {
for(int j=0;j<i;j++) {
if(dp[j]==true&&wordDict.contains(s.substring(j, i)) ) {
dp[i]=true;
}
System.out.println("i:"+i+" "+dp[i]);
}
}
System.out.println(dp[length]);
return dp[length];
}
198-打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:[1,2,3,1] 输出:4 解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 偷窃到的最高金额 = 1 + 3 = 4 。
设定dp[ i ] 为前i个元素可选,可取的最大价值 。
dp【i】来源:
1.取当前第i个,那么第i-1个不可取,dp【i】=dp[i-2]+nums[i]
2.不取第i个,则可以考虑取第i-1个 dp[i]=dp[i-1];
状态方程 dp[i]=Math.max(dp[i-1], dp[i-2]+nums[i-1]);
注意: 初始化时考虑到状态转移方程中需要用到dp【i-2】,那么起码要初始化dp【0】 dp【1】 dp【2】
代码:
static int rob(int[] nums) {
int [] dp=new int [nums.length+1];
// dp[i]表示前i个元素可取时,所取的最大值
// dp【i】来源:1.取当前第i个,那么第i-1个不可取,dp【i】=dp[i-2]+nums[i];
// 2.不取第i个,则可以考虑取第i-1个 dp[i]=dp[i-1];
// 初始化时考虑到状态转移方程中需要用到dp【i-2】,那么起码要初始化dp【0】 dp【1】 dp【2】
if(nums.length==0) return 0;
if(nums.length==1) return nums[0];
if(nums.length==2) return nums[0]>nums[1]?nums[0]:nums[1];
dp[0]=0;
dp[1]=nums[0];
dp[2]=nums[0]>nums[1]?nums[0]:nums[1];
for(int i=3;i<=nums.length;i++) {
// 考虑第i个,下标为i-1;
dp[i]=Math.max(dp[i-1], dp[i-2]+nums[i-1]);
}
System.out.println(dp[nums.length]);
return dp[nums.length];
213-打家劫舍 II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
示例 1:
输入:nums = [2,3,2] 输出:3 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
将数组元素围成一个圈,不可取相邻元素。通常在一个圈里面我们不好分析问题,那么我们就将圈切开,从数组的首 尾切开,如果小偷偷首,那么一定不可偷尾,如果偷尾,那么一定不可偷首;这样问题就变成了,两个数组(一个不含尾,一个不含首) 小偷分别偷取两数组不相邻的元素,偷不含尾数组得到的最大价值为value1,偷不含首数组得到的最大价值为value2,小偷偷这一圈房屋可获得的最大价值取value1 、value2的最大值
设定dp【i】为数组前i个可偷的最大价值
代码:
static int rob(int[] nums) {
if(nums.length==0) return 0;
if(nums.length==1) return nums[0];
if(nums.length==2) return nums[0]>nums[1]?nums[0]:nums[1];
// 将圈切开,若考虑首,就不考虑尾;若考虑尾部,就不考虑首部 或者尾部首部都不考虑,这样首位一定不会相邻被偷
int [] dp_L=new int [nums.length+1];
int [] dp_R=new int [nums.length+1];
// 只考虑首,不考虑尾 初始化:
dp_L[0]=0;
dp_L[1]=nums[0];
dp_L[2]=nums[0]>nums[1]?nums[0]:nums[1];
// 考虑首部,则下标从0-nums.length-2,即从第1个到第nums.length -1个
for(int i=3;i<=nums.length-1;i++) {
dp_L[i]=Math.max(dp_L[i-1], dp_L[i-2]+nums[i-1]);
}
// 只考虑尾部,不考虑首部 初始化:
dp_R[0]=0;
dp_R[1]=nums[1];
dp_R[2]=nums[1]>nums[2]?nums[1]:nums[2];
// 考虑首部,则下标从1-nums.length-1,即从第2个到第nums.length 个
for(int i=3;i<=nums.length-1;i++) {
dp_R[i]=Math.max(dp_R[i-1], dp_R[i-2]+nums[i]);
}
int result=dp_L[nums.length-1]>dp_R[nums.length-1]?dp_L[nums.length-1]:dp_R[nums.length-1];
System.out.println(result);
return result;
}
我们可以将计算偷一个数组的最大价值封装成函数,简化代码:
static int dividNums(int []nums) {
if(nums.length==1) return nums[0];
int [] nums1=new int [nums.length-1];//不要头
for(int i=1;i<nums.length;i++) {
nums1[i-1]=nums[i];
}
int sum1=rob(nums1);
int [] nums2=new int [nums.length-1];//不要尾巴
for(int i=0;i<nums.length-1;i++) {
nums2[i]=nums[i];
}
int sum2=rob(nums2);
int [] nums3=new int [nums.length-2];//不要尾 tou
for(int i=1;i<nums.length-1;i++) {
nums3[i-1]=nums[i];
}
int sum3=rob(nums3);
int max=sum1;
if(sum2>max) max=sum2;
if(sum3>max) max=sum3;
System.out.println("max:"+max);
return max;
}
static int rob(int[] nums) {
int [] dp=new int [nums.length+1];
if(nums.length==0) return 0;
if(nums.length==1) return nums[0];
if(nums.length==2) return Math.max(nums[0], nums[1]);
dp[0]=0;
dp[1]=nums[0];
dp[2]=Math.max(nums[0], nums[1]);
for(int i=3;i<=nums.length;i++) {
int num=nums[i-1];
dp[i]=Math.max(dp[i-1], dp[i-2]+num);
// 背包大小不是定值,无需判断第第i个是否可以装下
System.out.println(" i "+i+" "+dp[i]);
}
System.out.println(dp[nums.length]);
return dp[nums.length];
}
此题与上一题198题的共同特点是,都需要去计算小偷偷数组不相邻元素的最大价值;区别在于此题数组元素为成了一个圈,小偷不可同时偷首尾。解题过程中,数组首尾相连的题目不好分析,我们可以把圈切开,按照不偷相邻元素原则,分成两种情况,一是偷首不偷尾,二是偷尾不偷首;两种情况对应两个新数组传入rob函数计算小偷偷数组不相邻元素的最大价值。
121- 买卖股票的最佳时机
给定一个数组
prices
,它的第i
个元素prices[i]
表示一支给定股票第i
天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回
0
。示例 1:
输入:[7,1,5,3,6,4] 输出:5 解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
题目求数组中下标大的数-下标小的数 可得的最大差值。其实这个题用贪心也能做出来,我们这里还是用dp来引入买卖股票这一系列问题。买卖股票一系列问题可以通用dp的解法。
首先要定义dp【i】的含义。动态规划中最重要的就是dp【i】的定义和状态转移方程,本质就是题目中状态的转变。
那么dp【i】从哪里得来呢,我们考虑题目中股票的状态,第i天的股票只有在我手上和不在我手上两种状态,即持有和不持有两种状态。现在出现了两个维度的元素,一个是第i天,一个是股票持有与不持有,所以我们应该定义二维数组,dp【i】【j】,i表示第i天,j表示股票持有与不持有,那么持有与不持有只有两个状态,我们直接用0 、1 来表示。
dp【i】【0】表示第i天持有股票的最大利润。
dp【i】【1】表示第i天不持有股票的最大利润。
现在dp的定义完成了我们来找状态转移方程,题中需要求的是最后一天(最后一天股票已经卖出)可以获得的最大利润,相当于dp【nums.length-1】【1】(从第0天开始,第0天考虑下标为0的股票)。
dp【i】【0】怎么得来:
1.第i天以前就持有,则dp[i][0]延续dp[i-1][0]的状态;
2.第i天以前不持有则现金流为0,第i天才买入,则需要花费prices[i]元钱买入 ,0-prices【i】;
dp【i】【1】怎么得来:
1.第i天以前就不持有,则dp[i][1]延续dp[i-1][1]的状态;
2.第i天以前持有则现金流为dp[i-1][0],第i天才不持有 卖出,则可以收获prices[i]元钱,dp[i-1][0]+prices【i】;
初始化时需注意:
dp[0][0]=0-prices[0];
dp[0][1]=0;
代码:
public int maxProfit(int[] prices) {
// 由于每天的股票只能选择卖或买,且你只有一次买和一次卖的机会,那么买必定在卖之前,假定你买之前现金流为0元
// dp[i][0]表示第i的持有股票 dp[i][1]表示第i天不持有股票
int [][] dp=new int [prices.length][2];
// 初始化
dp[0][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], 0-prices[i]);
dp[i][1]=Math.max(dp[i-1][1], dp[i-1][0]+prices[i]);
}
int profit=Math.max(dp[prices.length-1][0], dp[prices.length-1][1]);
// System.out.println(profit);
return profit;
}