动态规划: 最优(最大或最小); 分割成子问题(自下而上,整体最优依赖于子最优);小问题还有相互重叠的更小的子问题;每一步都可能面临多种选择。
一、背包dp 每一步有多种选择,选或不选,组合最优
for(int i = 1; i <= amount; i++) {
for(int j = 0; j < coins.length; j++) {
if(coins[j] <= i) {
dp[i] = Math.min(dp[i - coins[j]] + 1, dp[i]);
}
}
}
1.完全背包
1.零钱兑换:
零钱: [1, 2, 3, 6, 8]
amount = 10;
组合成amount的零钱个数最小 零钱可重复
四要素:
dp[i] 代表到amount=i为止,组成i的最少硬币数
dp[i] = Math.min(dp[i - coins[j]] + 1, dp[i]);
自底向上:dp[0] = 0 (不知道零钱数额,当金额为0时,硬币数为0)
初始,方便比较:dp[i] = amount+1,amount+1为了在满足不了刚刚好组成的amount的情况下,输出-1
class Solution {
public int coinChange(int[] coins, int amount) {
// dp[i] 到面额i为止,组成i的零钱数
// dp[i] = min(dp[i - Cj] + 1, dp[i]) 自下而上
// dp[i] = amount + 1 初始(为解决2 3 -1),
// 底: dp[0] = 0;
int[] dp = new int[amount+1];
Arrays.fill(dp, amount+1);
dp[0] = 0;
for(int i = 1; i <= amount; i++) {
for(int j = 0; j < coins.length; j++) {
if(coins[j] <= i) {
dp[i] = Math.min(dp[i - coins[j]] + 1, dp[i]);
}
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
}
特殊:如果组合小于,即满足不了刚刚组合amount的情况,
用以上解决, Arrays.fill(dp, amount+1); dp[0] = 0; return dp[amount] > amount ? -1 : dp[amount];
2.完全平方数 dp[i] = Math.min(dp[i - sqaure_nums[j]] + 1, dp[i]);
完全平方数:[1 4 9 16 25] 自己生成的
n:100
组合成数n的完全平方数的个数最小 完全平方数可重复
四要素:
dp[i] 代表到n=i为止,组成i的最少完全平方数
dp[i] = dp[i] = Math.min(dp[i - sqaure_nums[j]] + 1, dp[i]);
自底向上:dp[0] = 0 dp[1] = 1
初始,方便比较:dp[i] = i; 每一个完全平方数都是1,最大
class Solution {
public int numSquares(int n) {
// dp[i] 到数i时,组成的完全平方数个数最小
// 自己生成完全平方数的数组后,就和零钱问题一样
// dp[i] 代表数i的组成的完全平方数个数最小
// dp[i] = i
// dp[i] = min(dp[i - sqaure_nums[j] + 1, dp[i])
int sqaure_nums_len = (int) Math.sqrt(n) + 1;
int[] sqaure_nums = new int[sqaure_nums_len];
for(int i = 1; i < sqaure_nums_len; i++) {
sqaure_nums[i] = i * i;
}
int[] dp = new int[n+1];
for(int i = 0; i <= n; i++) {
dp[i] = i;
}
for(int i = 1; i <= n; i++) {
for(int j = 1; j < sqaure_nums_len; j++) {
if(sqaure_nums[j] <= i) {
dp[i] = Math.min(dp[i - sqaure_nums[j]] + 1, dp[i]);
}
}
}
return dp[n];
}
}
注意:
int sqaure_nums_len = (int) Math.sqrt(n) + 1;
if(sqaure_nums[j] <= i)
3.钢管切割 dp[i] = Math.max(dp[i - gangDuan[j]] + prices[j], dp[i]);
gangDuan = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
钢管长度 = length
段的价格prices = [1,5,8,9,10,17,17,20,24,30]
切割完后,得到的钢管加起来的价值最大
四要素:
dp[i] 代表到length=i为止,组成i的最大钢段价格和数
dp[i] = Math.max(dp[i - gangDuan[j]] + prices[j], dp[i]);
自底向上:dp[0] = 0
初始,方便比较:dp[i] = 0; 每一个长度价值为0
public static int cuttingGangTiao(int length) {
// dp[i] 代表的是钢管长度为i时,切割所获利润最大
// dp[i] = Math.max(dp[i - gangDuan[j]] + prices[j], dp[i]);
// dp[i] = 0 最小
int[] gangDuan = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 每一步有10种选择(在gangDuan[j] < i 的前提下)
int[] prices = {1,5,8,9,10,17,17,20,24,30};
int[] dp = new int[length+1]; //
for (int i = 1; i <= length; i++) {
for (int j = 0; j < gangDuan.length; j++) {
if(gangDuan[j] <= i) {
dp[i] = Math.max(dp[i - gangDuan[j]] + prices[j], dp[i]);
}
}
}
return dp[length];
}
4.剪绳子
绳子段长度:[1 2 3 4]
绳子长度:length
减后,绳子各段相乘的乘积最大
四要素:
dp[i] 绳子长度为 i 时,绳子减后,各段绳子长度乘积最大值
dp[i] = Math.max(Math.max(j * dp[i-j], j * (i - j)), dp[i]); // 根据递推等式,dp[i-j]只可能由两段及以上组成,j * dp[i-j] 段数 >= 3, 所以还要考虑一种只有两段组成的情况 j * (i - j),所以是三个比较
自底向上: dp[0] = 0; dp[1] = 1; dp[2] = 1; dp[3] = 2 只有0 1 2 3 只有一种减的情况
初始:dp[i] = 1 所有绳子段长度 = 1 ,乘积最小。
class Solution {
public int cuttingRope(int n) {
// 每一步 可以在1 -- i-1的位置上减
// dp[i] = Math.max(Math.max(dp[j]*dp[i-j], i*(i-j)), dp[i])
// dp[i] = 1; // 最小,都切一段
// if(n == 0) return 0; if(n == 1) return 0; if(n == 2) return 1; if(n == 3) return 2
// 自底向上:dp[0] = 0; dp[1] = 1; dp[2] = 1; dp[3] = 2
if(n == 0) return 0;
if(n == 1) return 0;
if(n == 2) return 1; // 只能分成1和1
if(n == 3) return 2; // 只能分成1和2
int[] dp = new int[n+1];
Arrays.fill(dp, 1);
dp[0] = 0;
dp[1] = 1;
dp[2] = 1;
dp[3] = 2;
for(int i = 4; i <= n; i++) {
for(int j = 1; j <= i / 2; j++) {
dp[i] = Math.max(Math.max(j * dp[i-j], j * (i - j)), dp[i]);
}
}
return dp[n];
}
}
注意:if(n == 1) return 0; dp[1] = 1; 方便灵活
2. 0-1背包 和完全背包相比,每一步选择过后的选项就不能再选
1.宽背包
宽度背包 0-1 dp
lengths = {1,2,3,4,5,6,7}
prices = {10,9,8,7,6,5,4}
length = 10
四要素
dp[i] 代表货物柜长度为i时,所摆放商品的最大价格
dp[i] = Math.max(dp[i - lengths[j]] + prices[j], dp[i]);
自顶向下:dp[0] = 0 // 长度为0时,利润为0
初始:dp[i] = 0 最小
https://www.zhihu.com/search?type=content&q=Threadocal%E5%BA%94%E7%94%A8%E5%9C%BA%E6%99%AF
public static int kuanDuBeiBao(int length) {
// dp[i] 代表货物柜长度为i时,所摆放商品的最大价格
// dp[i] = Math.max(dp[i - lengths[j]] + prices[j], dp[i]);
// 自顶向下:dp[0] = 0 // 长度为0时,利润为0
// 初始:dp[i] = 0 最小
int[] lengths = {1,2,3,4,5,6,7};
int[] prices = {10,9,8,7,6,5,4};
int[] dp = new int[length+1];
for (int j = 0; j < lengths.length; j++) {
for (int i = length; i >= 1; i--) {
if(lengths[j] <= i) {
dp[i] = Math.max(dp[i - lengths[j]] + prices[j], dp[i]);
}
}
}
return dp[length];
}
不懂:
为什么要for (int i = length; i >= 1; i–) 当length=10, ans = 34
for (int i = 1; i >= length; i++) 当length=10, ans = 100
二、线性dp
dp[i] = dp[i-1] + … 和前一个状态有关
for(i) { for(j) {dp[i] = dp[j] + } } 和前某几个状态有关
单串:最长上升子序列
双串:最长公共子序列
打家劫舍系列:
股票系列:
1.最大子序和 dp[i] = Math.max(dp[i-1], 0) + nums[i];
四要素:
dp[i] 到数组位置i为止,最大序列和
dp[i] = Math.max(dp[i-1], 0) + nums[i];
自底:dp[0] = nums[0], 从 i = 1 开始
初始化: dp[i] = 0 最小
res保留最优解,最优解很有可能在中间
class Solution {
public int maxSubArray(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
dp[0] = nums[0];
int res = nums[0];
for(int i = 1; i < nums.length; i++) {
dp[i] = Math.max(dp[i-1], 0) + nums[i];
res = Math.max(res, dp[i]);
}
return res;
}
}
注意:
dp[0] = nums[0];
int res = nums[0];
dp[i]只和前一个状态有关,可以用一个currMax表示
class Solution {
public int maxSubArray(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
int currMax = nums[0];
int res = nums[0];
for(int i = 1; i < nums.length; i++) {
currMax = Math.max(currMax, 0) + nums[i];
res = Math.max(res, currMax);
}
return res;
}
}
2.单串:最长上升子序列 dp[i] = Math.max(dp[j] + 1, dp[i]);
四要素:
dp[i] 到数组位置i为止,的最长上升子序列
dp[i] = max(dp[j] + 1, dp[i])
dp[i] = 1 每个元素算一个
class Solution {
public int lengthOfLIS(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int[] dp = new int[nums.length];
Arrays.fill(dp, 1);
if(nums.length < 2) return nums.length;
int res = 0;
for(int i = 1; i < nums.length; i++) {
for(int j = 0; j < i; j++) {
if(nums[j] < nums[i]) {
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
}
res = Math.max(res, dp[i]);
}
return res;
}
}
注意:考虑nums.length = 1的情况,直接返回1, 而不是res = 0;`
3.买卖股票(只能卖一次) dp[i] = Math.max(prices[i] - minPrice, dp[i-1]);
四要素:
dp[i] 代表到第 i 天为止,所获的最大利润
dp[i] = max(dp[i] - minPrice, dp[i-1])
自底向上:dp[0] = 0;
初始化:dp[i] = 0;
额外:
minPrice = nums[0]
res = 0
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0) return 0;
int[] dp = new int[prices.length];
int minPrice = prices[0];
int res = 0;
for(int i = 1; i < prices.length; i++) {
if(prices[i] < minPrice) {
minPrice = prices[i];
}else if(prices[i] > minPrice) {
dp[i] = Math.max(prices[i] - minPrice, dp[i-1]);
}
res = Math.max(res, dp[i]);
}
return res;
}
}
class Solution {
public int maxProfit(int[] prices) {
if(prices == null || prices.length == 0) return 0;
int currMax = 0;
int minPrice = prices[0];
for(int i = 1; i < prices.length; i++) {
if(prices[i] < minPrice) {
minPrice = prices[i];
}else if(prices[i] > minPrice) {
currMax = Math.max(currMax, prices[i] - minPrice);
}
}
return currMax;
}
}