LeetCodeCampsDay38动态规划part06

最小个数问题:零钱兑换、完全平方数

求排列问题:单词拆分

322. 零钱兑换

https://leetcode.cn/problems/coin-change/

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

1
2
3
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1

示例 2:

1
2
输入:coins = [2], amount = 3
输出:-1

示例 3:

1
2
输入:coins = [1], amount = 0
输出:0

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 231 - 1
  • 0 <= amount <= 104

动态规划思路

完全背包、装满到target的最小物品数量的问题

  1. dp定义与下标含义,dp[i][j]表示用物品0~i装满到j的最少物品个数

  2. 递推公式:凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j](考虑coins[i])所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。

    递推公式:dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i]] + 1)

  3. 初始化:dp的大小:dp[len(coins)][amount + 1],并且全部初始化为无穷大;再对第一行、第一列进行初始化; 第一行,如果j能整除coins[0],则需要j//coins[0]个,否则初始化为0;第一列,全部为0

  4. 遍历顺序:先物品再容量,如果coins[i] > j,则dp[i][j] = dp[i - 1][j];否则dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i]] + 1)

  5. 举例

    0, 1, 2, 3, 4, 5, 6

    1 0, 1, 2, 3, 4, 5, 6

    2 0, 1, 1, 2, 2, 3, 3

动态规划代码

  • 时间复杂度: O(n * amount),其中 n 为 coins 的长度
  • 空间复杂度: O(amount)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# 完全背包、装满到target的最小物品数量

# dp定义与下标含义,dp[i][j]表示用物品0~i装满到j的最少物品个数

# 递推公式:
# 1,2,5, target = 11
# 原始:dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + val[i])
#
# 刚开始的想法,错误的
# val = j // coins[i]
# remain = j % coins[i]
# dp[i][j] = min(dp[i - 1][j], dp[i][remain] + val)

# 纠正后的想法,正确的
# dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i]] + 1)

# 初始化
# dp的大小:dp[len(coins)][amount + 1]
Lc = len(coins)
dp = [[float('inf')] * (amount + 1) for _ in range(Lc)]

# 对第一行,第一列进行初始化,其它位置初始化为0
# 第一行,如果j能整除coins[0],则需要j//coins[0]个,否则初始化为0
# 第一列,全部为0
for j in range(amount + 1):
if coins[0] <= j and j % coins[0] == 0:
dp[0][j] = j // coins[0]
else:
dp[0][j] = float('inf')

for i in range(Lc):
dp[i][0] = 0

# 遍历顺序,先物品再容量
for i in range(1, Lc):

for j in range(amount + 1):
if coins[i] > j:
dp[i][j] = dp[i - 1][j]
continue
dp[i][j] = min(dp[i - 1][j], dp[i][j - coins[i]] + 1)

if dp[-1][-1] != float('inf'):
return dp[-1][-1]
else:
return -1

# 举例
# 0, 1, 2, 3, 4, 5, 6
# 1 0, 1, 2, 3, 4, 5, 6
# 2 0, 1, 1, 2, 2, 3, 3
#



# 本题也可以使用贪心算法

279. 完全平方数

https://leetcode.cn/problems/perfect-squares/

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

示例 1:

1
2
3
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

示例 2:

1
2
3
输入:n = 13
输出:2
解释:13 = 4 + 9

提示:

  • 1 <= n <= 104

动态规划思路

和题目和322. 零钱兑换基本一样,完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品

物品应该只遍历完全平方数(我这里先求出了完全平方数的物品列表item)

  1. 数组与下标定义,dp[i][j]表示物品(完全平方数) 从0到i,构成完全平方数j的最小数量

  2. 递推公式:dp[i][j] = min(dp[i - 1][j], dp[i][j - item(i)] + 1); 卡哥说:dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。 此时我们要选择最小的dp[j],所以递推公式:dp[j] = min(dp[j - i * i] + 1, dp[j]);

  3. 初始化:dp[n][n + 1]全初始化为float(‘inf’), 第一行(数字1),构成从0到n,如果j大于数字1则为j//i,第一列全是0

  4. 遍历顺序,先物品再容量

  5. 0, 1, 2, 3, 4, 5, 6, 7, 8,

1 0, 1, 2, 3, 4, 5, 6, 7, 8

4 0, 1, 2, 3, 1, 2, 3, 4, 2

动态规划代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class Solution:
def numSquares(self, n: int) -> int:
# 完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?

# dp数组与下标定义,dp[i][j]表示物品(完全平方数) 从0到i,构成完全平方数j的最小数量


# 完全背包问题递推公式
# 原公式:dp[i][j] = max(dp[i - 1][j] , dp[i][j - weight[i]] + val[i])
# 本题公式
# dp[i][j] = min(dp[i - 1][j], dp[i][j - i] + 1)

# dp初始化
# dp[n][n + 1]全初始化为float('inf')
# 第一行(数字1),构成从0到n,如果j大于数字1则为j//i
# 第一列全是0

# 0, 1, 2, 3, 4, 5, 6, 7, 8,
# 1 0, 1, 2, 3, 4, 5, 6, 7, 8
# 4 0, 1, 2, 3, 1, 2, 3, 4, 2

# 物品应该只遍历完全平方数
def getSquartList(n: int) -> list:
res = list()
for i in range(1, int(n ** 0.5) + 1):
res.append(i * i)
return res

items = getSquartList(n)
L = len(items)

dp = [[float('inf')] * (n + 1) for _ in range(L)]

