学习恋上数据结构与算法的记录,本篇主要内容是动态规划
动态规划(Dynamic Programming)
动态规划,简称DP,是求解最优化问题的一种常用策略
●通常的使用套路(一步一步优化)
①暴力递归(自顶向下,出现了重叠子问题)
②记忆化搜索(自顶向下)
③递推(自底向上)
动态规划的常规步骤
动态规划中的“动态”可以理解为是“会变化的状态”
①定义状态(状态是原问题、子问题的解)
✓比如定义dp(i) 的含义
②设置初始状态(边界)
✓比如设置dp(0) 的值
③确定状态转移方程
✓比如确定dp(i) 和dp(i–1) 的关系
动态规划的一些相关概念
来自维基百科的解释:Dynamic Programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems, solving each of those subproblems just once, and storingtheir solutions
①将复杂的原问题拆解成若干个简单的子问题
②每个子问题仅仅解决1次,并保存它们的解
③最后推导出原问题的解
可以用动态规划来解决的问题,通常具备2个特点
●最优子结构(最优化原理):通过求解子问题的最优解,可以获得原问题的最优解
●无后效性
✓某阶段的状态一旦确定,则此后过程的演变不再受此前各状态及决策的影响(未来与过去无关)
✓在推导后面阶段的状态时,只关心前面阶段的具体状态值,不关心这个状态是怎么一步步推导出来的
无后效性
从起点(0, 0)走到终点(4, 4)一共有多少种走法?只能向右、向下走
假设dp(i, j) 是从(0, 0)走到(i, j)的走法
✓dp(i, 0) = dp(0, j) = 1
✓dp(i, j) = dp(i, j–1) + dp(i–1, j)
●无后效性
✓推导dp(i, j) 时只需要用到dp(i, j–1)、dp(i–1, j) 的值
✓不需要关心dp(i, j–1)、dp(i–1, j) 的值是怎么求出来的
●有后效性
如果可以向左、向右、向上、向下走,并且同一个格子不能走2 次
有后效性dp(i, j) 下一步要怎么走,还要关心上一步是怎么来的
✓也就是还要关心dp(i, j–1)、dp(i–1, j) 是怎么来的?
练习1 –找零钱
leetcode_322_零钱兑换:https://leetcode-cn.com/problems/coin-change/
假设有25分、20分、5分、1分的硬币,现要找给客户41分的零钱,如何办到硬币个数最少?此前用贪心策略得到的并非是最优解(贪心得到的解是5 枚硬币)
假设dp(n) 是凑到n 分需要的最少硬币个数
如果第1 次选择了25 分的硬币,那么dp(n) = dp(n–25) + 1
如果第1 次选择了20 分的硬币,那么dp(n) = dp(n–20) + 1
如果第1 次选择了5 分的硬币,那么dp(n) = dp(n–5) + 1
如果第1 次选择了1 分的硬币,那么dp(n) = dp(n–1) + 1
所以dp(n) = min { dp(n–25), dp(n–20), dp(n–5), dp(n–1) } + 1
找零钱–暴力递归
类似于斐波那契数列的递归版,会有大量的重复计算,时间复杂度较高
static int coins1(int n) {
if(n<1) return Integer.MAX_VALUE;
if(n==1 || n==5 || n==20 || n==25) return 1;
int min1 = Math.min(coins1(n -1), coins1(n-5));
int min2 = Math.min(coins1(n -20), coins1(n-25));
return Math.min(min1, min2)+1;
}
找零钱–记忆化搜索
/**
* 找零钱–记忆化搜索(自顶向下的调用)
*/
static int coins2(int n) {
if(n<1) return -1;
int dp[] = new int [n+1];
int[] faces = {1,5,20,25};
for(int face : faces) {
if(n < face) break;
dp[face] = 1;
}
return coins2(n, dp);
}
static int coins2(int n, int[] dp) {
if(n < 1) return Integer.MAX_VALUE;
if(dp[n] == 0) {
int min1 = Math.min(coins2(n - 25, dp), coins2(n - 20, dp));
int min2 = Math.min(coins2(n - 5, dp), coins2(n - 1, dp));
dp[n] = Math.min(min1, min2)+1;
}
return dp[n];
}
找零钱–递推
static int coins3(int n) {
if(n <1) return -1;
int[] dp = new int[n+1];
for (int i = 1; i <= n; i++) {
int min = dp[i-1];
if (i >= 5) min = Math.min(dp[i - 5], min);
if (i >= 20) min = Math.min(dp[i - 20], min);
if (i >= 25) min = Math.min(dp[i - 25], min);
dp[i] = min + 1;
}
return dp[n];
}
找零钱–通用实现
/**
* 找零钱–通用实现
*/
static int coins5(int n, int[] faces) {
if(n <1 || faces == null || faces.length ==0) return -1;
int dp[] = new int [n+1];
for (int i = 1; i <= n; i++) {
int min = Integer.MAX_VALUE;
for (int face : faces) {
if(i < face) continue;
int v = dp[i - face];
if(v < 0 || v >= min) continue;
min = v;
}
if(min == Integer.MAX_VALUE) {
dp[i] = -1;
}else {
dp[i] = min +1;
}
}
return dp[n];
}
请输出找零钱的具体方案(具体是用了哪些面值的硬币)
/**
* 输出找零钱的具体方案(具体是用了哪些面值的硬币)
*/
static int coins4(int n) {
if(n <1) return -1;
int dp[] = new int[n+1];
// faces[i]是凑够i分时最后那枚硬币的面值
int[] faces = new int [dp.length];
for (int i = 1; i <= n; i++) {
int min = dp[i-1];
faces[i] = 1;
if(i >= 5 && dp[i-5]<min) {
min = dp[i -5];
faces[i] = 5;
}
if (i >= 20 && dp[i - 20] < min) {
min = dp[i - 20];
faces[i] = 20;
}
if (i >= 25 && dp[i - 25] < min) {
min = dp[i - 25];
faces[i] = 25;
}
dp[i] = min +1;
print(faces, i);
}
return dp[n];
}
static void print(int[] faces, int n) {
System.out.print("[" + n + "] = ");
while (n > 0) {
System.out.print(faces[n] + " ");
n -= faces[n];
}
System.out.println();
}
练习2 –最大连续子序列和
给定一个长度为n 的整数序列,求它的最大连续子序列和
比如–2、1、–3、4、–1、2、1、–5、4 的最大连续子序列和是4 + (–1) + 2 + 1 = 6
●状态定义
假设dp(i) 是以nums[i] 结尾的最大连续子序列和(nums是整个序列)
✓以nums[0] –2 结尾的最大连续子序列是–2,所以dp(0) = –2
✓以nums[1] 1 结尾的最大连续子序列是1,所以dp(1) = 1
✓以nums[2] –3 结尾的最大连续子序列是1、–3,所以dp(2) = dp(1) + (–3) = –2✓以nums[3] 4 结尾的最大连续子序列是4,所以dp(3) = 4
✓以nums[4] –1 结尾的最大连续子序列是4、–1,所以dp(4) = dp(3) + (–1) = 3
✓以nums[5] 2 结尾的最大连续子序列是4、–1、2,所以dp(5) = dp(4) + 2 = 5
✓以nums[6] 1 结尾的最大连续子序列是4、–1、2、1,所以dp(6) = dp(5) + 1 = 6
✓以nums[7] –5 结尾的最大连续子序列是4、–1、2、1、–5,所以dp(7) = dp(6) + (–5) = 1
✓以nums[8] 4 结尾的最大连续子序列是4、–1、2、1、–5、4,所以dp(8) = dp(7) + 4 = 5
●状态转移方程
如果dp(i –1) ≤0,那么dp(i) = nums[i]
如果dp(i –1) > 0,那么dp(i) = dp(i –1) + nums[i]
●初始状态
dp(0) 的值是nums[0]
●最终的解
最大连续子序列和是所有dp(i) 中的最大值max { dp(i) },i ∈ [0, nums.length)
最大连续子序列和–动态规划–实现
static int maxSubArray1(int[] nums) {
if(nums == null || nums.length ==0) return 0;
int [] dp = new int[nums.length];
int max = dp[0] = nums[0];//初始状态
for (int i = 1; i < dp.length; i++) {
int prev = dp[i-1];
if(prev >0) {
dp[i] = prev + nums[i];//如果dp(i –1) > 0,那么dp(i) = dp(i –1) + nums[i]
}else {
dp[i] = nums[i];// 如果dp(i –1) ≤0,那么dp(i) = nums[i]
}
max = Math.max(max, dp[i]);
}
return max;
}
最大连续子序列和–动态规划–优化实现
/**
* 最大连续子序列和–动态规划–优化实现
* 优化空间
*/
static int maxSubArray2(int[] nums) {
if(nums == null || nums.length ==0) return 0;
int dp = nums[0];//初始状态
int max = dp;
for (int i = 1; i < nums.length; i++) {
if(dp >0) {
dp = dp +nums[i];
}else {
dp = nums[i];
}
max = Math.max(max, dp);
}
return max;
}
练习3 –最长上升子序列(LIS)
最长上升子序列(最长递增子序列,Longest Increasing Subsequence,LIS)
leetcode_300_最长上升子序列:https://leetcode-cn.com/problems/longest-increasing-subsequence/
给定一个无序的整数序列,求出它最长上升子序列的长度(要求严格上升)
比如[10,2, 2, 5, 1, 7, 101, 18] 的最长上升子序列是[2, 5, 7, 101]、[2, 5, 7, 18],长度是4
最长上升子序列–动态规划–状态定义
假设数组是nums,[10, 2, 2, 5, 1, 7, 101, 18]
dp(i) 是以nums[i] 结尾的最长上升子序列的长度,i∈ [0, nums.length)
✓以nums[0] 10 结尾的最长上升子序列是10,所以dp(0) = 1
✓以nums[1] 2 结尾的最长上升子序列是2,所以dp(1) = 1
✓以nums[2] 2 结尾的最长上升子序列是2,所以dp(2) = 1
✓以nums[3] 5 结尾的最长上升子序列是2、5,所以dp(3) = dp(1) + 1 = dp(2) + 1 = 2
✓以nums[4] 1 结尾的最长上升子序列是1,所以dp(4) = 1
✓以nums[5] 7 结尾的最长上升子序列是2、5、7,所以dp(5) = dp(3) + 1 = 3
✓以nums[6] 101 结尾的最长上升子序列是2、5、7、101,所以dp(6) = dp(5) + 1 = 4
✓以nums[7] 18 结尾的最长上升子序列是2、5、7、18,所以dp(7) = dp(5) + 1 = 4
最长上升子序列的长度是所有dp(i) 中的最大值max { dp(i) },i∈ [0, nums.length)
最长上升子序列–动态规划–状态转移方程
遍历j∈ [0, i),当nums[i] > nums[j]
✓nums[i] 可以接在nums[j] 后面,形成一个比dp(j) 更长的上升子序列,长度为dp(j) + 1
✓dp(i) = max { dp(i), dp(j) + 1 }
当nums[i] ≤ nums[j]
✓nums[i] 不能接在nums[j] 后面,跳过此次遍历(continue)
状态的初始值
dp(0) = 1
所有的dp(i) 默认都初始化为1
最长上升子序列–动态规划–实现
static int lengthOfLIS1(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
int max = dp[0] = 1;
for (int i = 1; i < dp.length; i++) {
dp[i] = 1;
for (int j = 0; j < i; j++) {
if(nums[i] <= nums[j]) continue;
dp[i] = Math.max(dp[i], dp[j]+1);
}
max = Math.max(max, dp[i]);
}
return max;
}
最长上升子序列–二分搜索–思路
●思路(假设数组是nums,也就是最初的牌数组)
top[i] 是第i个牌堆的牌顶,len是牌堆的数量,初始值为0
●遍历每一张牌num
✓利用二分搜索找出num 最终要放入的牌堆位置index
✓num 作为第index 个牌堆的牌顶,top[index] = num
✓如果index 等于len,相当于新建一个牌堆,牌堆数量+1,也就是len++
最长上升子序列–二分搜索–实现
static int lengthOfLIS(int[] nums) {
if(nums == null || nums.length ==0) return 0;
// 牌堆的数量
int len = 0;
// 牌顶数组
int top[] = new int[nums.length];
// 遍历所有的牌
for(int num : nums) {
int begin =0;
int end = len;
while(begin<end) {
int mid = (begin + end ) >> 1;
if(num <= top[mid]) {
end = mid;
}else {
begin = mid + 1;
}
}
// 覆盖牌顶
top[begin] = num;
// 检查是否要新建一个牌堆
if(begin == len) len++;
}
return len;
}
练习4 –最长公共子序列(LCS)
最长公共子序列(Longest Common Subsequence,LCS)
leetcode_1143_最长公共子序列:https://leetcode-cn.com/problems/longest-common-subsequence/
求两个序列的最长公共子序列长度
[1, 3, 5, 9, 10] 和[1, 4, 9, 10] 的最长公共子序列是[1, 9, 10],长度为3
ABCBDAB 和BDCABA 的最长公共子序列长度是4,可能是
✓ABCBDAB和BDCABA > BDAB
✓ABCBDAB和BDCABA > BDAB
✓ABCBDAB和BDCABA > BCAB
✓ABCBDAB 和BDCABA> BCBA
最长公共子序列–思路
假设2 个序列分别是nums1、nums2
i∈ [1, nums1.length]
j∈ [1, nums2.length]
假设dp(i, j) 是【nums1前i 个元素】与【nums2前j个元素】的最长公共子序列长度
dp(i, 0)、dp(0, j) 初始值均为0
如果nums1[i–1] = nums2[j–1],那么dp(i, j) = dp(i–1, j–1) + 1
如果nums1[i–1] ≠nums2[j–1],那么dp(i, j) = max { dp(i–1, j), dp(i, j–1) }
最长公共子序列–递归实现
public static void main(String[] args) {
int len = lcs(new int[] {1, 3, 5, 9, 10}, new int[] {1, 4, 9, 10});
System.out.println(len);
}
static int lcs1(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0)
return 0;
if (nums2 == null || nums2.length == 0)
return 0;
return lcs1(nums1, nums1.length, nums2, nums2.length);
}
/**
* 求nums1前i个元素和nums2前j个元素的最长公共子序列长度 递归实现--重复递归调用
* */
static int lcs1(int[] nums1, int i, int[] nums2, int j) {
if (i == 0 || j == 0)
return 0;
if (nums1[i - 1] != nums2[j - 1]) {
return Math.max(lcs1(nums1, i - 1, nums2, j), lcs1(nums1, i, nums2, j - 1));
}
return lcs1(nums1, i - 1, nums2, j - 1) + 1;
}
最长公共子序列–非递归实现
static int lcs2(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) return 0;
if (nums2 == null || nums2.length == 0) return 0;
int dp[][] = new int[nums1.length + 1][nums2.length + 1];
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;
}else {
dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]);
}
}
}
return dp[nums1.length][nums2.length];
}
最长公共子序列–非递归实现–滚动数组
可以使用滚动数组优化空间复杂度
static int lcs3(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) return 0;
if (nums2 == null || nums2.length == 0) return 0;
int dp[][] = new int[2][nums2.length + 1];
for (int i = 1; i <= nums1.length; i++) {
int row = i & 1;//%2的位运算优化
int prevRow = (i-1)&1;
for (int j = 1; j <= nums2.length; j++) {
if(nums1[i-1] == nums2[j-1]) {
dp[row][j] = dp[prevRow][j-1]+1;
}else {
dp[row][j] = Math.max(dp[prevRow][j], dp[row][j-1]);
}
}
}
return dp[nums1.length & 1][nums2.length];
}
最长公共子序列–非递归实现–一维数组
可以将二维数组优化成一维数组,进一步降低空间复杂度
static int lcs4(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) return 0;
if (nums2 == null || nums2.length == 0) return 0;
int dp[] = new int[nums2.length + 1];
for (int i = 1; i <= nums1.length; i++) {
int cur=0;
for (int j = 1; j <= nums2.length; j++) {
int leftTop = cur;
cur=dp[j];
if(nums1[i-1] == nums2[j-1]) {
dp[j] = leftTop+1;
}else {
dp[j] = Math.max(dp[j], dp[j-1]);
}
}
}
return dp[nums2.length];
}
最长公共子序列–非递归实现–一维数组再次优化
降低空间复杂度
static int lcs(int[] nums1, int[] nums2) {
if (nums1 == null || nums1.length == 0) return 0;
if (nums2 == null || nums2.length == 0) return 0;
int[] rowsNums = nums1,colsNums = nums2;
if(nums1.length<nums2.length) {
colsNums = nums1;
rowsNums = nums2;
}
int[]dp = new int[colsNums.length+1];
for (int i = 1; i <= rowsNums.length; i++) {
int cur=0;
for (int j = 1; j <= colsNums.length; j++) {
int leftTop = cur;
cur = dp[j];
if(rowsNums[i-1] == colsNums[j-1]) {
dp[j]=leftTop+1;
}else {
dp[j] = Math.max(dp[j], dp[j-1]);
}
}
}
return dp[colsNums.length];
}
练习5 –最长公共子串
最长公共子串(Longest Common Substring)
子串是连续的子序列
求两个字符串的最长公共子串长度
ABCBA 和BABCA 的最长公共子串是ABC,长度为3
最长公共子串–思路
假设2 个字符串分别是str1、str2
i∈ [1, str1.length]
j∈ [1, str2.length]
假设dp(i, j) 是以str1[i–1]、str2[j–1] 结尾的最长公共子串长度
dp(i, 0)、dp(0, j) 初始值均为0
如果str1[i–1] = str2[j–1],那么dp(i, j) = dp(i–1, j–1) + 1
如果str1[i–1] ≠str2[j–1],那么dp(i, j) = 0
最长公共子串的长度是所有dp(i, j) 中的最大值max { dp(i, j) }
最长公共子串–实现
static int lcs1(String str1, String str2) {
if(str1 == null || str2 == null) return 0;
char[] cs1 = str1.toCharArray();
if(cs1.length == 0) return 0;
char[] cs2 = str2.toCharArray();
if(cs2.length == 0) return 0;
int dp[][] = new int[cs1.length+1][cs2.length+1];
int max = 0;
for (int i = 1; i <= cs1.length; i++) {
for (int j = 1; j <= cs2.length; j++) {
if(cs1[i-1] != cs2[j-1]) continue;
dp[i][j] = dp[i-1][j-1] +1;
max = Math.max(max, dp[i][j]);
}
}
return max;
}
最长公共子串–一维数组实现
static int lcs2(String str1, String str2) {
if (str1 == null || str2 == null) return 0;
char[] chars1 = str1.toCharArray();
if (chars1.length == 0) return 0;
char[] chars2 = str2.toCharArray();
if (chars2.length == 0) return 0;
char[] rowsChars = chars1, colsChars = chars2;
if (chars1.length < chars2.length) {
colsChars = chars1;
rowsChars = chars2;
}
int[] dp = new int[colsChars.length + 1];
int max = 0;
for (int row = 1; row <= rowsChars.length; row++) {
int cur = 0;
for (int col = 1; col <= colsChars.length; col++) {
int leftTop = cur;
cur = dp[col];
if(chars1[row-1] != chars2[col-1]) {
dp[col] = 0;
}else {
dp[col] = leftTop+1;
max = Math.max(dp[col], max);
}
}
}
return max;
}
最长公共子串–一维数组倒序优化实现
static int lcs(String str1, String str2) {
if (str1 == null || str2 == null) return 0;
char[] chars1 = str1.toCharArray();
if (chars1.length == 0) return 0;
char[] chars2 = str2.toCharArray();
if (chars2.length == 0) return 0;
char[] rowsChars = chars1, colsChars = chars2;
if (chars1.length < chars2.length) {
colsChars = chars1;
rowsChars = chars2;
}
int[] dp = new int[colsChars.length + 1];
int max = 0;
for (int row = 1; row <= rowsChars.length; row++) {
for (int col = colsChars.length; col >=1; col--) {
if(chars1[row-1] != chars2[col-1]) {
dp[col] =0;
}else {
dp[col] = dp[col-1]+1;
max = Math.max(max, dp[col]);
}
}
}
return max;
}
练习6 –0-1背包
有n 件物品和一个最大承重为W 的背包,每件物品的重量是𝑤i、价值是𝑣i
在保证总重量不超过W 的前提下,选择某些物品装入背包,背包的最大总价值是多少?
注意:每个物品只有1 件,也就是每个物品只能选择0 件或者1 件
假设values 是价值数组,weights是重量数组
编号为k 的物品,价值是values[k],重量是weights[k],k ∈ [0, n)
假设dp(i, j) 是最大承重为j、有前i 件物品可选时的最大总价值,i∈ [1, n],j∈ [1, W]
dp(i, 0)、dp(0, j) 初始值均为0
如果j< weights[i–1],那么dp(i, j) = dp(i–1, j)
如果j≥ weights[i–1],那么dp(i, j) = max { dp(i–1, j), dp(i–1, j–weights[i–1]) + values[i–1] }
0-1背包–实现
static int maxValue1(int[] values, int[] weights, int capacity) {
if(values == null || values.length == 0) return 0;
if(weights == null || weights.length == 0) return 0;
if(weights.length != values.length) return 0;
if(capacity <=0) return 0;
int[][] dp = new int [values.length+1][capacity+1];
for (int i = 1; i <= values.length; i++) {
for (int j = 1; j <= capacity; j++) {
if(j<weights[i-1]) {
dp[i][j] = dp[i-1][j];
}else {
dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weights[i-1]]+values[i-1]);
}
}
}
return dp[values.length][capacity];
}
0-1背包–非递归实现
0-1背包–非递归实现–一维数组
dp(i, j) 都是由dp(i–1, k) 推导出来的,也就是说,第i 行的数据是由它的上一行第i–1 行推导出来的
因此,可以使用一维数组来优化
另外,由于k ≤ j ,所以j 的遍历应该由大到小,否则导致数据错乱
static int maxValue(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >= weights[i-1]; j--) {
dp[j] = Math.max(dp[j], values[i-1]+dp[j-weights[i-1]]);
}
}
return dp[capacity];
}
0-1背包–非递归实现–一维数组优化
观察二维数组表,得出结论:j 的下界可以从1 改为weights[i–1]
static int maxValue(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >= weights[i-1]; j--) {
dp[j] = Math.max(dp[j], values[i-1]+dp[j-weights[i-1]]);
}
}
return dp[capacity];
}
0-1背包–恰好装满
有n 件物品和一个最大承重为W 的背包,每件物品的重量是𝑤i、价值是𝑣i
在保证总重量恰好等于W 的前提下,选择某些物品装入背包,背包的最大总价值是多少?
注意:每个物品只有1 件,也就是每个物品只能选择0 件或者1 件
dp(i, j) 初始状态调整
dp(i, 0) = 0,总重量恰好为0,最大总价值必然也为0
dp(0, j) = –∞(负无穷),j ≥ 1,负数在这里代表无法恰好装满
0-1背包–恰好装满–实现
static int maxValueExactly(int[] values, int[] weights, int capacity) {
if (values == null || values.length == 0) return 0;
if (weights == null || weights.length == 0) return 0;
if (values.length != weights.length || capacity <= 0) return 0;
int[] dp = new int[capacity + 1];
for (int j = 1; j <= capacity; j++) {
dp[j] = Integer.MIN_VALUE;
}
for (int i = 1; i <= values.length; i++) {
for (int j = capacity; j >=weights[i - 1]; j--) {
dp[j] = Math.max(dp[j], values[i-1]+ dp[j-weights[i-1]]);
}
}
return dp[capacity] < 0 ? -1 : dp[capacity];
}