博弈论相关
292. Nim游戏
你和你的朋友,两个人一起玩 Nim 游戏:
桌子上有一堆石头。
你们轮流进行自己的回合, 你作为先手 。
每一回合,轮到的人拿掉 1 - 3 块石头。
拿掉最后一块石头的人就是获胜者。
假设你们每一步都是最优解。请编写一个函数,来判断你是否可以在给定石头数量为 n 的情况下赢得游戏。如果可以赢,返回 true;否则,返回 false 。
不管轮到谁,当
当前局面剩下1-3个的时候,先手必赢
当前局面剩下4个的时候,先手必输
如果想赢,必须避免当前局面出现4的倍数,
/**
博弈论:
不管轮到谁,当
当前局面剩下1-3个的时候,先手必赢
当前局面剩下4个的时候,先手必输
如果想赢,必须避免当前局面出现4的倍数,
推广:
n个石头,每人每次可拿1-m个,拿掉最后一块石头的赢,
只要避免出现m+1的倍数,先手就能赢
*/
class Solution {
public boolean canWinNim(int n) {
return n%4!=0;
}
}
877.石子游戏
Alice 和 Bob 用几堆石子在做游戏。一共有偶数堆石子,排成一行;每堆都有 正 整数颗石子,数目为 piles[i] 。
游戏以谁手中的石子最多来决出胜负。石子的 总数 是 奇数 ,所以没有平局。
Alice 和 Bob 轮流进行,Alice 先开始 。 每回合,玩家从行的 开始 或 结束 处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中 石子最多 的玩家 获胜 。
假设 Alice 和 Bob 都发挥出最佳水平,当 Alice 赢得比赛时返回 true ,当 Bob 赢得比赛时返回 false 。
例如:石子数组[2,8,3,5]的dp数组如下:
其中 dp[i][j] 表示 piles[i...j] 先手的分数是第一个数字,后手的分数是第二个数字。
大致框架:
for i in [0,n):
for j in [i, n):
先手选择
后手选择
对于piles[ i, i+1,...j-1, j ]:
- 如果先手选择最左边的 piles[i] ,那么对于剩下的piles[i+1, ...j-1, j ]此时又变成了后手,
所以当选择最左边的时候,最终dp[i][j]先 的得分是:piles[i]+ dp[i+1][j]后
此时先手选完后,后手剩下piles[i+1, ... j-1, j],后手此时变成了先手,最终dp[i][j]后 的得分是dp[i+1][j]先
- 如果先手选择最后边的 piles[j], 那么对于剩下的piles[i, i+1...j-1]此时又变成了后手,
所以当选择最右边的时候,最终dp[i][j]先 的得分是:piles[j]+ dp[i][j-1]后
此时先手选完后,后手剩下piles[i, i+1, ...j-1],后手此时变成了先手,最终dp[i][j]后 的得分是dp[i][j-1]先
dp[i][j].fir = max(piles[i] + dp[i+1][j].sec, piles[j] + dp[i][j-1].sec)
dp[i][j].fir = max( 选择最左边的石头堆 , 选择最右边的石头堆 )
# 解释:我作为先手,面对 piles[i...j] 时,有两种选择:
# 要么我选择最左边的那一堆石头,然后面对 piles[i+1...j]
# 但是此时轮到对方,相当于我变成了后手;
# 要么我选择最右边的那一堆石头,然后面对 piles[i...j-1]
# 但是此时轮到对方,相当于我变成了后手。
if 先手选择左边:
dp[i][j].sec = dp[i+1][j].fir
if 先手选择右边:
dp[i][j].sec = dp[i][j-1].fir
# 解释:我作为后手,要等先手先选择,有两种情况:
# 如果先手选择了最左边那堆,给我剩下了 piles[i+1...j]
# 此时轮到我,我变成了先手;
# 如果先手选择了最右边那堆,给我剩下了 piles[i...j-1]
# 此时轮到我,我变成了先手。
很显然,dp[i][j]的计算需要dp[i+1][j] 和dp[i][j-1],所以我们需要倒序遍历了。
具体怎么实现呢?我们需要一个二维数组作为dp,但dp的每个元素有俩值,我么可以整一个new int[2]来存储先手和后手的分数,所以dp可以new成一个三维数组,具体实现如下:
class Solution {
/**
偶数堆石子、每堆数目为 piles[i]、石子的总数是奇数、从行的开始或结束处取走整堆石头、游戏以谁手中的石子最多来决出胜负
博弈论。。。只不过Nim游戏博弈论更简单点
*/
public boolean stoneGame(int[] piles) {
int n = piles.length;
int[][][] dp = new int[n][n][2];
// 初始化dp
for (int i = 0; i < n; i++) {
dp[i][i][0] = piles[i];
dp[i][i][1] = 0;
}
for (int i = n - 2; i >= 0; i--) {
for (int j = i + 1; j < n; j++) {
// 先手先选择,判断左右谁大,就选谁
int l = piles[i] + dp[i + 1][j][1];
int r = piles[j] + dp[i][j - 1][1];
if (l > r) {
dp[i][j][0] = l;
dp[i][j][1] = dp[i + 1][j][0]; // 后手根据先手的选择选
} else {
dp[i][j][0] = r;
dp[i][j][1] = dp[i][j - 1][0];
}
}
}
return dp[0][n - 1][0] > dp[0][n - 1][1] ? true : false;
}
}
买卖股票相关
188题是最基本的,理解188之后,所有的股票都是具体形式而已(学自labuladong)
121、买卖股票的最佳时机
给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。
你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。
返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。
class Solution {
// 只能某天买入,某天卖出,意味着只能买卖一次,输出最大利润
// 在数组中找到最小买入价格,并在最小买入价格之后找到最高卖出价格,即最大利润
// 时间复杂度On
// 在数组中找到最小买入价格,并在最小买入价格之后找到最高卖出价格,即最大利润
public int maxProfit(int[] prices) {
// 记录最小买入价格和其索引
int minIn = prices[0];
// 记录最大利润
int maxP = -prices[0];
for (int i = 1; i < prices.length; i++) {
if (prices[i] < minIn) {
minIn = prices[i];
}
if (prices[i] > minIn && (prices[i] - minIn) > maxP) {
maxP = prices[i] - minIn ;
}
}
return maxP<=0?0:maxP;
}
}
122、买卖股票的最佳时机II
给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。
在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
class Solution {
/*
动态规划:dp[i]表示第i的最大利润
第i天的状态分两种:有股票1,没有股票0,所以有dp[i][1] 和 dp[i][0]
有股票dp[i][1]:
从昨天继承来的,所以昨天必须有股票:dp[i][1] = dp[i-1][1]
昨天没有股票,是今天买的,dp[i][1] = dp[i-1][0]-prices[i]
没有股票dp[i][0]:
昨天没有股票,今天也没有买:dp[i][0] = dp[i-1][0]
昨天有股票,今天卖了:dp[i][0] = dp[i-1][1]+prices[i]
没有限制交易次数
最终股票卖完才会会的最大利润,所以我们返回dp[n][0]
*/
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n + 1][2];
// 初始化第0天还没开始,没有持股票,所以利润是0,没有开始怎么持有股票,所以利润初始化为最小,方便我们找最大值
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
for (int i = 1; i < dp.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1]);
}
return dp[n][0];
}
}
309、买卖股票的最佳时机含冷冻期
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
// 题目要求:卖出股票后,第二天不能买入
/*
今天没有股票dp[i][0]
可能是昨天就没有股票dp[i][0] = dp[i-1][0]
可能是昨天有股票,今天卖了dp[i][0] = dp[i-1][1]+prices[i-1]
今天有股票dp[i][1]
昨天就有股票dp[i][1] = dp[i-1][1]
昨天没有股票,今天买的 dp[i][1] = dp[i-2][0]-prices[i-1]
原因:今天要买,说明昨天不能卖,昨天是冷冻期,且昨天没有股票,又不能交易,只能说明昨天是继承了昨天的昨天没有股票dp[i-2][0]
*/
public int maxProfit(int[] prices) {
int n = prices.length;
int[][] dp = new int[n + 1][2];
// 初始化dp
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
for (int i = 1; i < dp.length; i++) {
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
if(i == 1){ // 第1天,有股票,只能买入了,因为是第一天嘛,他又没有前一天
dp[i][1] = -prices[i - 1];
}else{
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 2][0] - prices[i - 1]);
}
}
return dp[n][0];
}
}
714、买卖股票的最佳时机含手续费
给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。
你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。
返回获得利润的最大值。
注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。
class Solution {
// 每笔交易都需要付手续费,每次交易的开始是买入的时候,所以我们只有在买入的时候付手续费即可
/*
依旧之前的dp公式
*/
public int maxProfit(int[] prices, int fee) {
int n = prices.length;
int[][] dp = new int[n + 1][2];
//初始化dp
dp[0][0] = 0;
dp[0][1] = Integer.MIN_VALUE;
for (int i = 1; i < dp.length; i++) {
//第i天没有股票:可能前一天就没有股票;可能前一天有股票,今天卖了
dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i - 1]);
//第i天有股票:可能继承前一天的股票;可能前一天没有股票今天买的,买就需要付手续费fee
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i - 1] - fee);
}
return dp[n][0];
}
}
123、买卖股票的最佳时机(限定交易次数)
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
// 限制了交易次数 最多只能完成2笔交易
/*
第i天的状态依旧是没有股票dp[i][0]和有股票dp[i][1],但增加了k,所以我们也加,变成
第i天的状态:没有股票dp[i][k][0]和 有股票dp[i][k][1] 意思是:第i天至多交易k次,手上目前有/没有股票的最大利润
~~~~~~~买入是交易的开始,所以只有买入才会使得k++~~~~~~~
没有股票dp[i][k][0]:
可能前一天没有股票dp[i-1][k][0]
可能前一天有股票,今天卖了 dp[i-1][k][1]+prices[i-1]
有股票dp[i][k][1]:
可能继承前一天的股票dp[i-1][k][1]
可能前一天没有股票,今天买的,dp[i-1][k-1][0]-prices[i-1]
*/
public int maxProfit(int[] prices) {
int n = prices.length;
int[][][] dp = new int[n+1][3][2];
//初始化
dp[0][1][0] = 0; // 第0天没有开始交易,没有股票,最多交易1次的利润是0
dp[0][1][1] = Integer.MIN_VALUE;// 第0天没有开始交易,有股票,最多交易1次的利润是最小值
dp[0][2][0] = 0; // 第0天没有开始交易,没有股票,最多交易2次的利润是0
dp[0][2][1] = Integer.MIN_VALUE;// 第0天没有开始交易,有股票,最多交易2次的利润是最小值
for (int i = 1; i < dp.length; i++) {
for (int k = 1; k <= 2; k++) {
//今天没有股票。至今最大交易次数是k:可能前一天没有股票,可能前一天有股票今天卖了
dp[i][k][0] = Math.max(dp[i - 1][k][0], dp[i - 1][k][1] + prices[i - 1]);
//今天有股票。至今最大交易次数是k:可能前一天有股票,可能前一天没有股票今天买的(那么前一天最大交易次数是k-1
dp[i][k][1] = Math.max(dp[i - 1][k][1], dp[i - 1][k - 1][0] - prices[i - 1]);
}
}
return dp[n][2][0];
}
}
188、买卖股票的最佳时机(限定交易次数为k)
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
class Solution {
// 最多可以完成 k 笔交易
public int maxProfit(int k, int[] prices) {
int n = prices.length;
int[][][] dp = new int[n + 1][k + 1][2];
//初始化
for (int i = 1; i <= k; i++) {
dp[0][i][0] = 0;// 第0天没有开始交易,没有股票,最多交易i次的利润是0
dp[0][i][1] = Integer.MIN_VALUE;// 第0天没有开始交易,有股票,最多交易i次的利润是最小值
}
for (int i = 1; i < dp.length; i++) {
for (int j = 1; j <= k; j++) {
//今天没有股票。至今最大交易次数是k:可能前一天没有股票,可能前一天有股票今天卖了
dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i - 1]);
//今天有股票。至今最大交易次数是k:可能前一天有股票,可能前一天没有股票今天买的(那么前一天最大交易次数是k-1
dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i - 1]);
}
}
return dp[n][k][0];
}
}
数组动态规划:
45、跳跃游戏
给你一个非负整数数组 nums ,你最初位于数组的第一个位置。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
你的目标是使用最少的跳跃次数到达数组的最后一个位置。
假设你总是可以到达数组的最后一个位置。
class Solution {
/**
* 用最少的跳跃次数到达数组的最后一个位置。题目假设:最后一个位置可达
动态规划:dp[i]表示到位置i最少需要跳跃的次数
*
* @param nums
* @return
*/
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 = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i] >= (j - i)) { // 在位置i可达位置j
dp[j] = Math.min(dp[j], dp[i] + 1);
}
}
}
// for (int i = 0; i < n; i++) {
// System.out.print(dp[i] + " ");
// }
return dp[n - 1]; // 返回最后一个位置需要的最少跳跃次数
}
}
55、跳跃游戏
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标 。
数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。
class Solution {
/**
* 判断是否能到达最后一个位置
* 45题轻微改下就行
*
* @param nums
* @return
*/
public boolean canJump(int[] nums) {
int n = nums.length;
int[] dp = new int[n];
Arrays.fill(dp, Integer.MAX_VALUE); // 初始化各个位置不可达
dp[0] = 0; // 最初位于第一个位置,所以在第一个位置不用跳跃即达到
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (nums[i] >= (j - i)) { // 在位置i可达位置j
dp[j] = Math.min(dp[j], dp[i] + 1);
}
}
}
for (int i = 0; i < n; i++) {
if(dp[i] == Integer.MAX_VALUE){ // 最后一个位置之前某个位置就不可达,那么最后一个位置肯定不可达
return false;
}
}
return true;
}
}
打家劫舍
198、打家劫舍
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
class Solution {
/**
* dp[i]表示,目前到房屋i,偷到的最大金额
* 因为不能偷相邻的两间屋子,那么对于nums[i]
* 偷nums[i],nums[i-1]不能偷 ,dp[i] = nums[i]+dp[i-2]
* 不偷nums[i],nums[i-1]能偷 ,dp[i] = dp[i-1] 这样我们的dp公式就出来了
*
* @param nums
* @return
*/
public int rob(int[] nums) {
int n = nums.length; // n间屋子,题目已知n最小为1
if (n == 1) {
return nums[0];
}
if (n == 2) {
return Math.max(nums[0], nums[1]);
}
int[] dp = new int[n];
dp[0] = nums[0]; // 对于第0间屋子,前面没有屋子,所以偷,金额才最大
dp[1] = Math.max(nums[1], nums[0]);
for (int i = 2; i < n; i++) {
dp[i] = Math.max(dp[i - 1], nums[i] + dp[i - 2]);
}
return dp[n-1];
}
}
231、打家劫舍II
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
class Solution {
/**
* 房屋围城一个圈,怎么解决?把一圆圈数组分成两部分,一部分只有头,一部分没有头,两部分分别按照打家劫舍做法做
*
* @param nums
* @return
*/
public int rob(int[] nums) {
int n = nums.length;
if (n == 1) {
return nums[0];
}
if (n == 2) {
return Math.max(nums[0], nums[1]);
}
int[] part1 = Arrays.copyOfRange(nums, 0, n - 1);
int[] part2 = Arrays.copyOfRange(nums, 1, n);
return Math.max(subRob(part1), subRob(part2));
}
public int subRob(int[] nums) {
int n = nums.length; // n间屋子,题目已知n最小为1
if (n == 1) {
return nums[0];
}
if (n == 2) {
return Math.max(nums[0], nums[1]);
}
int[] dp = new int[n];
dp[0] = nums[0]; // 对于第0间屋子,前面没有屋子,所以偷,金额才最大
dp[1] = Math.max(nums[1], nums[0]);
for (int i = 2; i < n; i++) {
dp[i] = Math.max(dp[i - 1], nums[i] + dp[i - 2]);
}
return dp[n - 1];
}
}
337、打家劫舍III
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
/**
类似于树结构,不能偷相连的节点,那么对于一个节点来说,如果偷,则不能偷孩子节点,如果不偷,则可以偷孩子节点
可以知道某个节点上的最大值,依赖于他的子节点,所以我们需要从底向上确认
*/
Map<TreeNode, Integer> f = new HashMap<TreeNode, Integer>();
Map<TreeNode, Integer> g = new HashMap<TreeNode, Integer>();
public int rob(TreeNode root) {
if (root == null){
return 0;
}
if (root.left == null && root.right == null){
return root.val;
}
dfs(root);
// 返回偷root和不偷root的最大值
return Math.max(f.getOrDefault(root, 0) , g.getOrDefault(root, 0));
}
public void dfs(TreeNode root){
if (root == null){
return;
}
dfs(root.left);
dfs(root.right);
// 偷root节点
f.put(root, root.val + g.getOrDefault(root.left, 0) + g.getOrDefault(root.right, 0));
// 不偷root节点
g.put(root,
Math.max(f.getOrDefault(root.left, 0), g.getOrDefault(root.left, 0)) +
Math.max(f.getOrDefault(root.right, 0), g.getOrDefault(root.right, 0))
);
}
}
背包问题
416、分成等和子集
给你一个 只包含正整数 的 非空 数组 nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
这其实是一个背包问题嘛,就是说。现在有一个背包,容量是数组和的一半,让你从数组中找出填满背包的一种方式。
但我发现一个意外的收获,对于这一题,我直接返回sum%2==0,竟然冲破104/117个用例!!!投机取巧,笔试骗分学到了!
下面是正经的动态规划
1、写出一般框架:
for 状态1 in 所有状态1:
for 状态2 in 所有状态2;
dp[状态1][状态2] = 择优(选择)
2、明确dp定义:
状态:变量,,背包和在不断变化,数组个数在不断变化,所以我们有两个状态
选择:对于一个数字,选还是不选
写出伪代码;
for 数字nums[i] in nums:
for 背包和j in sum/2:
if 当前背包可以容纳nums[i]:
dp[i][j] = (选nums[i] or 不选nums[i])
else:
只能不选了,因为容纳不下当数字nums[i]
3、明确base case
初始dp都初始化为false,假设对于所有的背包容量都无法满足
dp[i][0] 背包容量为0,当然可以满足,因为不用选任何数字即可满足,设为true
4、java代码
class Solution {
/**
要分成等和的两部分,首先nums的所有元素的sum必须能整除2
在此前提下:我们从nums中不断寻找数字,使得其累加和是sum的一半,这有点类似背包问题了,
以nums = [1,5,11,5]为例,sum=22,sum%2==0,sum/2=11;我们需要建立一个4行12列的二维数组
列表示容量,行表示该位置的元素,dp[i][j]表示选与不选nums[i]构成和为j
如果nums[i]不选,dp[i][j] = dp[i-1][j]
如果nums[i]选,dp[i][j] = dp[i-1][j-nums[i]]
*/
public boolean canPartition(int[] nums) {
int n = nums.length; // 题目已知数组非空,但至少两个数字才能分成两部分吧..
if (n < 2) {
return false;
}
int sum = Arrays.stream(nums).sum(); // 使用流的方式求数组和,和不能被2整除,肯定不能分成两部分
if (sum % 2 != 0) {
return false;
}
boolean[][] dp = new boolean[n][sum / 2 + 1];
// 第一行只能让容量为nums[0]的填满
if (nums[0] < sum / 2) {
dp[0][nums[0]] = true;
}
for (int i = 1; i < n; i++) {
for (int j = 0; j < dp[0].length; j++) {
// 直接从上一行先把结果抄下来,然后再修正
dp[i][j] = dp[i - 1][j];
// 容量为nums[i]的可以为1
if (nums[i] == j) {
dp[i][j] = true;
}
// 如果容量j大于nums[i],可以选择选或者不选nums[i]
if (nums[i] < j) {
dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]];
}
}
}
return dp[n - 1][dp[0].length-1];
}
}
322、零钱兑换
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
动态规划思路:首先求最值,可以先考虑动态规划,其次我们希望凑成amount所需要的硬币最少,那么凑成小于amount的硬币也应该最少,符合最优子结构,所以决定用动态规划;
1、动态规划的一般框架:
for 状态1 in 所有的状态1:
for 状态2 in 所有的状态2:
dp[状态1][状态2] = 择优(选择1, 选择2 ,...)
2、明确dp定义:
选择:很明确,就是所提供的给我们的所有的coin,对于每个coin,选择还是不选择
状态:变量,就是我们的目标金额amount和不同面值的coin
dp[i][j] 表示前 i 个硬币来凑目标金额 j 所需的最少硬币
3、明确base case 写出伪代码
base case:dp[i][0] 尽管有i个硬币,但目标金额是0,不需要任何硬币,所以我们初始化为0
其他初始化为最大,因为我们要求的是最小硬币数嘛。
伪代码:
for i 所有的硬币: 状态1:目标金额i
for j in 所有的目标金额:
如果当前硬币 i 小于等于目前金额 j:(为了加速选择,只有当硬币值小于目标金额才能参与选择)
dp[i][j] = min(选硬币i的组合数,不选硬币i的组合数)
dp[i][j] = min(dp[i][j-coins[i]] +1 , dp[i-1][j] );
否则
硬币i不能参与选择,dp[i][j] 仍旧是前 i-1个硬币,目标金额j不变的组合数
dp[i][j] = dp[i-1][j]
4、java代码
public int coinChange(int[] coins, int amount) {
if (amount == 0) { // 目标金额为0,不需要任何硬币
return 0;
}
int n = coins.length;
int[][] dp = new int[n + 1][amount+1];
for (int[] d : dp) {
Arrays.fill(d, Integer.MAX_VALUE);
}
// base case:目标金额为0,不需要任何硬币
for (int i = 0; i < dp.length; i++) {
dp[i][0] = 0;
}
// 套框架
for (int i = 1; i <= n; i++) {// 状态1:所有面值的硬币
for (int j = 1; j <= amount; j++) { // 状态2:不同的目标金额
if (coins[i - 1] <= j && dp[i][j - coins[i - 1]] != Integer.MAX_VALUE) {
dp[i][j] = Math.min(dp[i][j - coins[i - 1]] + 1, dp[i - 1][j]);
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[n][amount] == Integer.MAX_VALUE ? -1 : dp[n][amount];
}
518、零钱兑换II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
1、动态规划一般框架:
for 状态1 in 所有的状态1:
for 状态2 in 所有的状态2:
dp[状态1][状态2] = 择优(选择1, 选择2, ……)
2、明确dp定义
选择:对于题目提供给我们的所有的硬币 coins,选择coin还是不选择coin去组合目标金额
状态:背包的容量和可选择物品----->即需要凑成的目标金额amount和可选择的硬币coin
dp[i][j] 表示前i个硬币可以凑成金额j的组合数
3、确定伪代码和base case :
for i 从 0到n:(所有的硬币的索引)
for j 从 1到amount:(所有目标金额)
如果当前目标金额j大于等于硬币面值coins[i],说明该硬币可以参与选择:
dp[i][j] = dp[i-1][j-coins[i]
否则,该硬币不能参与当前金额j的选择:
dp[i][j] = dp[i-1][j]
最终选与不选coins[i] 凑成金额j的所有目标和就是两者相加
dp[i][j] = dp[i-1][j-coins[i] + dp[i-1][j]
base case:
对于dp[i][0] 不管提供多少硬币,组成0元的方式只有一种:啥也不选,所以dp[i][0] = 1
最终我们需要返回用所有的硬币组成金额amount的组合数 dp[N][amount]
4、java代码
class Solution {
public int change(int amount, int[] coins) {
if(amount==0){
return 1;
}
int n = coins.length;
int[][] dp = new int[n+1][amount+1];
// 初始假定所有的金额都不能被凑出来,dp[i][j]=0
// 目标金额为0可以凑出来,组合数为1:即啥也不选
for(int i=0;i<=n;i++){
dp[i][0] = 1;
}
for(int i=1;i<=n;i++){ // 状态1:硬币
for(int j=1;j<=amount;j++){ // 状态2:目标金额
if(j >= coins[i-1]){
// coins[i] 可以参与选择,选coins[i] 不选coins[i]
dp[i][j] = dp[i][j-coins[i-1]] + dp[i-1][j];
}else{
// coins[i]不能参与选择
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][amount] == amount+1 ? 0 : dp[n][amount];
}
}
474、一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集
1、动态规划一般框架:
for 状态1 in 所有状态1:
for 状态2 in 所有状态2:
dp[状态1][状态2] = 择优(选择1, 选择2, ......)
2、明确dp定义:
选择:题目提供的二进制字符串数组中的每一个字符串,我们选还是不选
状态:背包容量和被选择物品:背包容量是m个0和n个1,被选择的字符串的个数
dp[i][j][k] 表示strs[0...i]填背包j个0,k个1,的最大长度
3、伪代码和base case
for i 从0到strs.length-1:
for j 从0到m:
for k 从0到n:
如果当前背包容量可以容纳strs[i],可以选择strs[i]
dp[i][j][k] = dp[i][j-strs[i]_0][k-strs[i]_1]+1
容纳不了,不能选择strs[i]
dp[i][j][k] = dp[i-1][j][k]
base case:
背包容量为0个0,0个1时,不能容纳任何str,所以dp[i][0][0]=0
没有str时,背包也没得装,所以dp[0][j][k]=0
4、java代码
class Solution {
/**
for i 从0到strs.length-1:
for j 从0到m:
for k 从0到n:
如果当前背包容量可以容纳strs[i],可以选择strs[i]
dp[i][j][k] = dp[i][j-strs[i]_0][k-strs[i]_1]+1
容纳不了,不能选择strs[i]
dp[i][j][k] = dp[i-1][j][k]
base case:
背包容量为0个0,0个1时,不能容纳任何str,所以dp[i][0][0]=0
没有str时,背包也没得装,所以dp[0][j][k]=0
*/
public int findMaxForm(String[] strs, int m, int n) {
int l = strs.length; // 字符串的个数
int[][][] dp = new int[l+1][m+1][n+1];
// 这里没有做初始化,是因为当i为0,或者j和k为0时,背包里面啥也不能装,就是0,
for(int i=1;i<=l;i++){
int c0 = cnt_0(strs[i-1]);
int c1 = strs[i-1].length()-c0;
for(int j=0;j<=m;j++){
for(int k=0;k<=n;k++){
// strs[i-1]可以选择,那么我们是选还是不选呢?取max
if(j>=c0 && k >=c1){
dp[i][j][k] = Math.max(dp[i-1][j-c0][k-c1]+1, dp[i-1][j][k]);
}else{
// strs[i-1]不可以选择,那么我们只能是不选
dp[i][j][k] = dp[i-1][j][k];
}
}
}
}
return dp[l][m][n];
}
// 计算str中0的个数
public int cnt_0(String s){
int cnt = 0;
for(int i=0;i<s.length();i++){
if(s.charAt(i) == '0'){
cnt++;
}
}
return cnt;
}
}
494、目标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
感觉这个题更像是回溯,一上来我肯定不会用动态规划
回溯的一般框架:
public void backTry(){
if 满足结束条件
do something
递归调用backTry()
回溯
}
java代码:
class Solution {
/**
回溯:
利用起nums中所有的数字,在数字之前加+或-,使得表达式的结果等于target
*/
int count = 0; // 计数有多少个表达式的结果等于target
public int findTargetSumWays(int[] nums, int target) {
find(nums, target, 0, 0); // 从下标为0,sum为0开始
return count;
}
public void find(int[] nums, int target, int curSum, int curIdx) {
if (curIdx == nums.length) { // 结束条件:已经遍历到数组的最后一位,此时判断sum是否等于target
if (curSum == target) {
count++;
return;
} else {
return;
}
}
find(nums, target, curSum + nums[curIdx], curIdx + 1);
//回溯
// find(nums, target, curSum-nums[curIdx], curIdx-1);
find(nums, target, curSum - nums[curIdx], curIdx + 1);
//回溯
// find(nums, target, curSum+nums[curIdx], curIdx-1);
}
}
413、等差数列划分
如果一个数列 至少有三个元素 ,并且任意两个相邻元素之差相同,则称该数列为等差数列。
例如,[1,3,5,7,9]、[7,7,7,7] 和 [3,-1,-5,-9] 都是等差数列。
给你一个整数数组 nums ,返回数组 nums 中所有为等差数组的 子数组 个数。
子数组 是数组中的一个连续序列
分析:
状态: 变化的量---选不同个数的数字,等差数列的个数
dp定义:
dp[i]表示以i结尾的等差子数组的个数
base case:
dp[0]和dp[1]初始化为0,因为少于3个数字的无法组成等差数列
择优选择:
如果nums[i+1]-nums[i] == nums[i]-nums[i-1],那么dp[i+1] = dp[i]+1
最终返回的结果是:dp数组之和
java 实现
class Solution {
/**
dp定义:
dp[i]表示以i结尾的等差子数组的个数
base case:
dp[0]和dp[1]初始化为0,因为少于3个数字的无法组成等差数列
择优选择:
如果nums[i+1]-nums[i] == nums[i]-nums[i-1],那么dp[i+1] = dp[i]+1
最终返回的结果是:dp数组之和
*/
public int numberOfArithmeticSlices(int[] nums) {
int n = nums.length;
if (n < 3) { // 至少3个数字才能划分为等差数列
return 0;
}
int[] dp = new int[n];
dp[0] = 0;
dp[1] = 0;
for (int i = 2; i < n; i++) {
if (nums[i] - nums[i - 1] == nums[i - 1] - nums[i - 2]) {
dp[i] = dp[i - 1] + 1;
}
}
return Arrays.stream(dp).sum();
}
}
368、最大整除子集
给你一个由 无重复 正整数组成的集合 nums ,请你找出并返回其中最大的整除子集 answer ,子集中每一元素对 (answer[i], answer[j]) 都应当满足:
answer[i] % answer[j] == 0 ,或
answer[j] % answer[i] == 0
如果存在多个有效解子集,返回其中任何一个均可。