# 行初始化
for i in range(L):
dp[i][0] = 0

# 列初始化
for j in range(1, n + 1):
if j >= 1:
dp[0][j] = dp[0][j - 1] + 1
else:
dp[0][j] = 0


for i in range(1, L):
for j in range(n + 1):
if j < items[i]:
dp[i][j] = dp[i - 1][j]
else:
dp[i][j] = min(dp[i - 1][j], dp[i][j - items[i]] + 1)


return dp[-1][-1]

动态规划代码精简

1
2
3
4
5
6
7
8
9
10
11
class Solution:
def numSquares(self, n: int) -> int:
dp = [float('inf')] * (n + 1)
dp[0] = 0

for i in range(1, int(n ** 0.5) + 1): # 遍历物品
for j in range(i * i, n + 1): # 遍历背包
# 更新凑成数字 j 所需的最少完全平方数数量
dp[j] = min(dp[j - i * i] + 1, dp[j])

return dp[n]

139. 单词拆分

https://leetcode.cn/problems/word-break/

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

1
2
3
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

1
2
3
4
输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
注意,你可以重复使用字典中的单词。

示例 3:

1
2
输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

提示:

  • 1 <= s.length <= 300
  • 1 <= wordDict.length <= 1000
  • 1 <= wordDict[i].length <= 20
  • swordDict[i] 仅由小写英文字母组成
  • wordDict 中的所有字符串 互不相同

动态规划思路

把字符串s当成一个背包,wordDict就是物品,看能否刚好装满这个背包(每个物品可以使用多次),完全背包能否装满的问题

  1. dp含义与下标:dp[i][j],使用物品0~i组成一个长度为j的字符串,如果能组成就是true;如果使用一维dp,则dp[j]组成一个长度为j的字符串,如果能组成就是true
  2. 递推公式:本题里被遍历的对象是s里的每个字符,如s=applepenapple,则遍历:a, ap, app, appl, apple, applep…;需要双指针,一个i用来遍历每个字符,另一个j用来记录一个完整单词的起点;比如当前遍历到i='e’而j='p’即pe,再判断当前字段是否在wordDict中,而pe不在wordDict里;当遍历到i='n’而j='p’即pen,但pen在wordDict里,此时再判断dp[j]是否也是True,如果dp[j]为True表示j前面的词是可以被wordDict表示的,随后再令dp[i]=True;否则dp[i]=False
a p p l e p e n
j i

此时的当前字段为’pe’并不在wordDict中,令dp[i]为False

a p p l e p e n
j i

此时的字段’pen’在wordDict中,并且dp[j]为True(指的是p前面的apple词,通常情况下可能是appleapple等词是可以使用wordDict表达的),此时再令dp[i]=True;如果dp[j]为False,令dp[i]为False,表示哪怕pen可以被wordDict表示但前面的词不可以

  1. dp初始化, dp大小为[False] * (len(s) + 1),其中第一个为空字符串;而令dp[0]=True,表示空字符串是一定可以被表示的

  2. 遍历顺序:前面也有题目是组合数/排列数

    1. 如果问能否是否装满,还有两个子问题:

      1、排列数 – 先遍历背包再遍历物品

      2、组合数 – 先遍历物品再遍历背包

    2. 求组合数:动态规划:518.零钱兑换II

    3. 求排列数:动态规划:377. 组合总和 Ⅳ动态规划:70. 爬楼梯进阶版(完全背包)

    4. 求最小数:动态规划:322. 零钱兑换 动态规划:279.完全平方数

举例

输入

s =“leetcode”

wordDict = [“leet”,“code”]

标准输出

[True, False, False, False, False, False, False, False, False]

l

le

e

lee

ee

e

leet 找到第一个词

[True, False, False, False, True, False, False, False, False]

eet

et

t

leetc

eetc

etc

tc

c

leetco

eetco

etco

tco

co

o

leetcod

eetcod

etcod

tcod

cod

od

d

leetcode

eetcode

etcode

tcode

code 找到了第二个词

[True, False, False, False, True, False, False, False, True]

ode

de

e

最终输出

[True, False, False, False, True, False, False, False, True]

动态规划代码

  • 时间复杂度:O(n^3),最差情况n^3,最好n^2
  • 空间复杂度:O(n)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# 把字符串s当成一个背包,wordDict就是物品,看能否刚好装满这个背包(每个物品可以使用多次),完全背包能否装满的问题
# dp含义与下标,dp[i][j],使用物品0~i组成一个长度为j的字符串,如果能组成就是true

# 递推公式
# 原公式 dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + val[i])
# if

# 初始化
# dp[0][0] 一定为True
# 其它值都为False
dp = [False] * (len(s) + 1)
dp[0] = True
# 遍历顺序
# 如果问能否是否装满,还有两个子问题:
# 1、排列数 -- 先遍历背包再遍历物品
# 2、组合数 -- 先遍历物品再遍历背包
print(dp)


# 这也可以看成一个双指针?
# sIndex指针指向新单词起点,fast指针遍历每个字符, fast - sIndex就是一个可能的单词长度
for i in range(1, len(s) + 1):
# j其实是一个新单词的起点
for j in range(i):
word = s[j: i]
print(word)
if word in wordDict and dp[j] == True:
print(word, j, i)
dp[i] = True
print(dp)
return dp[-1]

# "" a , ap, app, appl, apple, applep, applepe, applepen
# apple 1 0 0 0 0 1
# pen