这道题要求:从一个起始基因串 startGene 变异到目标基因串 endGene,每次变异只能改变一个字符,而且变异后的新串必须出现在给定的基因库 bank 中,问最少需要多少次变异;如果无法到达,返回 -1。algo+1
节点:每一个合法的基因串(startGene、endGene 和 bank 里的串)。algo
边:如果两个基因串恰好有 1 个字符不同(汉明距离为 1),并且都在合法集合里,那么它们之间存在一条"可以一步变异"的边。algo
每条边的权重都是 1(一次变异就是一步),所以这是一个无权图上的最短路径问题,天然适合用 BFS 求最短步数。vultr+1
题目关键点回顾
围绕题意,有几个容易搞混的点需要确认清楚:vultr+1
中间每一步都必须在 bank 里
除了起始的 startGene 可以不在 bank 之外,变异过程中每一步产出的新基因,都必须在 bank 里,否则这一步变异是无效的,不能走这条边。zxi.mytechroad+1
一次变异只允许改一个字符
例如 AACCGGTT → AACCGGTA 是一次变异,因为只改了最后一位;两位以上不同就不是"一步变异"。leetcode+1
不能只看 start 和 end 的不同字符数
即便两个串相差 k 位,理论上的"最少至少 k 步",但题目还要求"每一步中间状态都在 bank 里"。可能存在某些位的修改导致中间串不在 bank,这条直接按位改的路径就走不通,需要绕路,甚至可能根本走不到。dev+1
所以,本质就是:
在一个隐式定义的图上,求从 startGene 到 endGene 的最短路径长度。
显式建图 vs 隐式建图
你一开始的直觉是「先遍历 bank 建图,再在图上跑 BFS」,思路完全正确:vultr+1
- 写一个
can_mutations(from, to)判断两个串是否只差一位。 - 遍历 bank 里的所有字符串,两两检查
can_mutations,如果能一位变异就连一条边。 - 再把能从 startGene 一步变过去的节点当作 BFS 起点,求到 endGene 的最短步数。zxi.mytechroad+1
这个叫显式建图 :把节点和边都事先算好,用邻接表存下来,然后标准 BFS 遍历邻接表。geeksforgeeks+1
更推荐的写法是隐式建图 :algocademy+1
- 不提前存所有边。
- 每次从队列里取出一个基因
gene,现算出它的所有"合法邻居":- 依次枚举 8 个位置。
- 在当前位尝试改成 A/C/G/T 四种字符(跳过改成原字符的情况)。
- 生成的新串如果在
bankSet里,并且没访问过,就当作邻居入队。devexcode+1
两者的本质没有区别:
- 节点不变,边的定义不变。
- 显式建图是"先把邻居全列出来,存进
adj[u]"; - 隐式建图是"用规则 + bankSet 即时算出邻居,不提前保存 adj"。codeforces+1
很多 BFS 题(例如棋盘四方向走、数字加减一、密码盘旋转等)都是这种"隐式图 + 即时枚举邻居"的写法,这是非常常见的模式。algocademy+1
BFS 两种写法:step 入队 vs 用 queue.size 控层
你原来熟悉的是"标准层序 BFS":geeksforgeeks+1
- 外层 while queue 不空。
- 每轮先记
levelSize = queue.size(),只处理这一层的 levelSize 个节点。 - 这一层处理完后,统一
step++,表示进入下一层。
在这道题上,常见的实现有两种,都正确:
写法一:把 step(层数)绑在队列元素上
队列里存 (gene, step),每次出队时顺便拿到该节点的"距离起点的步数":algomap+1
- 初始化:
queue.push( (startGene, 0) )。 - 每次出队
(gene, step):- 如果
gene == endGene,直接返回 step。 - 否则,生成所有邻居 newGene,入队
(newGene, step + 1)。
- 如果
BFS 的性质保证:
第一次弹出 endGene 时的 step 就是最短路径的长度。 wikipedia+1
写法二:不把 step 入队,用 queue.size 控制层
这就是你常说的"标准 BFS"方式,用一个外部 step 变量配合 levelSize 来表示当前层数:algo+1
- 初始化:
step = 0,queue.push(startGene)。 - 每轮:
levelSize = queue.size(),只循环 levelSize 次,把这一层所有节点都出队并扩展。- 所有新生成的邻居入队,属于下一层。
- 这一层处理完之后,统一
step++。
两种写法只是"层信息放在哪里"不同:
- 写法一:层信息放在节点上(队列里每个元素自带 step)。
- 写法二:层信息放在外层循环(levelSize 控制"这一层节点的个数")。
逻辑完全等价,任意选择一种即可。stackoverflow+1
枚举邻居的 BFS 伪代码
下面给出一个完整的"枚举 8×4 + BFS"的伪代码版本,采用写法一(step 入队),易于配合"遇到 end 立刻返回"的早停逻辑:devexcode+1
text
function minMutation(start, end, bank):
bankSet = set(bank) // 存所有合法基因,便于 O(1) 查询
if end not in bankSet:
return -1 // 目标都不在 bank 里,基本不可能到达
chars = ['A', 'C', 'G', 'T']
// 队列元素: (当前基因字符串, 从 start 走到这里用了多少步)
queue = new Queue()
queue.push( (start, 0) )
visited = set()
visited.add(start)
while queue 非空:
gene, step = queue.pop_front()
if gene == end:
return step // BFS 第一次到 end 就是最短步数
// 从 gene 生成所有"一步变异"的邻居
for i from 0 to 7: // 基因长度固定为 8
original = gene[i]
for c in chars: // 尝试 A/C/G/T
if c == original:
continue // 不要生成和自己一样的串
newGene = gene 拷贝一份
newGene[i] = c // 改第 i 位字符
if newGene 在 bankSet 中 且 newGene 不在 visited:
visited.add(newGene)
queue.push( (newGene, step + 1) )
// 队列空了还没到 end,说明无法通过 bank 变异到 end
return -1
如果你更习惯"用 queue.size() 控层"的写法,可以改成写法二,大致如下(只展示层的控制):takeuforward+1
text
function minMutation(start, end, bank):
bankSet = set(bank)
if end not in bankSet:
return -1
chars = ['A', 'C', 'G', 'T']
queue = new Queue()
queue.push(start)
visited = set()
visited.add(start)
step = 0
while queue 非空:
levelSize = queue.size() // 当前层的节点数
for k from 1 to levelSize:
gene = queue.pop_front()
if gene == end:
return step // 当前层就是距离 step
// 扩展当前节点的所有邻居到"下一层"
for i from 0 to 7:
original = gene[i]
for c in chars:
if c == original:
continue
newGene = gene 拷贝一份
newGene[i] = c
if newGene 在 bankSet 且 newGene 不在 visited:
visited.add(newGene)
queue.push(newGene)
step = step + 1 // 这一层处理完了,进入下一层
return -1
为什么"枚举 8×4 不会太多"?
直觉上可能觉得"对每个基因枚举 8 个位置 × 4 种字符会生成很多串",但在这道题的约束下,这个开销实际上是常数级的:dev+1
- 每个基因长度固定为 8,所以最多尝试 8×4=32 个候选串。
- 还会过滤掉"改回原字符"的情况,实际少于 32。
- 每个候选需要做的就是:看它是否在 bankSet 里、是否访问过,不在就直接丢弃。
- 题目约束
bank.length <= 10,整个状态空间非常小,本质的复杂度是 O(|bank|) 级别。 - 和你"遍历 bank + can_mutations 去判边"的方案在数量级上是一样的,只是编码方式不一样而已。zxi.mytechroad+1
小结
- 这题本质是:在"基因串为节点、一位变异为边"的无权图上,找 startGene 到 endGene 的最短路径,典型 BFS 模板题。algo+1
- 可以显式建图(预先算出哪些串互相一位可达),也可以隐式建图(用"枚举 8×4 + bankSet"即时生成邻居),推荐后者,代码更简单通用。devexcode+1
- BFS 层数既可以存进队列元素
(gene, step),也可以用queue.size()控制每层节点数,两种写法完全等价,按自己的习惯选择即可。khanacademy+1