322. 零钱兑换
想象你有一堆不同面值的硬币,现在的任务是用这些硬币凑出一个指定的金额,比如说11元,而且要求用的硬币数量尽可能少。
-
准备工作:首先,我们做了一张表(叫dp),这张表记录了从0元到目标金额(比如11元)每一个金额所需要的最少硬币数量。刚开始,我们假设每个金额所需的硬币数量都是最多的,这就像是一个最坏的情况。
-
开始计算:然后,我们开始尝试用手头上的每一种硬币去更新这张表。对于每一种硬币,我们看它能帮助减少哪些金额所需的硬币数量。
- 比如,如果我们有1元和2元的硬币,我们就从1元和2元开始,一直尝试到11元,看看加入这种硬币后能不能让所需硬币数量更少。
- 每次当我们考虑加入一个新的硬币时,我们会查看:如果我现在用这个硬币,再加上剩下的金额所需要的最少硬币数(这个信息已经在表里了),是不是比目前记录的要少。如果是,就更新这个金额所需的最少硬币数。
-
结果判断:最后,我们看看表中记录的目标金额所需的硬币数量是否被更新过(如果没有被更新过,说明用这些硬币凑不出这个金额),如果能凑出来,就告诉你需要的最少硬币数是多少。
python
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# 初始化从0到目标金额的逐个列表
dp = [amount+1 for _ in range(amount+1)]
dp[0] = 0 # 金额0不要任何硬币
# 遍历每个硬币
for c in coins:
# 遍历大于等于硬币面额的金额
for i in range(c, amount+1):
# 核心:更新当前金额的最少所需硬币数,求 只使用当前硬币的数量 和 使用一个当前硬币+差额所需的最小硬币数 的最小值
dp[i] = min(dp[i], dp[i-c]+1)
# 如果指定金额的硬币数没有被更新,说明没有组合能够满足
return dp[-1] if dp[-1] != amount+1 else -1
dp[i] = min(dp[i], dp[i-c]+1) 这行代码是动态规划的核心。dp[i-c]+1表示如果你选择了面额为c的硬币,那么对于剩余的金额i-c,所需的最小硬币数就是dp[i-c],再加上你这次选择的这一枚硬币(也就是+1)。min(dp[i], dp[i-c]+1)的意思是,比较当前金额i所需最小硬币数的现有值(dp[i])和使用当前面额硬币新计算得到的值(dp[i-c]+1),取二者中的较小值作为新的最小硬币数。
78. 子集
以nums = [1, 2, 3]为例。想象一下,我们有一个空盒子(temp),我们可以决定是否要把一些数字放进去。我们的目标是找出所有可能的方式来填充这个盒子,也就是所有的子集。
开始时,我们的盒子是空的。这也是一个子集,所以我们先把它加到结果列表res中。现在,res = [[]]。
递归的魔法:我们用一个函数dfs来帮助我们决定每个数字是否放入盒子。
我们从第一个数字开始,看看我们可以把它放入盒子(即列表temp),然后我们继续看下一个数字。
每当我们考虑一个数字(比如1),我们会做两件事:一次尝试把它加入盒子,另一次则不加。对于每种情况,我们都会继续向下看下一个数字,直到没有更多数字为止。
添加子集:每次我们到达数字列表的末尾时,我们的盒子里装的就是一个完整的子集。我们把这个子集的一个复制(使用list(temp)来确保我们复制的是内容,而不是引用)加到结果res中。
回溯:这个词听起来有点复杂,但实际上很简单。就是当我们考虑完一个数字(比如1)放入盒子后,我们会把它拿出来(temp.pop()),就好像我们从未做过这个决定一样。这样我们就可以回到之前的状态,然后考虑不放入1的情况。这个过程让我们能够探索所有可能的组合。
结束:当我们考虑完所有的数字后,res就包含了所有可能的子集。
python
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
res = []
def dfs(temp, index, nums):
# 直接添加当前子集到结果列表中,无需等到最后
res.append(list(temp)) # 使用list(temp)来复制当前子集
# 遍历从当前索引到末尾的所有元素
for i in range(index, len(nums)):
# 包含当前元素
temp.append(nums[i])
# 递归调用处理下一个元素
dfs(temp, i + 1, nums)
# 回溯:移除当前元素,尝试下一个选择
temp.pop()
# 开始递归
dfs([], 0, nums)
return(res)
221. 最大正方形
定义状态 :我们可以定义一个二维的DP数组dp,其中dp[i][j]代表以(i, j)为右下角的最大正方形的边长。注意,这个定义是关键的,因为它让我们能够通过左边、上边和左上角的状态来决定当前位置的状态。
找到状态转移方程 :对于每个位置(i, j),如果该位置的值是'1',那么dp[i][j]应该是其左边、上边和左上角的dp值中的最小值加1。这是因为,一个位置如果要构成一个更大的正方形,它的左边、上边和左上角也必须能构成正方形。数学表达式为:dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1。
初始化 :对于矩阵的第一行和第一列,dp[i][j]就直接是矩阵中的值(因为它们只能形成最大边长为1的正方形)。
遍历矩阵填充DP数组 :从(1, 1)开始遍历整个矩阵,并根据状态转移方程更新DP数组。
找到最大的dp[i][j]:遍历DP数组,找到最大的值,这个值的平方即为最大正方形的面积。
创建了一个辅助矩阵,元素是int,这样就解决了对原始矩阵元素转换类型的问题,同时还有在原始矩阵里修改元素影响实际计算的问题。然后针对元素为'1'时才开始计算,同时考虑边界情况。状态转移方程是根据每一步前面的上、左、左上角的累计最小值+1决定当前1的边长,这样在本身自己是一个单位的基础上和上面的合起来就是2,依次累加
python
class Solution:
def maximalSquare(self, matrix: List[List[str]]) -> int:
# 长宽,初始化最大边长
rows,cols = len(matrix), len(matrix[0])
max_side = 0
# 辅助矩阵,全0初始化
dp = [[0]*cols for _ in range(rows)]
# 遍历每个元素
for r in range(rows):
for c in range(cols):
if matrix[r][c] != '0': # 为1才进行计算
if r == 0 or c == 0: # 对于边界情况进行处理
dp[r][c] = 1
else: # 状态转移方程,假设当前是正方形的右下角,其值由左、上、左上的最小值决定
dp[r][c] = min(dp[r][c-1], dp[r-1][c], dp[r-1][c-1]) + 1
# 更新最大边长
max_side = max(max_side, dp[r][c])
# 边长2次方
return max_side ** 2