LeetCode 433. 最小基因变化
题目描述


题意分析
基因序列是长度为八的字符串,每个位置只能是
A、C、G、T。从起始序列变到目标序列,每次恰好改变一个位置,且改变后的整个序列必须出现在基因库bank中,返回最少变化次数;无法到达时返回-1。起点默认合法,即使不在基因库中也可以开始。后续每个中间序列都必须合法,不能只比较起点与终点有多少处不同,因为需要的中间序列可能缺失,也可能必须绕路。若起点已经等于终点,则不需要变化。
解法:BFS 求最少单字符变化次数
核心思路
[!blue]
把起点和基因库中的不同字符串看成节点。若库中的候选与当前状态恰好有一个位置不同,就存在指向该候选的一次变化边。每条边都只花一次变化,因此最少变化次数就是无权图中从起点到终点的最短距离。
使用 BFS,从变化次数零的起点开始逐层扩展。先处理所有一步可达的状态,再处理两步可达的状态,以此类推。第一次取出目标时,更短距离的全部可能已经处理过,所以当前距离就是最少次数,不能用 DFS 首次遇到目标代替这个保证。
本题基因库很小,不必预先创建整张图。每处理一个基因,就扫描库中的候选,逐位置比较差异,选出恰好差一位的邻居。Java 在发现第二处差异时提前停止;它的
c > 0也允许零处差异,但同一个字符串早已在访问集合中,因此实际新入队的仍只能是差一位的状态。访问标记必须在入队时设置,而不是等出队再设置。同一个状态可能被本层多个节点发现,第一次发现已经具有最短距离,后续再入队只会重复工作。起点也先标记,避免从基因库绕回起点形成循环。
Java 用当前层固定的队列长度统一管理
depth;Go 在每个队列项中直接保存距离。两者都让新邻居比当前状态多一次变化。队列耗尽还未遇到目标,就说明所有可达合法状态已经检查完,返回失败。
解题步骤
- 将起点和距离零加入队列,并标记已访问。
- 取出状态,若等于目标,返回其变化次数。
- 扫描基因库,找出恰好相差一位且尚未访问的候选。
- 候选入队时立即标记,将其距离设为当前距离加一。
- 队列为空仍未找到目标时返回
-1。
代码实现
class Solution {
public int minMutation(String startGene, String endGene, String[] bank) {
Deque<String> q = new ArrayDeque<>();
q.offer(startGene);
Set<String> vis = new HashSet<>();
vis.add(startGene);
int depth = 0;
while (!q.isEmpty()) {
for (int m = q.size(); m > 0; --m) {
String gene = q.poll();
if (gene.equals(endGene)) {
return depth;
}
for (String next : bank) {
int c = 2;
for (int k = 0; k < 8 && c > 0; ++k) {
if (gene.charAt(k) != next.charAt(k)) {
--c;
}
}
if (c > 0 && !vis.contains(next)) {
q.offer(next);
vis.add(next);
}
}
}
++depth;
}
return -1;
}
}
func minMutation(startGene string, endGene string, bank []string) int {
type pair struct {
s string
depth int
}
q := []pair{
{startGene, 0},
}
vis := map[string]bool{startGene: true}
for len(q) > 0 {
p := q[0]
q = q[1:]
if p.s == endGene {
return p.depth
}
for _, next := range bank {
diff := 0
for i := 0; i < len(startGene); i++ {
if p.s[i] != next[i] {
diff++
}
}
if diff == 1 && !vis[next] {
vis[next] = true
q = append(q, pair{next, p.depth + 1})
}
}
}
return -1
}
复杂度分析
- 时间复杂度:$O((m + 1)mL)$,
m为基因库长度,L = 8。最多访问起点及库中的m个不同状态,每次扫描全部候选并比较至多L个字符。- 空间复杂度:$O(m + 1)$,队列和访问集合保存已发现的状态,基因长度固定。
关键点总结
[!green]
- 合法序列构成状态图,单次变化代价相同,使 BFS 的层数等于最短距离。
- 起点可不在库中,但生成后续状态时必须从库中选。
- 首次入队就标记,避免重复发现改变队列规模或距离处理。
易错点总结
[!yellow]
- 只计算起点与终点的字符差异数,无法保证对应中间状态都在基因库中。
- 要求起点也必须出现在
bank,会错误排除题目允许的起始状态。- 出队后才设置访问标记,会让同一状态被多个父节点重复加入。
- Java 处理当前层时不断读取增长后的队列长度,会把下一层节点混入当前深度。
- 将零处差异也当作新的变化入队,会制造自环;现有 Java 实现由访问集合排除了这种情况。
- 找到目标不代表所有搜索方法都得到最短距离,这里依赖 BFS 按距离递增的访问顺序。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 127. 单词接龙 | 困难 | 同样构建单字符变化图并求最短路,本题字符串长度固定且 bank 较小,直接扫描候选即可。 |
| 752. 打开转盘锁 | 中等 | 同样在有限状态空间按单位代价搜索,区别是邻居由密码锁操作生成而非扫描 bank。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!