你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组 nums
,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
示例 1:
输入:nums = [1,2,3,1]
输出:4
解释:偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3),偷窃到的最高金额 = 1 + 3 = 4 。
示例 2:
输入:nums = [2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋(金额 = 2),偷窃 3 号房屋(金额 = 9),接着偷窃 5 号房屋(金额 = 1),偷窃到的最高金额 = 2 + 9 + 1 = 12 。
利用动态规划思想,通过滚动变量优化空间复杂度至 (O(1))。
思路:
定义两个变量 first
和 second
,分别表示偷到前前一间房屋和前一间房屋的最大金额。对于当前房屋,有两种选择:
first + 当前房屋金额
。second
。代码:
class Solution:
def rob(self, nums: List[int]) -> int:
if not nums:
return 0
if len(nums) == 1:
return nums[0]
first, second = nums[0], max(nums[0], nums[1])
for i in range(2, len(nums)):
first, second = second, max(second, first + nums[i])
return second
复杂度分析:
答案解析:
first
为第一间房屋金额,second
为前两间房屋的较大值。first
和 second
,first
代表前前一间的最大值,second
代表前一间的最大值。second
即为偷到最后一间房屋时的最大金额,返回该值。给定正整数 ( n ),找到若干个完全平方数(比如 ( 1, 4, 9, 16, \dots ))使得它们的和等于 ( n ),要求组成和的完全平方数的个数最少。
思路:
定义 ( dp[i] ) 表示组成 ( i ) 的最少完全平方数个数。对于每个 ( i ),遍历小于 ( i ) 的完全平方数 ( j^2 ),通过状态转移方程 ( dp[i] = \min(dp[i], dp[i - j^2] + 1) ) 计算最小值。
代码:
import math
def numSquares(n):
dp = [float('inf')] * (n + 1)
dp[0] = 0
for i in range(1, n + 1):
max_j = int(math.sqrt(i))
for j in range(1, max_j + 1):
if i >= j * j:
dp[i] = min(dp[i], dp[i - j * j] + 1)
return dp[n]
解释:
dp[0] = 0
表示 ( 0 ) 不需要任何完全平方数。时间复杂度:( O(n\sqrt{n}) ),遍历 ( n ) 次,每次遍历 ( \sqrt{n} ) 次。
空间复杂度:( O(n) ),存储 ( dp ) 数组。
此方法通过动态规划高效地计算出最少完全平方数个数,确保了算法的正确性和效率。
给你两个单词 word1
和 word2
,请返回将 word1
转换成 word2
所使用的最少操作数。你可以对一个单词进行如下三种操作:
from typing import List
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
# 创建二维数组,dp[i][j]表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符的最少操作数
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化边界条件
for i in range(m + 1):
dp[i][0] = i # word1 前 i 个字符转换为空字符串,需要删除 i 个字符
for j in range(n + 1):
dp[0][j] = j # 空字符串转换为 word2 前 j 个字符,需要插入 j 个字符
# 填充 dp 数组
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i - 1] == word2[j - 1]:
# 当前字符相同,不需要操作,直接取左上角的值
dp[i][j] = dp[i - 1][j - 1]
else:
# 取插入、删除、替换三种操作的最小值,然后加 1(因为进行了一次操作)
dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
return dp[m][n]
动态规划思路:
dp[i][j]
表示将 word1
的前 i
个字符转换为 word2
的前 j
个字符的最少操作数。word1
为空字符串(i = 0
),则需要插入 j
个字符才能转换为 word2
的前 j
个字符,即 dp[0][j] = j
。word2
为空字符串(j = 0
),则需要删除 i
个字符才能将 word1
的前 i
个字符转换为空字符串,即 dp[i][0] = i
。word1[i - 1] == word2[j - 1]
时,当前字符相同,不需要进行插入、删除或替换操作,所以 dp[i][j] = dp[i - 1][j - 1]
。word1[i - 1] != word2[j - 1]
时,有三种操作选择:
word1
的前 i
个字符转换为 word2
的前 j - 1
个字符(dp[i][j - 1]
),再插入一个字符,操作数为 dp[i][j - 1] + 1
。word1
的前 i - 1
个字符转换为 word2
的前 j
个字符(dp[i - 1][j]
),再删除一个字符,操作数为 dp[i - 1][j] + 1
。word1
的前 i - 1
个字符转换为 word2
的前 j - 1
个字符(dp[i - 1][j - 1]
),再替换一个字符,操作数为 dp[i - 1][j - 1] + 1
。dp[i][j] = 1 + min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
。复杂度分析:
word1
和 word2
的长度,需要遍历一个 ( (m + 1) \times (n + 1) ) 的二维数组。给定两个单词 word1
和 word2
,计算将 word1
转换成 word2
所需的最少操作数。允许的操作有三种:
from typing import List
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
# 创建二维数组 dp,其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符的最少操作数
dp = [[0] * (n + 1) for _ in range(m + 1)]
# 初始化边界条件:当其中一个单词为空时的操作数
for i in range(m + 1):
dp[i][0] = i # 删除 word1 前 i 个字符(全部删除,操作数为 i)
for j in range(n + 1):
dp[0][j] = j # 插入 word2 前 j 个字符(全部插入,操作数为 j)
# 填充 dp 数组
for i in range(1, m + 1):
for j in range(1, n + 1):
if word1[i-1] == word2[j-1]:
# 当前字符相同,无需操作,直接继承左上角的结果
dp[i][j] = dp[i-1][j-1]
else:
# 当前字符不同,选择三种操作中的最小值并加 1(当前操作)
dp[i][j] = 1 + min(
dp[i-1][j], # 删除 word1 的第 i 个字符(对应 word1 前 i-1 转换为 word2 前 j)
dp[i][j-1], # 插入 word2 的第 j 个字符(对应 word1 前 i 转换为 word2 前 j-1)
dp[i-1][j-1] # 替换 word1 的第 i 个字符为 word2 的第 j 个字符(对应前 i-1 和 j-1 转换)
)
return dp[m][n] # 返回最终结果,即两个完整单词的最少操作数
编辑距离问题是典型的动态规划问题,核心是通过子问题的解推导原问题的解。
dp[i][j]
表示将 word1
的前 i
个字符转换为 word2
的前 j
个字符所需的最少操作数。word1[i-1] == word2[j-1]
),则无需操作,直接继承左上角的状态 dp[i-1][j-1]
。word1
的第 i
个字符,操作数为 dp[i-1][j] + 1
。word1
中插入 word2
的第 j
个字符,操作数为 dp[i][j-1] + 1
。word1
的第 i
个字符替换为 word2
的第 j
个字符,操作数为 dp[i-1][j-1] + 1
。word1
为空(i=0
)时,需要插入 word2
的前 j
个字符,操作数为 j
。word2
为空(j=0
)时,需要删除 word1
的前 i
个字符,操作数为 i
。m
和 n
分别为两个单词的长度。需要遍历一个 ( (m+1) \times (n+1) ) 的二维数组。以 word1 = "horse", word2 = "ros"
为例:
0,1,2,3
和 0,1,2,3,4,5
(对应插入或删除操作)。h
vs r
不同,o
vs o
相同),根据状态转移方程逐步计算每个 dp[i][j]
。dp[5][3]
即为结果 3
(实际最少操作:horse
→ rorse
(替换 h→r)→ rose
(删除 r)→ ros
(删除 e),共 3 步)。编辑距离问题通过动态规划将复杂的字符串转换问题分解为子问题,利用状态转移方程高效求解。关键在于正确定义状态和转移逻辑,边界条件的处理也至关重要。该解法是此类问题的经典解法,时间和空间复杂度均为多项式级别,适用于大多数实际场景。
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
示例 1:
输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步到下标 1,然后跳 3 步到达最后一个下标。
示例 2:
输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置(值为 0),无法继续跳跃到最后一个下标。
通过维护当前能到达的最远位置,遍历数组时不断更新该位置,若在遍历过程中最远位置覆盖最后一个下标,则返回 True
;若当前位置超过最远位置,说明无法继续跳跃,返回 False
。
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
n = len(nums)
max_reach = 0 # 初始化当前能到达的最远位置
for i in range(n):
# 如果当前位置在可到达范围内,则尝试更新最远位置
if i <= max_reach:
max_reach = max(max_reach, i + nums[i])
# 提前判断是否已到达或超过最后一个下标,优化性能
if max_reach >= n - 1:
return True
else:
# 当前位置超出可到达范围,无法继续跳跃
break
# 遍历结束后,若最远位置仍未到达最后一个下标,返回False
return max_reach >= n - 1
贪心策略:
每次遍历到位置 i
时,若 i
在当前最远可达位置 max_reach
内,则更新 max_reach
为 i + nums[i]
(即从 i
出发能到达的最远位置)和当前 max_reach
中的较大值。
max_reach
在遍历过程中已覆盖最后一个下标(n-1
),则直接返回 True
,无需继续遍历。i
超出 max_reach
,说明后续位置无法到达,直接跳出循环并返回 False
。边界处理:
0
或 1
时,直接返回 True
(空数组题目保证输入合法,长度为 1 时无需跳跃即可到达)。i
超过 max_reach
,说明中间存在无法跨越的“断层”,直接终止遍历。nums
的长度。仅需遍历数组一次,每个元素处理时间为常数。max_reach
和循环变量)。以示例 2 nums = [3,2,1,0,4]
为例:
max_reach = 0
。i=0
:0 <= 0
,更新 max_reach = max(0, 0+3=3)
,此时 max_reach=3
,未达最后下标(4)。i=1
:1 <= 3
,更新 max_reach = max(3, 1+2=3)
,仍为 3。i=2
:2 <= 3
,更新 max_reach = max(3, 2+1=3)
,仍为 3。i=3
:3 <= 3
,更新 max_reach = max(3, 3+0=3)
,仍为 3。i=4
:4 > 3
,跳出循环,返回 False
,符合预期。True
,优化最坏情况下的性能。该解法通过线性遍历和常数空间,高效解决了跳跃游戏问题,是此类问题的经典贪心解法。
给定一个非负整数数组 nums
,你最初位于数组的 第一个下标。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标。
通过维护当前能到达的最远位置,遍历数组时动态更新该位置,若在遍历过程中最远位置覆盖最后一个下标,则直接返回 True
;若当前位置超出最远可达范围,说明无法继续跳跃,返回 False
。
from typing import List
class Solution:
def canJump(self, nums: List[int]) -> bool:
n = len(nums)
max_reach = 0 # 当前能到达的最远下标
for i in range(n):
# 如果当前位置在可达范围内,则尝试更新最远可达位置
if i <= max_reach:
max_reach = max(max_reach, i + nums[i])
# 提前判断是否已到达终点,优化性能
if max_reach >= n - 1:
return True
else:
# 当前位置不可达,后续位置也无法到达
break
# 遍历结束后,检查最远可达位置是否覆盖终点
return max_reach >= n - 1
维护最远可达位置:
用 max_reach
表示从起点出发,经过一系列跳跃后能到达的最远下标。初始时 max_reach = 0
(起点位置)。
i
,若 i
在 max_reach
范围内(即 i <= max_reach
),说明可以从前面的某个位置跳跃到 i
,此时更新 max_reach
为 i + nums[i]
(从 i
出发能跳到的最远位置)和当前 max_reach
中的较大值。i
超出 max_reach
(即 i > max_reach
),说明无法到达 i
,后续下标也无法到达,直接终止遍历。提前终止条件:
一旦 max_reach
覆盖最后一个下标(n - 1
),立即返回 True
,无需遍历剩余元素,优化最坏情况下的时间复杂度。
max_reach
和循环变量)。示例 1:nums = [2, 3, 1, 1, 4]
i=0
:0 <= 0
,max_reach = max(0, 0+2=2)
→ 2
(未达终点,继续)。i=1
:1 <= 2
,max_reach = max(2, 1+3=4)
→ 4
(已达终点 4
,返回 True
)。示例 2:nums = [3, 2, 1, 0, 4]
i=0
:0 <= 0
,max_reach = 3
。i=1
:1 <= 3
,max_reach = 3
(1+2=3
)。i=2
:2 <= 3
,max_reach = 3
(2+1=3
)。i=3
:3 <= 3
,max_reach = 3
(3+0=3
)。i=4
:4 > 3
,跳出循环,返回 False
。1
时,直接返回 True
(无需跳跃即可到达终点);当某位置不可达时,后续位置必然不可达,提前终止遍历。该解法通过线性扫描和常数空间,高效解决了跳跃游戏问题,是此类问题的最优解法。
给定一个非负整数数组 nums
,你最初位于数组的第一个位置,数组中的每个元素代表你在该位置可以跳跃的最大长度。目标是使用最少的跳跃次数到达最后一个位置。
通过维护当前跳跃的最远可达位置 max_reach
和当前跳跃的终点 end
,遍历数组时更新这些变量。当到达当前跳跃的终点时,跳跃次数加 1
,并将终点更新为新的最远可达位置。若最远可达位置已覆盖最后一个位置,提前结束遍历。
from typing import List
class Solution:
def jump(self, nums: List[int]) -> int:
n = len(nums)
steps = 0 # 跳跃次数
end = 0 # 当前跳跃的终点
max_reach = 0 # 目前能到达的最远位置
for i in range(n - 1):
max_reach = max(max_reach, i + nums[i])
if i == end:
steps += 1
end = max_reach
if end >= n - 1:
break # 已到达或超过最后一个位置,提前结束
return steps
该算法通过贪心策略,每次在可跳跃范围内找到最远可达位置,确保跳跃次数最少,高效解决问题。
一个机器人位于一个 m x n
网格的左上角,每次只能向下或者向右移动一步,试图达到网格的右下角。求总共有多少条不同的路径?
定义 dp[i][j]
表示到达网格 (i, j)
位置的不同路径数。
dp[0][j] = 1
(只能一直向右移动)。dp[i][0] = 1
(只能一直向下移动)。dp[i][j] = dp[i-1][j] + dp[i][j-1]
(从上方或左方到达)。from typing import List
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [[1] * n for _ in range(m)]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i-1][j] + dp[i][j-1]
return dp[m-1][n-1]
from typing import List
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
dp = [1] * n
for i in range(1, m):
for j in range(1, n):
dp[j] += dp[j-1]
return dp[n-1]
通过动态规划,利用状态转移方程高效计算路径数,确保每个位置的路径数由相邻位置推导而来,最终得到右下角的路径总数。