LeetCode 433. 最小基因变化
字数 2141 2025-12-08 00:46:01
LeetCode 433. 最小基因变化
题目描述
给定两个字符串 start 和 end,以及一个字符串数组 bank,bank 中存放了有效的基因序列列表。你的目标是进行基因变化,从 start 变化到 end。每次变化只能改变一个字符,并且变化后的基因序列必须存在于 bank 中。你需要找出从 start 变化到 end 所需的最少变化次数。如果无法完成变化,返回 -1。
示例:
start = "AACCGGTT"
end = "AACCGGTA"
bank = ["AACCGGTA"]
输出:1
解释:只需将 start 的最后一个字符 'T' 改为 'A',得到 end,且 end 在 bank 中,所以变化 1 次。
注意:
start和end长度相同,且长度范围为 [1, 8]。bank中的字符串长度与start相同,且不会重复。- 所有字符串仅由字符 'A', 'C', 'G', 'T' 组成。
解题过程循序渐进讲解
第一步:理解问题核心
这个问题本质上是一个最短路径搜索问题。
- 将每个基因序列视为一个状态节点。
- 如果两个基因序列之间只有一个字符不同,则它们之间有一条无向边(可以互相变化)。
- 从起始状态
start出发,通过若干次变化(即走过若干条边),到达目标状态end。 - 要求出最短路径长度(最少变化次数)。
- 注意:变化后的状态必须在
bank中(bank是合法状态的集合),但start可以不在bank中。 - 如果
end不在bank中,则直接返回 -1,因为不可能合法变化到end。
第二步:明确搜索思路
因为要找最短路径,自然想到用广度优先搜索(BFS)。BFS 从 start 出发,逐层探索,第一次遇到 end 时的层数就是最少变化次数。
为什么 BFS 合适?
- 每次变化代价为 1(即边权相同),BFS 可以保证首次到达时的路径最短。
- 状态空间不大:每个位置可以是 4 种字符,长度 ≤8,但实际合法状态数不超过
bank.size()≤ 10,所以 BFS 很快。
第三步:BFS 的具体步骤
- 预处理:将
bank存入一个哈希集合,以便 O(1) 判断一个状态是否合法。 - 检查
end是否在bank中,如果不在,直接返回 -1。 - 初始化队列
queue,用于 BFS,队列元素是(当前基因序列, 当前变化次数)。 - 初始化一个
visited集合,记录已访问过的状态,避免重复访问。 - 将
(start, 0)入队,并标记start为已访问。 - 当队列不为空时,循环:
- 弹出队首元素
(current, steps)。 - 如果
current == end,返回steps。 - 否则,生成
current的所有可能的下一个状态:对current的每个位置,尝试替换成另外 3 个字符('A','C','G','T' 中不同于当前字符的),得到新序列next。 - 如果
next在bank中且未被访问过,将其标记为已访问,并将(next, steps+1)入队。
- 弹出队首元素
- 如果 BFS 结束仍未找到
end,返回 -1。
第四步:举例说明 BFS 过程
以示例为例:
start = "AACCGGTT"
end = "AACCGGTA"
bank = ["AACCGGTA"]
- 初始化:
end在bank中,合法。visited={},队列初始为[("AACCGGTT", 0)]。 - 弹出
("AACCGGTT", 0),不等于end。 - 生成它的所有单字符变化:
- 改变位置 7(最后一个字符):'T' -> 'A' 得到 "AACCGGTA",这个序列在
bank中,且未访问过,将其入队("AACCGGTA", 1),标记已访问。
- 改变位置 7(最后一个字符):'T' -> 'A' 得到 "AACCGGTA",这个序列在
- 弹出
("AACCGGTA", 1),发现等于end,返回 1。
第五步:边界情况处理
- 如果
start == end,那么不需要变化,返回 0。 - 如果
end不在bank中,直接返回 -1,因为不可能合法到达。 - 如果
bank为空,且start != end,返回 -1。 - 在生成下一个状态时,注意排除与当前字符相同的情况,减少无谓判断。
第六步:复杂度分析
- 设基因序列长度为 L,
bank中基因个数为 N。 - 每个状态在 BFS 中最多被访问一次,状态数为 O(N)(包括
start)。 - 每个状态生成下一个状态时,需要遍历 L 个位置,每个位置尝试 3 个不同字符,所以每个状态最多生成 3L 个新序列。
- 检查新序列是否在
bank中(哈希集合 O(1)),并检查是否访问过(哈希集合 O(1))。 - 总时间复杂度 O(N * L),这里 L ≤ 8,N ≤ 10,所以非常快。
- 空间复杂度 O(N),用于存储
bank集合和visited集合。
第七步:实现细节(伪代码)
function minMutation(start, end, bank):
bankSet = set(bank)
if end not in bankSet:
return -1
queue = deque()
queue.append((start, 0))
visited = set()
visited.add(start)
charSet = ['A', 'C', 'G', 'T']
while queue:
current, steps = queue.popleft()
if current == end:
return steps
for i in range(len(current)):
for ch in charSet:
if ch == current[i]:
continue
nextGene = current[:i] + ch + current[i+1:]
if nextGene in bankSet and nextGene not in visited:
visited.add(nextGene)
queue.append((nextGene, steps+1))
return -1
第八步:进一步思考
这个问题也可以用双向 BFS 优化,但状态空间很小,普通 BFS 已足够。另外,如果 bank 很大,也可以预先建立邻接表,但本题不需要。