初学python的我开始Leetcode题15-2
提示:100道LeetCode热题15-2主要是动态规划相关,包括三题:完全平方数、零钱兑换、单词拆分。由于初学,所以我的代码部分仅供参考。
前言
上一次的三道动态规划是常见基本练习,这次学习三个新的题目~
粘贴题目有点黑乎乎的,试图在csdn更改格式,后来发现还不如直接粘贴方便易看,毕竟改成正文格式的话次方数还不会展示出LaTeX结果出来/(ㄒoㄒ)/~~
提示:以下是本篇文章正文内容,下面结果代码仅供参考
题目1:完全平方数
1.题目要求:
题目如下:
给你一个整数
n
,返回 和为n
的完全平方数的最少数量 。完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,
1
、4
、9
和16
都是完全平方数,而3
和11
不是。示例 1:
输入:n =12
输出:3 解释:12 = 4 + 4 + 4
示例 2:
输入:n =13
输出:2 解释:13 = 4 + 9
提示:
1 <= n <=
代码框架已经提供如下:
class Solution(object):
def numSquares(self, n):
"""
:type n: int
:rtype: int
"""
2.结果代码:
class Solution(object):def numSquares(self, n):""":type n: int:rtype: int"""dp = [float('inf')] * (n + 1)dp[0] = 0max_square = int(n ** 0.5) # 兼容低版本squares = [j * j for j in range(1, max_square + 1)]for i in range(1, n + 1):for s in squares:if s > i:breakdp[i] = min(dp[i], dp[i - s] + 1)return dp[n]
说明:
1. 题目到底让我们干什么?
给你一个数字 n
,问“最少”需要几个完全平方数(1、4、9、16…)加起来正好等于 n
。
举例:
n = 12 → 最少 3 个数:4 + 4 + 4
n = 13 → 最少 2 个数:4 + 9
2. 为什么用“动态规划”?
想象你有一张表,第 i
格写着“和为 i
的最少平方数个数”。
我们从 i = 0
开始,一格一格填到 i = n
,最后第 n
格里的数字就是答案。
3. 先搭好“表”
dp = [float('inf')] * (n + 1) # 先全部写成“无穷大”,表示暂时不知道
dp[0] = 0 # 和为 0 当然用 0 个数
dp[i]
就是上面说的“第i
格”。先把它们都设成“无穷”,后面再慢慢改小。
4. 把所有“可能用到的平方数”列出来
max_square = int(n ** 0.5) # 计算最大平方根,向下取整
squares = [j * j for j in range(1, max_square + 1)]
如果
n = 12
,max_square = 3
,所以squares = [1, 4, 9]
。这些就是后面能用的“砖块”。
5. 开始填表(核心!)
for i in range(1, n + 1): # 从 1 填到 nfor s in squares: # 每个 i 都试一下所有平方数if s > i: # 平方数太大了,没必要再试breakdp[i] = min(dp[i], dp[i - s] + 1)
这句 dp[i] = min(...)
最关键:
含义:如果我用了一块
s
(比如 4),那剩下的i - s
(比如 12 - 4 = 8)早就算过,用了dp[8]
个数;再加上刚用的这块 4,就是dp[8] + 1
个数。把所有可能的
s
都试一遍,取最小值,就能更新dp[i]
。
6. 答案在哪?
填完表后,dp[n]
就是和为 n
的最少平方数个数,直接返回即可:
return dp[n]
题目2:零钱兑换
1.题目要求:
题目如下:
给你一个整数数组
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提示:
1 <= coins.length <= 12
1 <= coins[i] <=
- 1
0 <= amount <=
代码框架已经提供如下:
class Solution(object):
def coinChange(self, coins, amount):
"""
:type coins: List[int]
:type amount: int
:rtype: int
"""
2.结果代码:
class Solution(object):def coinChange(self, coins, amount):""":type coins: List[int]:type amount: int:rtype: int"""# dp[i] 表示凑出金额 i 所需的最少硬币数dp = [float('inf')] * (amount + 1)dp[0] = 0 # 初始条件# 从小到大枚举金额for i in range(1, amount + 1):for c in coins:if c <= i:dp[i] = min(dp[i], dp[i - c] + 1)return dp[amount] if dp[amount] != float('inf') else -1
说明:
设 dp[i]
表示凑出金额 i
所需的最少硬币数。
状态转移:对于每个金额 i
,枚举所有硬币 c
(c ≤ i
),则
dp[i] = min(dp[i], dp[i - c] + 1)
初始条件:dp[0] = 0
(金额 0 需要 0 枚硬币),其余置为无穷大。
若最后 dp[amount]
仍是无穷大,说明无解,返回 -1
。
但是,完成后发现别人有用时、内存利用更好的方法,如下:
class Solution:def coinChange(self, coins, amount) :dp = 1 << amountresult = 0while dp & 1 == 0:t = dpfor c in coins:dp |= t >> cif dp == t:return -1result += 1return result
啊w(゚Д゚)w!
这段代码其实用的是 位运算 + BFS 的思想捏...
它把“金额”映射到 一个整数的二进制位 上,从而用 一个 32/64 位整数(甚至 Python 的“大整数”)来同时记录 所有金额 的可达性,因此:
空间 极致压缩:
只用一个整数变量dp
(外加少量辅助变量),而传统数组 DP 需要O(amount)
的整型数组。位运算并行:
一条t >> c
就能一次性把 所有已可达的金额 再“向后平移c
”,相当于一次 批量 BFS。
在 CPU 的 64 位寄存器里,一次移位 / 或运算就能处理 64 个状态,常数极小。
拆解一下代码,试图用笨脑子理解中...
dp = 1 << amount # 1. 把第 amount 位设为 1
result = 0
while dp & 1 == 0: # 2. 只要第 0 位还是 0(金额 0 还没到达)t = dp # 3. 记录上一轮所有可达的金额(位图)for c in coins: # 4. 对于每种硬币dp |= t >> c # 5. 把“当前所有可达金额 - c” 也设为可达if dp == t: # 6. 一轮下来没有任何新金额可达 → 无解return -1result += 1 # 7. 步数 +1
return result # 8. 第 0 位已置 1,返回步数
举个例子(amount = 11, coins = [1,2,5])
初始:
dp = 1<<11 = 0b100000000000
(只有 11 这一位是 1)第 1 轮(步数 1)
用硬币 1:右移 1 → 把 10 置 1
用硬币 2:右移 2 → 把 9 置 1
用硬币 5:右移 5 → 把 6 置 1
得到dp = 0b100100100100
(11,10,9,6 可达)… 依次类推
当第 0 位第一次变成 1 时,说明凑出了 0 元,此时result
就是答案。
的确快哈:
cache 友好:只需要几个寄存器级操作,没有随机内存访问。
并行度:一次位运算同时更新 32/64 个状态,相当于一次 向量化 BFS。
常数极小:没有 Python 层级的数组下标读写,移位和或运算都是 C 级指令。
但是也有缺点:
当
amount
很大(> 10^5 甚至 10^6)时,Python 的大整数会变慢,内存也会膨胀。可读性较差,不如数组 DP 直观。所以懂动态规划方法也很好啦~
题目3:单词拆分
1.题目要求:
题目如下:
给你一个字符串
s
和一个字符串列表wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出s
则返回true
。注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"] 输出: true 解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。示例 2:
输入: s = "applepenapple", wordDict = ["apple", "pen"] 输出: true 解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。注意,你可以重复使用字典中的单词。示例 3:
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] 输出: false提示:
1 <= s.length <= 300
1 <= wordDict.length <= 1000
1 <= wordDict[i].length <= 20
s
和wordDict[i]
仅由小写英文字母组成wordDict
中的所有字符串 互不相同
代码框架已经提供如下:
class Solution(object):
def wordBreak(self, s, wordDict):
"""
:type s: str
:type wordDict: List[str]
:rtype: bool
"""
2.结果代码:
class Solution(object):def wordBreak(self, s, wordDict):""":type s: str:type wordDict: List[str]:rtype: bool"""word_set = set(wordDict) # 用 set 加速查找n = len(s)dp = [False] * (n + 1) # dp[i] 表示 s[:i] 能否被拼出dp[0] = True # 空串一定为 True# 依次考察子串结束位置 ifor i in range(1, n + 1):# 枚举最后一个单词的起始位置 jfor j in range(i):if dp[j] and s[j:i] in word_set:dp[i] = Truebreak # 找到一个即可提前跳出return dp[n]
说明:
设 dp[i]
表示 字符串 s
的前 i
个字符(即 s[:i]
)能否被字典中的单词拼接而成。
状态转移:枚举最后一个单词 word
的起始下标 j
,只要
s[j:i] == word
且dp[j]
为真,
那么dp[i]
就为真。
时间复杂度:O(n² * m)
,其中 n = len(s)
,m
为单词平均长度;
空间复杂度:O(n)
。
总结
针对动态规划的三种题型进行了学习,了解了部分有关动态规划与python的相关知识,大家加油!