动态规划(DP)
(JAVA版本)
博文中对于第四部分背包问题还会单独拎出来讲解一期,这里只是部分。
1 DP
1.1 递归和动规关系
- 递归是一种程序的实现方式:函数的自我调用
- 动态规划:是一种解决问 题的思想,大规模问题的结果,是由小规模问 题的结果运算得来的。动态规划可用递归来实现(Memorization Search)。
1.2 DP的使用场景
满足三个条件:最优子结构(子问题的最优解是原问题的最优解),无后效性,重复子问题。
简单来说就是满足以下条件之一:
- 求最大/最小值(Maximum/Minimum )
- 求是否可行(Yes/No )
- 求可行个数(Count(*) )
- 满足不能排序或者交换(Can not sort / swap )
1.3 四点要素
- 状态 State - 灵感,创造力,存储小规模问题的结果
- 方程 Function - 状态之间的联系,怎么通过小的状态,来算大的状态
- 初始化 Initialization - 最极限的小状态是什么, 起点
- 答案 Answer - 最大的那个状态是什么,终点
1.4 常见四种类型
- 矩阵类型 Matrix DP (10%)
- 序列类型 Sequence (40%)
- 两个序列类型 Two Sequences DP (40%)
- 背包问题类型 Backpack (10%)
2 矩阵类型 Matrix DP (10%)
2.1 (lee-64) 最小路径和
给定一个包含非负整数的 m*n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。
思路:由于路径的方向只能是向下或向右,因此网格的第一行的每个元素只能从左上角元素开始向右移动到达,网格的第一列的每个元素只能从左上角元素开始向下移动到达,此时的路径是唯一的,因此每个元素对应的最小路径和即为对应的路径上的数字总和。
对于不在第一行和第一列的元素,可以从其上方相邻元素向下移动一步到达,或者从其左方相邻元素向右移动一步到达,元素对应的最小路径和等于其上方相邻元素与其左方相邻元素两者对应的最小路径和中的最小值加上当前元素的值。由于每个元素对应的最小路径和与其相邻元素对应的最小路径和有关,因此可以使用动态规划求解。
创建二维数组 dp,与原始网格的大小相同,dp[i][j] 表示从左上角出发到 (i,j) 位置的最小路径和。
显然,dp[0][0]=grid[0][0]。对于 dp 中的其余元素,通过以下状态转移方程计算元素值。
当 i>0 且 j=0 时,dp[i][0]=dp[i−1][0]+grid[i][0]。
当 i=0 且 j>0 时,dp[0][j]=dp[0][j−1]+grid[0][j]。
当 i>0 且 j>0 时,dp[i][j]=min(dp[i−1][j],dp[i][j−1])+grid[i][j]。
最后得到 dp[m−1][n−1] 的值即为从网格左上角到网格右下角的最小路径和。
public class Matrix_minPathSum {
/*
* 思路:DP
* 1.state: f[x][y]从起点走到 x,y 的最短路径
* 2.function: f[x][y] = min(f[x-1][y], f[x][y-1]) + A[x][y]
* 3.initialize: f[0][0] = A[0][0]; f[i][0] = sum(0,0 -> i,0); f[0][i] = sum(0,0 -> 0,i)
* 4.answer: f[n-1][m-1]
* 自顶向下
* 时间复杂度 O(m*n),其中 m 和 n 分别是网格的行数和列数。需要对整个网格遍历一次,计算 dp 的每个元素的值。
* 空间复杂度 O(m*n)
*/
public int minPathSum(int[][] grid) {
if(grid == null || grid.length == 0 || grid[0].length==0) {
return 0;
}
int m = grid.length;
int n = grid[0].length;
if(n==1) {
return grid[0][0];
}
int[][] dp = new int[m][n];
dp[0][0] = grid[0][0];
//处理边界
for(int i = 1;i <m;i++) {
dp[i][0] = grid[i][0] + dp[i-1][0];
}
for(int i = 1;i <n;i++) {
dp[0][i] = grid[0][i] + dp[0][i-1];
}
for(int i = 1;i <m ; i++) {
for(int j = 1; j<n ; j++) {
dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
}
}
return dp[m-1][n-1];
}
}
2.2 (lee-120) 三角形最小路径和
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。 相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
思路:用 f[i][j] 表示从三角形顶部走到位置 (i,j) 的最小路径和。这里的位置 (i,j) 指的是三角形中第 iii 行第 j 列(均从 0 开始编号)的位置。由于每一步只能移动到下一行「相邻的节点」上,因此要想走到位置 (i,j),上一步就只能在位置 (i−1,j−1) 或者位置 (i−1,j)。我们在这两个位置中选择一个路径和较小的来进行转移,状态转移方程为:
f[i][j]=min(f[i−1][j−1],f[i−1][j])+c[i][j] ,其中 c[i][j] 表示位置 (i,j) 对应的元素值。
(1)递归DFS — 超出时间限制
/*
* 1.递归DFS ---超出时间限制
*/
public int minimumTotal(List<List<Integer>> triangle) {
return dfs(triangle,0,0);
}
private int dfs(List<List<Integer>> triangle, int i, int j) {
if(i == triangle.size()) {
return 0;
}
return Math.min(dfs(triangle, i+1, j), dfs(triangle, i+1, j+1)) + triangle.get(i).get(j);
}
(2)记忆化搜索 — 超出时间限制
/*
* 2.优化DFS ---超出时间限制
* 缓存已经被计算的值(称为:记忆化搜索 本质上:动态规划)
* 自顶向下
*/
int row;
Integer[][] memo;
public int minimumTotal1(List<List<Integer>> triangle) {
row = triangle.size();
memo = new Integer[row][row];
return helper(0,0,triangle);
}
private int helper(int level, int c, List<List<Integer>> triangle) {
if(memo[level][c] != null) {
return memo[level][c];
}
if(level == row-1) {
return memo[level][c] = triangle.get(level).get(c);
}
return Math.min(helper(level+1, c, triangle), helper(level+1, c+1, triangle)) + triangle.get(level).get(c);
}
(3)dp 二维数组
/*
* 3.dp 二维数组
* 从底部开始转移,到顶部结束
* 时间复杂度O(N^2)
* 空间复杂度O(N^2)
* 状态转移方程:f(i,j) = Math.min( f(i+1,j) , f(i+1,j+1) ) + triangle[i][j]
*/
public int minimumTotal2(List<List<Integer>> triangle) {
int n = triangle.size();
int[][] dp = new int[n+1][n+1];
for(int i =n-1;i >=0;i--) {
for(int j = 0;j <= i;j++) {
dp[i][j] = Math.min(dp[i+1][j], dp[i+1][j+1]) + triangle.get(i).get(j);
}
}
return dp[0][0];
}
(4)dp ----优化空间到一维数组
/*
* 4.dp ----优化空间到一维数组
* 从底部开始转移,到顶部结束
* 时间复杂度O(N^2)
* 空间复杂度O(N)
* 思路:不要想的太复杂,它是一个三角形,一个点的相邻的两个点中取最小值然后加上它自己的值,一层一层寻找最小路径
*/
public int minimumTotal3(List<List<Integer>> triangle) {
int row = triangle.size();
int[] dp = new int[row+1];
for(int level = row-1;level >= 0;level--) {
for(int i = 0;i <= level;i++) {
dp[i] = Math.min(dp[i], dp[i+1])+triangle.get(level).get(i);
}
}
return dp[0];
}
2.3 (lee-62) 不同路径
一个机器人位于一个 m*n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?
输入:m = 3, n = 2
输出:3
解释:从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下
(1)dp 二维数组
/*
* 思路:DP
* 1.state: f(i,j)表示从左上角走到(i,j)的路径数量 0 <= i < m, 0 <= j < n
* 2.function: f(i,j) = f(i-1,j) + f(i,j-1) 每一步只能从向下或者向右移动一步
* 3.initialize: f(0,0) = 1 从左上角走到左上角有一种方法
* 4.answer: f(m-1,n-1)
* 自顶向下
* 时间复杂度 O(m*n)
* 空间复杂度 O(m*n)
*/
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
dp[0][0] = 1;
//边界条件
for(int i = 0;i <m;i++) {
dp[i][0] = 1;
}
for(int i = 0;i <n;i++) {
dp[0][i] = 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];
}
(2) dp 优化空间
/*
* dp--优化空间
* 注意到: f(i,j)仅与第i行和第i−1行的状态有关,因此我们可以使用滚动数组代替代码中的二维数组,使空间复杂度降低为 O(n)
* 动态方程优化为: dp[i]= dp[i] + dp[i-1]
*/
public int uniquePaths1(int m, int n) {
int[] dp = new int[n];
Arrays.fill(dp, 1);
for(int i = 1;i <m;i++) {
for(int j = 1;j <n;j++) {
dp[j] = dp[j] + dp[j-1];
}
}
return dp[n-1];
}
2.4 (lee-63) 不同路径2
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?网格中的障碍物和空位置分别用 1 和 0 来表示。
现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?
输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右
/*
* 思路:DP
* 如果第一个格点 obstacleGrid[0,0] 是 1,说明有障碍物,那么机器人不能做任何移动,返回结果 0。
* 否则,如果 dp[0,0] 是 0,我们初始化这个值为 1 然后继续算法。
* 遍历第一行,如果有一个格点初始值为 1 ,说明当前节点有障碍物,没有路径可以通过,设值为 0 ;否则设这个值是前一个节点的值 dp[i,j] = dp[i,j-1]。
* 遍历第一列,如果有一个格点初始值为 1 ,说明当前节点有障碍物,没有路径可以通过,设值为 0 ;否则设这个值是前一个节点的值 dp[i,j] = dp[i-1,j]。
* 现在,从 dp[1,1] 开始遍历整个数组,如果某个格点初始不包含任何障碍物,就把值赋为上方和左侧两个格点方案数之和 dp[i,j] = dp[i-1,j] + dp[i,j-1]。
* 如果这个点有障碍物,设值为 0 ,这可以保证不会对后面的路径产生贡献。
*/
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
if(obstacleGrid[0][0] == 1) {
return 0;
}
obstacleGrid[0][0] = 1;
//初始化边界
for(int i = 1;i < m;i++) {
obstacleGrid[i][0] = (obstacleGrid[i][0] == 0 && obstacleGrid[i-1][0] == 1) ? 1 : 0;
}
for(int i = 1;i <n;i++) {
obstacleGrid[0][i] = (obstacleGrid[0][i] == 0 && obstacleGrid[0][i-1] == 1) ? 1 : 0;
}
for(int i = 1;i< m;i++) {
for(int j = 1;j <n;j++) {
if(obstacleGrid[i][j] == 0) {
obstacleGrid[i][j] = obstacleGrid[i-1][j] +obstacleGrid[i][j-1];
}else {
obstacleGrid[i][j] = 0;
}
}
}
return obstacleGrid[m-1][n-1];
}
2.5 (lee-221) 最大正方形
在一个由 0 和 1 组成的二维矩阵内,找到只包含 1 的最大正方形,并返回其面积。
输入:
[[“1”,“0”,“1”,“0”,“0”],
[“1”,“0”,“1”,“1”,“1”],
[“1”,“1”,“1”,“1”,“1”],
[“1”,“0”,“0”,“1”,“0”]]
输出:4
(1) 暴力
/**
* 1.暴力
* 思路:遍历矩阵中的每个元素,每次遇到 1,则将该元素作为正方形的左上角;
* 确定正方形的左上角后,根据左上角所在的行和列计算可能的最大正方形的边长(正方形的范围不能超出矩阵的行数和列数),在该边长范围内寻找只包含 1 的最大正方形;
* 每次在下方新增一行以及在右方新增一列,判断新增的行和列是否满足所有元素都是 1。
*/
public int maximalSquare1(char[][] matrix) {
int maxSide = 0;
if(matrix == null || matrix.length == 0 || matrix[0].length == 0){
return 0;
}
int r = matrix.length;
int c = matrix[0].length;
for(int i = 0;i < r;i++) {
for(int j = 0;j < c;j++) {
if(matrix[i][j] == '1') {
maxSide = Math.max(maxSide, 1); // 遇到一个 1 作为正方形的左上角
int curMaxSide = Math.min(r-i, c-j); // 计算可能的最大正方形边长
for(int k = 1;k < curMaxSide;k++) { // 判断新增的一行一列是否均为 1
boolean flag = true;
if(matrix[i+k][j+k] == '0') {
break;
}
for(int m = 0;m <k;m++) {
if(matrix[i+k][j+m] == '0' || matrix[i+m][j+k] == '0' ){
flag = false;
break;
}
}
if(flag) {
maxSide = Math.max(maxSide, k+1);
}else {
break;
}
}
}
}
}
return maxSide * maxSide;
}
(2) DP
/**
* 2.DP
* 思路:
* 1.state: f(i,j)表示以(i,j)为右下角,且只包含1的正方形的边长最大值
* 2.function: f(i,j) = min(f(i-1,j) , f(i-1,j-1), f(i,j-1)) +1 ;
* 3.initialize: f(0,0) = 0 如果该位置是0,则当前位置不可能在由1组成的正方形中
* 4.answer: maxSide^2
* 时间复杂度 O(m*n) 矩阵的行数和列数
* 空间复杂度 O(m*n)
* @param matrix
* @return
*/
public int maximalSquare(char[][] matrix) {
int maxSide = 0;
if(matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return 0;
}
int r = matrix.length;
int c = matrix[0].length;
int[][] dp = new int[r][c];
for(int i = 0;i <r;i++) {
for(int j = 0;j < c;j++) {
if(matrix[i][j] == '1') {
if(i == 0 || j == 0) { //边界条件,如果i,j中至少有一个为0,则右下角走到右下角,最大边长只能是1
dp[i][j] = 1;
}else { //dp(i,j)的值由其上方/左方和左上方的三个相邻位置的dp值决定。
dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1])) + 1;
}
maxSide = Math.max(maxSide, dp[i][j]);
}
}
}
return maxSide * maxSide;
}
2.6 (lee-1277) 统计全为1的正方形子矩阵
与上题最大正方形相似,可以先理解这个题目,再做上题。
给你一个 m * n 的矩阵,矩阵中的元素不是 0 就是 1,请你统计并返回其中完全由 1 组成的 正方形 子矩阵的个数。
输入:matrix =
[
[0,1,1,1],
[1,1,1,1],
[0,1,1,1]
]
输出:15
解释:
边长为 1 的正方形有 10 个。
边长为 2 的正方形有 4 个。
边长为 3 的正方形有 1 个。
正方形的总数 = 10 + 4 + 1 = 15.
/**
* DP
* @param matrix
* @return
*/
public int countSquares(int[][] matrix) {
int res = 0;
if(matrix == null || matrix.length == 0 || matrix[0].length == 0) {
return 0;
}
int r = matrix.length;
int c = matrix[0].length;
int[][] dp = new int[r][c];
for(int i = 0;i < r;i++) {
for(int j = 0;j < c;j++) {
if(i==0 || j==0) {
dp[i][j] = matrix[i][j];
}else if(matrix[i][j] == 1) {
dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1],dp[i-1][j-1])) + 1;
}else {
dp[i][j] = 0;
}
res += dp[i][j];
}
}
return res;
}
2.7 (lee-JZ47)礼物的最大价值
在一个 m*n 的棋盘的每一格都放有一个礼物,每个礼物都有一定的价值(价值大于 0)。你可以从棋盘的左上角开始拿格子里的礼物,并每次向右或者向下移动一格、直到到达棋盘的右下角。给定一个棋盘及其上面的礼物的价值,请计算你最多能拿到多少价值的礼物?
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 12
解释: 路径 1→3→5→2→1 可以拿到最多价值的礼物
public class MaxValue {
/*
* state:f[i][j]表示从左上角走到i,j的礼物的价值
* function: f[i][j] = Max(f[i][j-1], f[i-1][j])
* initial:f[i][0] = sum(0,0 -> i,0); f[0][i] = sum(0,0 -> 0,i)
* answer:f[m-1][n-1]
*/
public int maxValue(int[][] grid) {
int m = grid.length;
int n = grid[0].length;
for(int i = 1;i <m;i++) {
grid[i][0] += grid[i-1][0];
}
for(int i = 1;i <n;i++) {
grid[0][i] += grid[0][i-1];
}
for(int i = 1;i < m;i++) {
for(int j = 1;j < n;j++) {
grid[i][j] += Math.max(grid[i-1][j], grid[i][j-1]);
}
}
return grid[m-1][n-1];
}
}
2.8 (lee-JZ63)股票的最大利润
假设把某股票的价格按照时间先后顺序存储在数组中,请问买卖该股票一次可能获得的最大利润是多少?
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
(1)暴力
/*
* 1.暴力
* 找出给定数组中两个数字之间的最大差值
* 时间复杂度:O(n^2)
* 空间复杂度:O(1)
*/
public int maxProfit(int[] prices) {
int n = prices.length;
int maxProfit = 0;
if(prices == null || n == 0) {
return 0;
}
for(int i = 0;i < n-1;i++) {
for(int j = i+1;j < n;j++) {
int profit = prices[j] - prices[i];
if(profit > maxProfit) {
maxProfit = profit;
}
}
}
return maxProfit;
}
(2)DP
/* 2.DP
* S:f[i]表示在第i天买入的时候的最大利润
* F:f[i] = Max(prices[i] - cost, f[i-1])
* I:f[0] = 0
* A:f[n-1]
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
public int maxProfit1(int[] prices) {
int n = prices.length;
if(prices == null || n == 0) {
return 0;
}
int[] dp = new int[n];
dp[0] = 0;
int cost = prices[0]; //历史最低价
for(int i = 1;i <n;i++) {
dp[i] = Math.max(prices[i] - cost, dp[i-1]); //注意是prices[i]
cost = Math.min(cost, prices[i]);
}
return dp[n-1];
}
(3)优化空间
/* 3.DP
* 用一个变量记录一个历史最低价格 minPrice,在第 i 天卖出股票能得到的利润就是 prices[i] - minPrice
* 时间复杂度:O(n)
* 空间复杂度:O(1)
*/
public int maxProfit2(int[] prices) {
int n = prices.length;
if(prices == null || n == 0) {
return 0;
}
int minPrices = prices[0];
int maxProfit = 0;
for(int i = 1;i < n;i++) {
if(minPrices > prices[i]) {
minPrices = prices[i];
}else if(prices[i] - minPrices > maxProfit) {
maxProfit = prices[i] - minPrices;
}
}
return maxProfit;
}
3 序列类型 Sequence (40%)
3.1 (lee-53) 最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
可以将上题中求最大乘积改为求最大和即为本题。
(1) 暴力
/**
* 最大子序和
* 1.暴力
* 通过,但是时间复杂度太高
* @param nums
* @return
*/
public int maxSubArray(int[] nums) {
int max = Integer.MIN_VALUE; //初始值定义为理论上的最小值
int n = nums.length;
for(int i = 0;i < n;i++) {
int sum = 0;
for(int j = i;j < n; j++) {
sum += nums[j];
if(sum > max) {
max = sum;
}
}
}
return max;
}
(2) 一维DP
/**
* 2.DP 一维
* state:f(i)代表以第i个数结尾的连续子数组的最大和
* function: f(i) = Max(f(i-1) + nums[i], nums[i]);
* initial: f(0) = nums[0]
* ans: f(n)
* 时间复杂度:O(n)
* 空间复杂度:O(n)
*/
public int maxSubArray1(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
int res = nums[0];
for(int i = 1;i < n;i++) {
dp[i]= Math.max(nums[i], dp[i-1] + nums[i]);
res = Math.max(res, dp[i]);
}
return res;
}
(3) 优化空间
/**
* 3.维护一个变量
* 考虑到 f(i) 只和 f(i−1) 相关,于是我们可以只用一个变量 dp 来维护对于当前 f(i) 的 f(i−1)的值是多少
* 从而让空间复杂度降低到 O(1)
*/
public int maxSubArray2(int[] nums) {
int n = nums.length;
int dp = 0 ;
int res = nums[0];
for(int i = 1;i < n;i++) {
dp = Math.max(nums[i], dp + nums[i]);
res = Math.max(res, dp);
}
return res;
}
(4) 分治法
/**
* 4.分治
*/
public int maxSubArray3(int[] nums) {
int n = nums.length;
int res = partition(nums,0,n-1);
return res;
}
private int partition(int[] nums, int left, int right) {
if(left == right) {
return nums[left];
}
int mid = left + ((right-left) >> 1);
int leftSum = partition(nums, left, mid);
int rightSum = partition(nums, mid+1, right);
int midSum = findMax(nums,left,mid,right);
int res = Math.max(leftSum, rightSum);
res = Math.max(res, midSum);
return res;
}
private int findMax(int[] nums, int left, int mid, int right) {
int leftSum = Integer.MIN_VALUE;
int sum = 0;
for(int i = mid; i >= left; i--) {
sum += nums[i];
leftSum = Math.max(leftSum, sum);
}
int rightSum = Integer.MIN_VALUE;
sum = 0;
for(int i = mid+1 ;i <= right;i++) { //注意这里i = mid + 1,避免重复用到nums[i]
sum += nums[i];
rightSum = Math.max(rightSum,sum);
}
return leftSum + rightSum;
}
3.2 (lee-152) 乘积最大子数组
给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。
与(lee-53)最大子序和类似,但是不同的是:如果按照最大子序和的状态转移方程:
fmax(i)= Max{f(i−1) × nums[i], nums[i]}
则最大乘积数组表示的是以第i个元素结尾的乘积最大子数组的乘积可以考虑 **nums[i]**加入前面的 **fmax(i−1)**对应的一段,或者单独成为一段,这里两种情况下取最大值。
求出所有的 f(i) 之后选取最大的一个作为答案。
但是这样做是错误的,不满足最优子结构。如果 a={5,6,−3,4,−3},那么此时 f对应的序列是{5,30,−3,4,−3}按照前面的算法我们可以得到答案为 30,即前两个数的乘积;
而实际上答案应该是全体数字的乘积,因为当前位置的最优解未必是由前一个位置的最优解转移得到的。
因此,需要根据正负性进行讨论:
于是这里我们可以再维护一个 fmin(i),它表示以第 i 个元素结尾的乘积最小子数组的乘积。
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。
(1) 二维DP
/**
* 1.二维DP
* state:fmax(i),fmin(i)
* function:fmax(i) = Max(fmax(i-1) * nums[i] ,fmin(i-1) * nums[i], nums[i]);
* fmin(i) = Max(fmax(i-1) * nums[i] ,fmin(i-1) * nums[i] ,nums[i]);
* initial: f(0) = 0
* res: f(n)
*/
public int maxProduct(int[] nums) {
int n = nums.length;
int[][] dp = new int[n][2];
dp[0][0] = nums[0]; //最大值
dp[0][1] = nums[0]; //最小值
int res = nums[0];
for(int i = 1;i < n;i++) {
int a = dp[i-1][0] * nums[i];
int b = dp[i-1][1] * nums[i];
dp[i][0] = Math.max(nums[i], Math.max(a, b));
dp[i][1] = Math.min(nums[i], Math.min(a, b));
res = Math.max(res, dp[i][0]);
}
return res;
}
(2) 优化空间
/**
* 一维优化
* @param nums
* @return
*/
public int maxProduct1(int[] nums) {
int n = nums.length;
int max = nums[0]; //最大值
int min = nums[0]; //最小值
int res = nums[0];
for(int i = 1;i < n;i++) {
int a = max;
int b = min;
max = Math.max(nums[i], Math.max(a * nums[i], b * nums[i]));
min = Math.min(nums[i], Math.min(a * nums[i], b * nums[i]));
res = Math.max(res, max);
}
return res;
}
3.3 (lee-70) 爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶,每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
(1)一维DP
/*
* 思路:DP (斐波那契一样)
* 1.state:f(i)
* 2.function:f(i) = f(i-1)+f(i-2)
* 3.Initial:f(0) = 1,f(1) = 2
* 4.answer: f(n-1)
*/
public int climbStairs(int n) {
int[] dp = new int[n+1];
dp[0] = 1;
dp[1] = 2;
for(int i = 2;i < n;i++) {
dp[i] = dp[i-1] +dp[i-2];
}
return dp[n-1];
}
(2)空间优化
/*
* 优化
* 考虑到dp[i]只与 dp[i-1] 和 dp[i-2] 有关,
* 因此可以只用两个变量来存储dp[i-1]和dp[i-2],使得原来的O(N)空间复杂度优化为O(1)复杂度。
*/
public int climbStairs(int n) {
if(n <= 2) {
return n;
}
int pre1 = 1;
int pre2 = 2;
for(int i = 2;i <n;i++) {
int cur = pre1 + pre2;
pre1 = pre2;
pre2 = cur;
}
return pre2;
}
3.4 (lee-55) 跳跃游戏
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个位置。
输入:nums = [2,3,1,1,4] 输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。
输入:nums = [3,2,1,0,4] 输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。
(1)DP
/*
* 1.思路:DP
* state: dp[i] 表示能否跳跃到下标i的位置
* function: dp[i]
* initial:dp[0] = true
* return:dp[n-1]
*/
public boolean canJump(int[] nums) {
int n = nums.length;
boolean[] dp = new boolean[n];
dp[0] = true;
for(int i = 1;i < n;i++) {
for(int j = 0;j < i; j++) {
if(dp[j] && j + nums[j] >= i) {
dp[i] = true;
break;
}
}
}
return dp[n-1];
}
(2)贪心
/*
* 2.思路:贪心
* 依次遍历数组中的每一个位置,并实时维护最远可以到达的位置,对于当前遍历到的位置 x,
* 如果它在最远可以到达的位置的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,
* 因此我们可以用 x+nums[x] 更新最远可以到达的位置。
* 在遍历的过程中,如果最远可以到达的位置 >= 数组中的最后一个位置,那就说明最后一个位置可达,直接返回 True
* 反之,如果在遍历结束后,最后一个位置仍然不可达,返回 False 。
* 时间复杂度O(n)
* 空间复杂度O(1)
*/
public boolean canJump(int[] nums) {
int n = nums.length;
int rightMost = 0;
for(int i = 0;i <n;i++) {
if(i <= rightMost) { // 如果i > rightMost,说明到不了rightMost,不更新
rightMost = Math.max(rightMost, i+nums[i]);
if(rightMost >= n-1) {
return true;
}
}
}
return false;
}
3.5 (lee-45) 跳跃游戏2
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。
输入: [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。
(1)DP
/*
* 1.思路:DP
* state: dp[i] 表示跳跃到下标i的最小次数
* function: dp[i] = min(dp[i],dp[j]+1)
* initial:dp[0] = 0
* return:dp[n-1]
*/
public int jump(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for(int i = 1;i < n;i++) {
for(int j = 0;j < i;j++) {
if(j + nums[j] >= i) {
dp[i] = Math.min(dp[i], dp[j]+1);
}
}
}
return dp[n-1];
}
(2)贪心
/*
* 思路:典型的贪心算法,通过局部最优解得到全局最优解
* 每次在上次能跳到的范围(end)内选择一个能跳的最远的位置作为下次的起跳点 !
* 维护当前能够到达的最大下标位置,记为边界。从左到右遍历数组,到达边界时,更新边界并将跳跃次数增加 1 。
* 注意:在遍历数组时,我们不访问最后一个元素,这是因为在访问最后一个元素之前,我们的边界一定大于等于最后一个位置,否则就无法跳到最后一个位置了。
* 如果访问最后一个元素,在边界正好为最后一个位置的情况下,我们会增加一次「不必要的跳跃次数」,因此我们不必访问最后一个元素
* 时间复杂度O(n)
* 空间复杂度O(1)
*/
public int jump(int[] nums) {
int n = nums.length;
int end = 0; // 判断是否到达上一个最大边界
int maxPosition = 0; // 当前能跳到的最大位置
int step = 0; // 记录跳的次数
for(int i = 0 ;i < n-1;i++) {
maxPosition = Math.max(maxPosition, i+nums[i]); // 更新最大边界
if(i == end) { // 如果到达上一个最大边界,更新数据,说明从上个位置一步可以跳到这里
end = maxPosition;
step++;
}
}
return step;
}
3.6 (lee-300) 最长递增子序列
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
(1)DP
/**
* 思路:动态规划
* 在binarySearch中使用二分搜索的方法,这里使用DP
* state: f(i)为考虑前i个元素,以第i个数字(nums(i))结尾的最长上升子序列的长度
* function: f(i) = max(f(j)) +1, 0 <= j < i
* Initial: f(i) = 1;
* answer: max(f(i))
* 时间复杂度:O(n^2)
* 空间复杂度:O(n)
*/
public int lengOfLIS(int[] nums) {
int n = nums.length;
if(n==0) {
return 0;
}
int[] dp = new int[n];
dp[0] = 1;
int maxans = 1;
for(int i = 1;i <n;i++) {
dp[i] = 1;
for(int j = 0;j < i;j++) {
if(nums[i] > nums[j]) {
dp[i] = Math.max(dp[i], dp[j]+1);
}
}
maxans = Math.max(maxans, dp[i]);
}
return maxans;
}
(2)贪心 + 二分搜索
public int lengOfLIS(int[] nums) {
int n = nums.length;
if(nums==null || n==0) {
return 0;
}
int[] tails = new int[n]; //定义一个 tails 数组,其中 tails[i] 存储长度为 i + 1 的最长递增子序列的最后一个元素
int maxLength = 0;
for(int num:nums) {
int index = binarySearch(tails,maxLength,num);
tails[index] = num;
if(index == maxLength) {
maxLength++;
}
}
return maxLength;
}
private int binarySearch(int[] tails, int len, int key) {
int left = 0;
int right = key;
while(left < right) {
int mid = left+(right-left)/2;
if(tails[mid] == key) {
return mid;
}else if(tails[mid] > key) {
right = mid;
}else {
left = mid+1;
}
}
return left;
}
(3)(NC-91)
/**
* 给定数组arr,设长度为n,输出arr的最长递增子序列。(如果有多个答案,请输出其中字典序最小的)
* 思路:先求最大长度,再求字典序中最小
* 超时
*/
public int[] LIS (int[] arr) {
int n = arr.length;
if(n==0){
return new int[0];
}
int[] dp = new int[n];
dp[0] = 1;
int maxans = 1;
for(int i = 1;i <n;i++){
dp[1] = 1;
for(int j = 0;j <i;j++){
if(arr[i] >arr[j]){
dp[i] = Math.max(dp[i] , dp[j]+1) ;
}
}
maxans = Math.max(maxans,dp[i]);
}
int[] res = new int[maxans];
for(int i = n-1,j=maxans ; i>=0 ;i--){
if(dp[i]==j){
res[--j] = arr[i];
}
}
return res;
}
3.7 (lee-132) 分割回文串2
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。返回符合要求的最少分割次数。
输入:s = “aab”
输出:1
解释:只需一次分割就可将 s 分割成 [“aa”,“b”] 这样两个回文子串。
输入:s = “a”
输出:0
/**
* 思路:DP
*/
public int minCut(String s) {
int n = s.length();
int[] dp = new int[n];
//初始化
for(int i = 0;i <n;i++) {
dp[i] = i;
}
// 把第i个字符当做中心,向两边扩散,考虑每个中心
for(int i = 0;i <n;i++) {
int j = 0; // j 表示某一个方向扩展的个数
// 考虑奇数的情况
while(true) {
if(i-j < 0 || i+j > n-1) {
break;
}else if(s.charAt(i-j) == s.charAt(i+j)) {
if(i-j == 0) {
dp[i+j] = 0;
}else {
dp[i+j] = Math.min(dp[i+j], dp[i-j-1] +1);
}
}else {
break;
}
j++;
}
j = 1;
// 考虑偶数的情况
while(true) {
if(i-j+1 < 0 || i+j >n-1) {
break;
}else if(s.charAt(i-j+1) == s.charAt(i+j)) {
if(i-j+1 == 0) {
dp[i+j] = 0;
}else {
dp[i+j] = Math.min(dp[i+j], dp[i-j]+1);
}
}else {
break;
}
j++;
}
}
return dp[n-1];
}
4 两个序列类型 Two Sequences DP (40%)
4.1 (lee-1143) 最长公共子序列
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列。一个字符串的子序列是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
(1)暴力递归
/**
* 1.暴力递归
* 通过一部分测试用例,超时
*/
public int longestCommonSubsequence1(String text1, String text2) {
int m = text1.length();
int n = text2.length();
char[] c1 = text1.toCharArray();
char[] c2 = text2.toCharArray();
return recursion(c1,c2,m,n);
}
private int recursion(char[] c1, char[] c2, int m, int n) {
int res = 0;
if(m==0 || n==0) {
return 0;
}else if(c1[m-1] == c2[n-1]) {
res = recursion(c1, c2, m-1, n-1)+1;
}else {
res = Math.max(recursion(c1, c2, m-1, n), recursion(c1, c2, m, n-1));
}
return res;
}
(2)递归+记忆搜索
/**
* 2.递归+记忆搜索
* 可以通过
*/
int[][] memo;
public int longestCommonSubsequence2(String text1, String text2) {
int m = text1.length();
int n = text2.length();
char[] c1 = text1.toCharArray();
char[] c2 = text2.toCharArray();
memo = new int[m+1][n+1];
return recursion2(c1,c2,m,n);
}
private int recursion2(char[] c1, char[] c2, int m, int n) {
int res = 0;
if(memo[m][n] != 0){
return memo[m][n];
}
if(m==0 || n==0) {
return 0;
}else if(c1[m-1] == c2[n-1]) {
res = recursion2(c1, c2, m-1, n-1)+1;
}else {
res = Math.max(recursion2(c1, c2, m-1, n), recursion2(c1, c2, m, n-1));
}
memo[m][n] = res;
return res;
}
(3)DP
/**
* 3.思路:DP
* 时间复杂度:O(mn)
* 空间复杂度:O(mn)
*/
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m+1][n+1];
for(int i = 1;i <=m;i++) {
for(int j = 1;j <=n;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[m][n];
}
(4)DP2
/*
* NC给定两个字符串str1和str2,输出两个字符串的最长公共子序列。
* 如果最长公共子序列为空,则返回"-1"。目前给出的数据,仅仅会存在一个最长的公共子序列。
* 输入:"1A2C3D4B56","B1D23A456A"
* 返回值:"123456"
*/
public String LCS (String s1, String s2) {
int m = s1.length();
int n = s2.length();
if(m==0 || n==0){
return "-1";
}
int[][] dp = new int[m+1][n+1];
for(int i = 1;i <=m;i++){
for(int j = 1;j <=n;j++){
if(s1.charAt(i-1)==s2.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]);
}
}
}
StringBuffer res = new StringBuffer();
for(int i = m,j = n;dp[i][j] >=1;){
if(s1.charAt(i-1) == s2.charAt(j-1)){
res.append(s1.charAt(i-1)) ;
i--;
j--;
}else if(dp[i-1][j] >= dp[i][j-1]){
i--;
}else{
j--;
}
}
return res.length() > 0 ? res.reverse().toString() : "-1";
}
4.2 (lee-72) 编辑距离
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2所使用的最少操作数 。
你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符。
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)
思路:定义 dp[i][j]:代表 word1 中前 i 个字符,变换到 word2 中前 j 个字符,最短需要操作的次数。
然后需要考虑 word1 或 word2 一个字母都没有,即全增加/删除的情况,所以预留 dp[0][j] 和 dp[i][0]。
状态转移:
增:dp[i][j] = dp[i][j - 1] + 1
删:dp[i][j] = dp[i - 1][j] + 1
改:dp[i][j] = dp[i - 1][j - 1] + 1
按顺序计算,当计算 dp[i][j] 时,dp[i - 1][j] , dp[i][j - 1] , dp[i - 1][j - 1] 均已经确定了;
配合增删改这三种操作,需要对应的 dp 把操作次数加一,取三种的最小;
如果刚好这两个字母相同 word1[i - 1] = word2[j - 1] ,那么可以直接参考 dp[i - 1][j - 1] ,操作不用加一。
/**
* DP
* 思路:和最长公共子序列很类似,相等则不需要操作,否则取删除、插入、替换最小操作次数的值+1
* 时间复杂度:O(mn)
* 空间复杂度:O(mn)
*/
public int minDistance(String word1, String word2) {
if(word1==null || word2==null) {
return 0;
}
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m+1][n+1];
for(int i = 1;i <=m;i++) {
dp[i][0] = i;
}
for(int i = 1;i <=n;i++) {
dp[0][i] = i;
}
for(int i = 1;i <=m;i++) {
for(int j = 1;j <=n;j++) {
if(word1.charAt(i-1) == word2.charAt(j-1)) {
dp[i][j] = dp[i-1][j-1];
}else {
dp[i][j] = Math.min(dp[i-1][j-1], Math.min(dp[i-1][j], dp[i][j-1]))+1;
}
}
}
return dp[m][n];
}
5 背包问题类型 Backpack (10%)
5.1 0-1背包
(1)递归解法
(2)递归+记忆搜索 自上而下记忆法
(3)动态规划 自下而上填表法
(4)动态规划 优化空间
public class BP_01 {
int[] v = {0,2,4,3,7}; //物品体积 增加了一个补位数0,防止数组越界
int[] w = {0,2,3,5,5}; //物品价值
Integer[][] dp = new Integer[5][11];
int[] newdp = new int[11];
/*
* 1.递归解法
*/
private int bestValue(int i,int c) {
int res = 0;
if(i==0 || c==0) {
res = 0;
}else if(w[i] > c) {
res = bestValue(i-1, c);
}else {
res = Math.max(bestValue(i-1, c), bestValue(i-1, c-w[i]) + v[i]);
}
return res;
}
/*
* 2.递归+记忆搜索 自上而下记忆法
* 增加了一个数组用来存储计算的中间结果来减少重复计算
*/
private int bestValue_1(int i,int c) {
int res = 0;
if(dp[i][c] != null) {
return dp[i][c];
}
if(i==0 || c==0) {
res = 0;
}else if(w[i] > c) {
res = bestValue_1(i-1, c);
}else {
res = Math.max(bestValue_1(i-1, c), bestValue_1(i-1, c-w[i]) + v[i]);
}
dp[i][c] = res;
return res;
}
/*
* 3.动态规划 自下而上填表法
*/
private int bestValue_2(int i,int j) {
//初始化
for(int m = 0; m <= i; m++) {
dp[m][0] = 0;
}
for(int m = 0; m <= j;m++) {
dp[0][m] = 0;
}
//填表
for(int m = 1;m <= i;m++) {
for(int n = 1;n <= j;n++) {
if(n < w[m]) { //容量不够
dp[m][n] = dp[m-1][n];
}else { //容量足够
if(dp[m-1][n] > dp[m-1][n-w[m]] + v[m] ){ //不装时最优价值更大
dp[m][n] = dp[m-1][n];
}else {
dp[m][n] = dp[m-1][n-w[m]] + v[m];
}
}
}
}
return dp[i][j];
}
/*
* 4.动态规划
* 将空间复杂度从O(n*c)优化到了O(c)
* 空间优化的代价是,我们只能知道最终的结果,但无法再回溯中间的选择,也就是无法根据最终结果来找到我们要选的物品组合
*/
private int bestValue_3(int i,int j) {
for(int m = 0; m < v.length;m++) {
int ws = w[m];
int vs = v[m];
for(int n = j; n >= ws;n--) {
newdp[n] = Math.max(newdp[n], newdp[n-ws] + vs);
//System.out.print(newdp[n]);// 可以在这里输出中间结果
}
//System.out.println();
}
return newdp[newdp.length-1];
}
public static void main(String[] args) {
BP_01 bp = new BP_01();
int res = bp.bestValue(4, 10);
int res1 = bp.bestValue_1(4, 10);
int res2 = bp.bestValue_2(4, 10);
int res3 = bp.bestValue_3(4, 10);
System.out.println(res);
System.out.println(res1);
System.out.println(res2);
System.out.println(res3);
}
}
5.2 完全背包
(1)递归解法
(2)递归+记忆搜索 自顶向下
(3)动态规划
(4)动态规划 优化空间
public class BP_complete {
private static int[] w = {0,5,8};
private static int[] v = {0,5,7};
private int C = 10;
private Integer[][] dp = new Integer[w.length+1][C+1];
private int[] newdp = new int[C+1];
/*
* 1.递归解法
* 递推关系式:F(i,c) = max(F(i-1 , c-v[i]*k) + w[i]*k) ; 0 <= k*v[i] <=c
*
*/
public int bestValue(int i,int c) {
int res = 0;
if(i==0 || c==0) {
return res;
}else if(v[i] > c){
res = bestValue(i-1, c);
}else { // 可以装下
for(int k = 0;k * v[i] <= c;k++) { //取k个物品i,取其中使得总价值最大的k
int temp = bestValue(i-1, c - v[i]*k) + w[i]*k; //递推关系式
if(temp > res) {
res = temp;
}
}
}
return res;
}
/*
* 2.递归+记忆 自顶向下
* 递推关系式:F(i,c) = max(F(i-1 , c-v[i]*k) + w[i]*k) ; 0 <= k*v[i] <=c
*/
public int bestValue_1(int i,int c) {
if(dp[i][c] != null) {
return dp[i][c];
}
int res = 0;
if(i==0 || c==0) {
res = 0;
}else if(v[i] > c) {
res = bestValue_1(i-1, c);
}else {
for(int k = 0; k*v[i] <= c;k++) {
int temp = bestValue_1(i-1, c- v[i]*k) + w[i]*k;
if(temp > res) {
res = temp;
}
}
}
dp[i][c] = res;
return res;
}
/*
* 3.动态规划
* 状态转移方程:F[i+1][j] = Math.max(F[i+1][j], F[i][j- k*v[i]] + k*w[i]) ;
*/
public int bestValue_2() {
for(int i = 0;i <w.length;i++) {
for(int j = 0;j <= C;j++) {
for(int k = 0; k*v[i] <= j;k++) {
dp[i+1][j] = Math.max(dp[i+1][j], dp[i][j- k*v[i]] + k*w[i]);
}
}
}
return dp[w.length][C];
}
/*
* 4.动态规划 优化空间
* 状态转移方程:F(c) = max{F(c) , F(c - vi) + wi}
*/
public int bestValue_3() {
for(int i = 0;i <w.length;i++) {
for(int j = v[i] ; j <= C;j++) {
newdp[j] = Math.max(newdp[j], newdp[j- v[i]] + w[i]) ;
}
}
return newdp[newdp.length-1];
}
public static void main(String[] args) {
BP_complete bp = new BP_complete();
//int res = bp.bestValue(w.length-1, 10);
//int res1 = bp.bestValue_1(w.length-1, 10);
int res2 = bp.bestValue_2();
int res3 = bp.bestValue_3();
System.out.println(res2);
}
}
5.3 相关题目
5.3.1 背包问题
在 n 个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为 m,每个物品的大小为 A[i]。
输入:数组 = [3,4,8,5] backpack size = 10
输出:9
解释:装4和5.
/**
* @param m 背包大小
* @param A 每个物品的大小为Ai
* @return
*/
public int backPack(int m, int[] A) {
int n = A.length;
boolean[] states = new boolean[m+1]; // 默认值false
states[0] = true; // 第一行的数据要特殊处理,可以利用哨兵优化
if(A[0] <= m) {
states[A[0]] = true;
}
for(int i = 1;i <n;i++) { // 动态规划
for(int j = m-A[i]; j >= 0;j--) { //把第i个物品放入背包
if(states[j] == true) {
states[j + A[i]] = true;
}
}
}
for(int i = m;i >=0;i--) { // 输出结果
if(states[i] == true) {
return i;
}
}
return 0;
}
public static void main(String[] args) {
Backpack bp = new Backpack();
Scanner in = new Scanner(System.in);
System.out.print("输入背包容量m:");
int m = in.nextInt();
System.out.print("输入物品个数n:");
int n = in.nextInt();
int[] A = new int[n];
System.out.print("输入每个物品的大小Ai:");
for(int i = 0;i < n;i++) {
A[i] = in.nextInt();
}
int res = bp.backPack(m, A);
System.out.println("输出: "+res);
}
5.3.2 背包问题2
有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值,问最多能装入背包的总价值是多大?
说明:你所挑选的要装入背包的物品的总大小不能超过 m ,每个物品只能取一次。
(1)常规解法
(2)优化解法
输入:
m = 10
A = [2, 3, 5, 7]
V = [1, 5, 2, 4]
输出:9
解释:装入 A[1] 和 A[3] 可以得到最大价值, V[1] + V[3] = 9
/**
* @param m 背包容量
* @param A 每个物品的大小为Ai
* @param V 每个物品的价值为Vi
* @return
*/
public int backPack(int m, int[] A, int[] V) {
int n = A.length;
int[][] states = new int[n][m+1];
// 初始化states
for(int i = 0;i <n;i++) {
for(int j = 0;j <m+1;j++) {
states[i][j] = -1;
}
}
states[0][0] = 0;
if(A[0] <= m) {
states[0][A[0]] = V[0];
}
// 动态规划,状态转移
for(int i =1;i <n;i++) {
for(int j = 0;j <=m;j++) { // 不选择第i个物品
if(states[i-1][j] >= 0) {
states[i][j] = states[i-1][j];
}
}
for(int j = 0;j <= m-A[i]; j++) { // 选择第i个物品
if(states[i-1][j] >= 0) {
int v = states[i-1][j] + V[i];
if(v > states[i][j + A[i]]) {
states[i][j + A[i]] = v;
}
}
}
}
// 找出最大值
int maxV = -1;
for(int j =0;j <= m ;j++) {
if(states[n-1][j] > maxV) {
maxV = states[n-1][j];
}
}
return maxV;
}
/**
* 优化解法
* @param m
* @param A
* @param V
* @return
*/
public int backPackII(int m, int[] A, int[] V) {
int n = A.length;
if (n == 0) {
return 0;
}
int[] maxVal = new int[m + 1];
for (int i = 0; i < n; i++) {
for (int j = m; j > 0; j--) {
if (j >= A[i]) {
maxVal[j] = Math.max(maxVal[j], maxVal[j - A[i]] + A[i]);
}
}
}
return maxVal[m];
}
public static void main(String[] args) {
Backpack2 bp = new Backpack2();
Scanner in = new Scanner(System.in);
System.out.print("输入背包容量m:");
int m = in.nextInt();
System.out.print("输入物品个数n:");
int n = in.nextInt();
int[] A = new int[n];
System.out.print("输入每个物品的大小Ai:");
for(int i = 0;i < n;i++) {
A[i] = in.nextInt();
}
int[] V = new int[n];
System.out.print("输入每个物品的价值Vi:");
for(int i = 0;i < n;i++) {
V[i] = in.nextInt();
}
int res = bp.backPack(m, A, V);
int res1 = bp.backPackII(m, A, V);
System.out.println("输出最大价值: "+res);
System.out.println("输出最大价值: "+res1);
}
5.3.3 (lee-322) 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。你可以认为每种硬币的数量是无限的。
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
/**
* 思路:因为硬币可以重复使用,因此这是一个完全背包问题。完全背包只需要将 0-1 背包的逆序遍历 dp 数组改为正序遍历即可。
* @param coins 不同面额的硬币
* @param amount 总金额
* @return
*/
public int coinChange(int[] coins, int amount) {
if(coins == null || coins.length == 0) {
return -1;
}
if(amount == 0) {
return 0;
}
int[] dp = new int[amount+1];
int coinMin = 0;
for(int coin : coins){
for(int i = coin; i <= amount;i++) {
if(i == coin) {
dp[i] = 1;
}else if(dp[i] == 0 && dp[i-coin] != 0) {
dp[i] = dp[i-coin] +1;
}else if(dp[i-coin] != 0) {
dp[i] = Math.min(dp[i], dp[i-coin] +1);
}
}
if(coin < coinMin) {
coinMin = coin;
}
}
if(coinMin > amount) {
return -1;
}
return dp[amount] == 0 ? -1 : dp[amount];
}
5.3.4 (lee-139) 单词拆分
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
注意:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
/*
* 完全背包问题 因为 dict 中的单词没有使用次数的限制
* 物品必须按一定顺序放入背包中
* 求解顺序的完全背包问题时,对物品的迭代应该放在最里层,对背包的迭代放在外层,只有这样才能让物品按一定顺序放入背包中。
*/
public boolean wordBreak(String s, List<String> wordDict) {
int n = s.length();
boolean[] dp = new boolean[n+1];
dp[0] = true;
for(int i = 0;i <= n;i++) {
for(String word : wordDict) {
int len = word.length();
if(len <= i && word.equals(s.substring(i-len,i))) {
dp[i] = dp[i] || dp[i-len];
}
}
}
return dp[n];
}
3 练习链接
- (lee-64) 最小路径和
- (lee-120) 三角形最小路径和
- (lee-62) 不同路径
- (lee-63) 不同路径2
- (lee-221) 最大正方形
- (lee-1277) 统计全为1的正方形子矩阵
- (lee-JZ47)礼物的最大价值
- (lee-JZ63)股票的最大利润
- (lee-53) 最大子序和
- (lee-152) 乘积最大子数组
- (lee-70) 爬楼梯
- (lee-55) 跳跃游戏
- (lee-45) 跳跃游戏2
- (lee-300) 最长递增子序列
- (lee-132) 分割回文串2
- (lee-1143) 最长公共子序列
- (lee-72) 编辑距离
- 背包问题
- 背包问题2
- (lee-322) 零钱兑换
- (lee-139) 单词拆分