前言
所有题目均来自力扣题库中的hot 100,之所以要记录在这里,只是方便后续复习
309.最佳买卖股票时机含冷冻期
题目:
给定一个整数数组prices,其中第 prices[i] 表示第 i 天的股票价格 。
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: prices = [1,2,3,0,2]
输出: 3
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]
示例 2:
输入: prices = [1]
输出: 0
解题思路:
【动态规划】首先我们想一下怎么衡量赚了多少?假设初始金额为0,买入就减去对应价格,卖出就加上对应价格;然后考虑一下每天分几种情况?第一种,今天持有股票,第二种,今天不持有股票;仔细思考一下第一种情况,今天持有股票可能是昨天就持有股票,也有可能是昨天不持有今天刚买的,那这种情况需要今天不是冷冻期,不是冷冻期又依赖昨天没有卖股票,所以我们要将今天不持有股票这种情况细化一下,即今天不持有股票且明天不是冷冻期和今天不持有股票且明天不是冷冻期,这样一共三种情况;再仔细思考一下第二种情况即今天不持有但明天是冷冻期,只有一种可能也就是昨天持有今天刚卖了;再仔细思考第三种情况即今天不持有且明天不是冷冻期,有两种可能,昨天就不持有但今天是冷冻期和昨天就不持有但今天不是冷冻期;思考完三种情况发现今天的各情况都与昨天有关系,所以可以考虑动态规划,从头开始递推每种情况的最大收益,最后取三种情况的最大值即可,那初始值是什么呢?也就是第一天的三种情况了,很容易想到各个情况的值;最后由于每个值至于上一个只有关我们可以用滚动变量代替列表
代码(python):
class Solution(object):
def maxProfit(self, prices):
"""
:type prices: List[int]
:rtype: int
"""
n = len(prices)
# 定义列表1 代表今天持有股票
have_list = list()
# 定义列表2 代表今天不持有股票 但是明天处于冷冻期
no_have_but_freeze_list = list()
# 定义列表3 代表今天不持有股票 同时明天也不处于冷冻期
no_have_no_freeze_list = list()
# 初始值定义,第一天各个列表的值 买入收益就是负的,卖出收益就是正的 收益初始值可以假定为0
have_list.append(0 - prices[0])
no_have_but_freeze_list.append(0)
no_have_no_freeze_list.append(0)
# 从第二天开始递推每天三种情况的收益
for i in range(1, n):
# 今天持有股票有两种可能:昨天就持有, 昨天不持有但是今天又不是冷冻期也就是今天刚买的 取两者最大值
have_list.append(max(have_list[i - 1], no_have_no_freeze_list[i - 1] - prices[i]))
# 今天不持有股票但明天是冷冻期 只有一种可能:昨天持有,今天卖了
no_have_but_freeze_list.append(have_list[i - 1] + prices[i])
# 今天不持有股票且明天不是冷冻期,有两种可能:昨天就没有持有了,今天可能是冷冻期,今天也可能不是冷冻期
no_have_no_freeze_list.append(max(no_have_no_freeze_list[i - 1], no_have_but_freeze_list[i - 1]))
# 返回最后一天三种状态的最大值即可
return max(max(have_list[n - 1], no_have_but_freeze_list[n - 1]), no_have_no_freeze_list[n - 1])
代码(java):
class Solution {
public int maxProfit(int[] prices) {
int f1 = 0 - prices[0];
int f2 = 0;
int f3 = 0;
for(int i = 1; i < prices.length; i++){
int newF1 = Math.max(f1, f3 - prices[i]);
int newF2 = f1 + prices[i];
int newF3 = Math.max(f3, f2);
f1 = newF1;
f2 = newF2;
f3 = newF3;
}
return Math.max(f2,f3);
}
}
知识点:
- 无
原题链接:最佳买卖股票时机含冷冻期
322.零钱兑换
题目:
给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。
计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
解题思路:
【动态规划】假设我们要兑换的金额为amount,当我们选取了一个硬币coin,那么硬币个数就是amount-coin的硬币个数+1(coin本身),可以看到amount的个数与amount-coin的个数相关,可以联想到用动态规划;那初始值是什么呢?当amount为0时,我们需要的硬币数就是0;在选取硬币时要注意的是硬币的值要小于等于目标值才能被选取;因为有很多个coin,所以我们要得到选取每个coin的硬币个数,从中取得最小值,如果没有组合可以组成amount怎么办,正常来说我们应该将个数置为-1,但为了消除递推过程中将-1当成正常值被选作最小值的影响,可以将个数置为int最大值,这样就不会被当做最小值选取,最后返回结果时进行一次判断即可
代码(python):
class Solution(object):
def coinChange(self, coins, amount):
"""
:type coins: List[int]
:type amount: int
:rtype: int
"""
# 定义结果列表
res = list()
#起始值,当amount为0时,需要0个硬币
res.append(0)
# 从1开始到amount递推每个值需要多少个硬币
for i in range(1, amount + 1):
# 初始化当前需要的硬币数
cur_val = sys.maxsize
# 对于每个硬币,如果小于等于当前值,说明可选取,则硬币数就是i-coin的硬币数+1;遍历每个硬币 与当前硬币数相比得到最小的硬币数
for coin in coins:
if coin <= i:
cur_val = min(cur_val, res[i - coin] + 1)
# 将当前硬币数添加到结果列表中
res.append(cur_val)
# 如果amount的硬币数仍为初始定义的int最大值则返回-1;如果不是而返回该值
if res[amount] == sys.maxsize:
return -1
else:
return res[amount]
代码(java):
class Solution {
public int coinChange(int[] coins, int amount) {
int[] res = new int[amount + 1];
res[0] = 0;
for(int i = 1; i <= amount; i++){
int min = Integer.MAX_VALUE;
for(int j = 0; j < coins.length; j++){
if(coins[j] == i){
min = 1;
}else if(coins[j] < i){
if(res[i - coins[j]] != -1){
min = Math.min(min, res[i - coins[j]] + 1);
}
}
}
if(min == Integer.MAX_VALUE){
res[i] = -1;
}else{
res[i] = min;
}
}
return res[amount];
}
}
知识点:
- 无
原题链接:零钱兑换
337.打家劫舍Ⅲ
题目:
小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root 。
除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。
给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额 。
示例 1:
输入: root = [3,2,3,null,3,null,1]
输出: 7
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
解题思路:
【动态规划+深度优先搜索】根据之前的打家劫舍我们可以很容易联想到动态规划,因为每个节点的被偷的值都可以取决于他的相邻节点(父节点或子节点),而且每个节点都有偷取和不偷取两种情况;之前可以用两个数组去维护这两种情况下各个位置,但现在我们是树结构,不是单一的链表或数组顺序,所以我们可以用两个字典结构去存放各个节点的两个情况偷取最大值;那动态规划我们递推的方向是什么呢?从上往下递推?但这种情况需要遍历子节点的时候可以拿到他的父节点,二叉树不能直接拿到父节点,那可以从下往上递推(到达每个节点所能偷取的最大值,包括偷取或不偷取两种情况),但是这要求我们先得到两个子节点的两种情况值,所以我们可以用深度优先搜索的思想,先递归将两个子节点的两种情况最大值放到字典中,再求当前节点的两种情况下最大值;当前节点的两种情况值与子节点的关系是什么呢?当前节点偷取的最大值(此时左右节点只能不偷取)=当前节点值+左节点不偷取的最大值+右节点不偷取的最大值,当前节点不偷取的最大值(此时左右节点可以偷取也可以不偷取)=左节点偷取或不偷取的较大值+右节点偷取或不偷取的较大值;最后返回根节点两种情况的较大值即可
代码(python):
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
# 定义两个字典,用来代表每个节点的两种情况:当前节点被偷取,当前节点不被偷取 下的能偷的值
choose = dict()
no_choose = dict()
def rob(self, root):
"""
:type root: TreeNode
:rtype: int
"""
# 调用深度搜索方法
self.dfs(root)
# 返回根节点的两种情况下的最大值 即结果值
return max(self.choose[root], self.no_choose[root])
def dfs(self, root):
# 如果当前节点是空节点,则直接返回
if not root:
return
# 深度搜索左子树 和 右子树,
# 为什么要先搜索子树,因为我们是从下往上递推,当前节点的两个情况的值依赖于左右节点的两个情况下的值,所以要先递归得到他们的值
self.dfs(root.left)
self.dfs(root.right)
# 当前节点被偷取的值 为 当前值 + 左节点没被偷的值 + 右节点没被偷的值
# 需要注意的是 如果当前节点为最底层节点,没有左右节点,此时我们通过字典获取他们的值可能会出错,所以可以用get方法,若取不到该值就返回0;当前也可以手动先判断一下是否有左右节点
self.choose[root] = root.val + self.no_choose.get(root.left, 0) + self.no_choose.get(root.right, 0)
# 当前节点没被偷取的值 为 左节点被偷和没被偷的最大值 + 右节点被偷和没被偷的最大值
self.no_choose[root] = max(self.choose.get(root.left, 0), self.no_choose.get(root.left, 0)) + max(self.choose.get(root.right, 0), self.no_choose.get(root.right, 0))
代码(java):
/**
* 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 {
HashMap<TreeNode, Integer> selected = new HashMap<>();
HashMap<TreeNode, Integer> noSelected = new HashMap<>();
public int rob(TreeNode root) {
dfs(root);
return Math.max(selected.getOrDefault(root, 0), noSelected.getOrDefault(root, 0));
}
public void dfs(TreeNode node){
if(node == null){
return;
}
dfs(node.left);
dfs(node.right);
selected.put(node, noSelected.getOrDefault(node.left, 0 ) + noSelected.getOrDefault(node.right, 0) + node.val);
noSelected.put(node, Math.max(selected.getOrDefault(node.left, 0), noSelected.getOrDefault(node.left, 0)) +
Math.max(selected.getOrDefault(node.right, 0), noSelected.getOrDefault(node.right, 0)));
}
}
知识点:
- 无
原题链接:打家劫舍Ⅲ
338.比特位计数
题目:
给你一个整数 n ,对于 0 <= i <= n 中的每个 i ,计算其二进制表示中 1 的个数 ,返回一个长度为 n + 1 的数组 ans 作为答案。
示例 1:
输入:n = 2
输出:[0,1,1]
解释:
0 --> 0
1 --> 1
2 --> 10
示例 2:
输入:n = 5
输出:[0,1,1,2,1,2]
解释:
0 --> 0
1 --> 1
2 --> 10
3 --> 11
4 --> 100
5 --> 101
解题思路:
【动态规划】对于一个数,如果当前数n是奇数,那么他和n-1得区别就在于二进制最后一位是1,也就是说它的二进制中1的个数是n-1的个数再加上1;如果当前数n是偶数,那么就不是n-1的个数加1了,而是和n/2的个数相等,为什么呢?因为n变成n/2就是位运算中的右移一位,又因为偶数最后一位肯定是0,所以不影响二进制中1的个数,所以此时n的二进制1的个数就是n/2的个数;综上,n的二进制1的个数和n-1和n/2有关,我们可以用动态规划进行递推;那动态规划的初始值是啥呢,就是n为0时,二进制1的个数自然也是0
代码(python):
class Solution(object):
def countBits(self, n):
"""
:type n: int
:rtype: List[int]
"""
res = list()
for i in range(0, n + 1):
cur = 0
while i > 0:
i = i & (i - 1)
cur += 1
res.append(cur)
return res
代码(java):
class Solution {
public int[] countBits(int n) {
// 定义结果数组
int[] res = new int[n + 1];
// 定义初始值 0 的时候 个数为0
res[0] = 0;
// 从1开始递推到n
for (int i = 1; i <= n; i ++){
//如果当前值为偶数,则1的个数为 n/2值1的个数
if (i % 2 == 0){
res[i] = res[i / 2];
// 如果当前值为奇数,则1的个数为 n-1值1的个数 + 1
}else{
res[i] = res[i - 1] + 1;
}
}
return res;
}
}
知识点:
- Brian Kernighan 算法:对于任意整数 x,令 x=x&(x-1),该运算将 x 的二进制表示的最后一个 1 变成 0
原题链接:比特位计数
416.分割等和子集
题目:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
解题思路:
【动态规划】两个相等的和,首先要求数组总和肯定时偶数,不然没办法分割两份;另外如果数组中得最大值已经大于总和得一半了,那其他的数再怎样都没办法凑成总和一半了;如果都满足的情况下,这个问题就变成了数组能否凑成总和的一半;那怎么判断数组是否能凑成和的一半呢?我们可以用动态规划的思想进行递推:定义一个二维列表,横坐标代表截止到数组哪个位置,范围时0到len(nums),纵坐标代表要拼成的target,范围是0到总和的一半,每个位置的含义是截至到当前数组位置能否凑成当前target。递推到最后一个位置就是截止到数组最后一个位置能否凑成总和的一半;那递推的关系是什么呢?每个位置都有两种状态,选取当前索引对应的数组值和不选取,若选取要满足当前索引对应的值小于或等于target,此时的能否凑成当前target就取决于截止到前一个位置能否凑成target-nums[i],若不选取的情况能否凑成就取决于截至到前一个位置能否凑成target,二者满足一个就说明可以凑成;递推的初始值是什么呢?当target为0时,截止到数组任何位置都可以凑成,另外当截止到数组索引0时,只有索引0对应的值的target才能被凑成
代码(python):
class Solution(object):
def canPartition(self, nums):
"""
:type nums: List[int]
:rtype: bool
"""
# 计算出数组的和 以及 最大值
sum_val = 0
max_val = 0
for num in nums:
sum_val += num
max_val = max(max_val, num)
# 如果和为奇数 直接返回 False
if sum_val % 2 == 1:
return False
# 如果最大值大于 和的一半 说明其他的值凑不到和的一半 直接返回False
half = sum_val / 2
if max_val > half:
return False
# 如果最大值等于和的一半,说明其他的值凑在一起正好是和的一半 直接返回True
if max_val == half:
return True
# 此时问题变成 数组能否凑成 和的一半即target
# 定义二维列表,其横坐标代表数组的索引:0到len(nums) ;纵坐标代表 target(0到 和的一半)
#每个位置的含义是:截至到数组当前位置(横坐标)能否凑成当前target(纵坐标)
res = list()
size = len(nums)
for i in range(0, half + 1):
res.append([False] * size)
# 定义初始值,当target为0时,也就是要拼凑出0,那截至到数组各个位置都可以
for j in range(0, size):
res[0][j] = True
# 定义初始值,当截止到数组0位置时,只要索引0对应的值 才能被拼凑出来 注意这个初始值的定义
res[nums[0]][0] = True
# 从 target为1 和 数组位置1开始递推每个位置的结果 注意要从位置1开始(因为后面递推的时候都依赖位置-1的情况)
for i in range(1, half + 1):
for j in range(1, size):
# 当前位置选取的情况 :前提条件是 当前索引的值要小于target,
# 是否能被凑成依赖于 截至到前一个位置时,target-当前索引对应值能被被凑成
if nums[j] <= i:
res[i][j] = res[i - nums[j]][j - 1]
# 当前位置不被选取的情况:是否被凑成依赖于 截至到前一个位置时,target能否被凑成
res[i][j] = res[i][j] or res[i][j - 1]
# 最后返回截止到数组最后一个位置,和的一半能否被凑成 也就是答案
return res[half][size - 1]
代码(java):
class Solution {
public boolean canPartition(int[] nums) {
if (nums.length < 2){
return false;
}
int maxv = 0;
int sumv = 0;
for (int i = 0; i < nums.length; i ++){
sumv += nums[i];
maxv = Math.max(maxv, nums[i]);
}
if (sumv % 2 !=0 | sumv / 2 < maxv){
return false;
}
int t = sumv / 2;
int n = nums.length;
boolean[][] res = new boolean[n][t+1];
for(int i = 0; i < res.length; i++){
for(int j = 0; j< res[0].length; j++){
if (j == 0){
res[i][j] = true;
}
res[i][j] = false;
}
}
res[0][nums[0]] = true;
for(int i = 1; i < res.length; i++){
for(int j = 1; j< res[0].length; j++){
if(j > nums[i]){
res[i][j] = res[i -1][j] | res[i - 1][j - nums[i]];
}else{
res[i][j] =res[i -1][j];
}
}
}
return res[n-1][t];
}
}
知识点:
- 无
原题链接:分割等和子集