433. 最小基因变化
✨核心逻辑
本题采用 BFS(广度优先搜索) 的策略,用于在无权图中寻找最短路径(最少变异次数):
- 预处理基因库:将合法的基因序列
bank存储到哈希集合set中,以便在 O(1) 的时间内判断变异后的基因是否合法。 - 快速剪枝:如果目标基因
endGene本身就不在基因库set中,说明无论怎么变异都无法到达终点,直接返回-1。 - 逐层遍历(BFS):
- 使用队列
queue存放当前一层的基因序列。 - 每次从队列中弹出一个基因序列,尝试将其 8 个位置上的字符,分别替换为
A、C、G、T中的另外三个字符。 - 如果生成的新的基因序列已经访问过(在
set中),则将新序列放入队列,并从set中移除(避免后续重复访问和死循环)。 - 如果在尝试替换的过程中,刚好生成了目标基因
endGene,则说明找到了最短路径,直接返回当前步数 + 1。
- 使用队列
- 层数统计:每次处理完队列中的一层元素后,步数
step加 1。
🔥代码实现(含详细变量注释)
class Solution {
public int minMutation(String startGene, String endGene, String[] bank) {
// set:用于存储基因库中所有有效的基因序列,提供 O(1) 的快速查找和删除能力
Set<String> set = new HashSet<>();
// 将 bank 数组中的所有合法基因序列加入到 set 中
for (String string : bank) {
set.add(string);
}
// 极端条件:如果目标基因 endGene 不包含在基因库中,根本无法达成,直接返回 -1
if (!set.contains(endGene)) {
return -1;
}
// step:记录当前搜索的层数(即基因变化的最少次数)
int step = 0;
// xyz:定义基因突变中仅允许出现的 4 个有效字符
char[] xyz = {'A', 'C', 'G', 'T'};
// queue:用于 BFS 遍历的队列,存放当前层待尝试变异的基因序列
Queue<String> queue = new LinkedList<>();
// 将起始基因序列加入队列
queue.offer(startGene);
// BFS 核心循环:只要队列不为空,就持续向外扩散探索
while (!queue.isEmpty()) {
// size:记录当前队列中属于同一层级的所有序列数量
int size = queue.size();
// 遍历当前层的所有节点(按层遍历,保证步数最小)
for (int i = 0; i < size; i++) {
// poll:从队列中弹出一个当前的基因序列
String poll = queue.poll();
// 将字符串转为字符数组(字符串是不可变的,转为字符数组方便修改单个字符)
char[] ch = poll.toCharArray();
// 遍历 8 个位置的基因字符
for (int j = 0; j < 8; j++) {
// originalChar:记录当前位的原始字符,用于后续尝试完其他选项后恢复原状
char originalChar = ch[j];
// 尝试将第 j 位替换为 xyz 中的 4 个字符
for (char current : xyz) {
// 如果替换的字符和原来一样,则跳过
if (current == originalChar) {
continue;
}
// 修改当前位的字符
ch[j] = current;
// now:将修改后的字符数组转回新的基因序列字符串
String now = new String(ch);
// 如果经过一次变异,直接得到了目标基因 endGene
if (now.equals(endGene)) {
return step + 1;
}
// 如果新生成的基因序列在基因库 set 中,说明这是合法的一步
if (set.contains(now)) {
// 将其加入到 BFS 的下一层队列中
queue.offer(now);
// 从 set 中删除该序列,防止后续其他路径重复访问,形成死循环
set.remove(now);
}
// 回溯:恢复第 j 位的原始字符,准备尝试下一个不同的突变字符
ch[j] = originalChar;
}
}
}
// 当前层的所有节点都探索完毕后,表示完成了一次基因变化,步数加 1
step++;
}
// 如果 BFS 遍历完所有可能都没有找到 endGene,说明无法完成变异,返回 -1
return -1;
}
}
- ⏱️复杂度分析
时间复杂度:O(N * 8 * 4) = O(N),其中 N 是基因库 bank 的长度。每一次 BFS 扩展,我们要尝试 8 个位置的替换,每个位置有 4 种可能的字符(排除自身),属于常数级操作(32 次),所有有效的基因序列在队列中最多会被访问一次。
空间复杂度:O(N)。主要消耗在于存储基因库的哈希集合 set 和维护 BFS 队列 queue,最多需要存储 N 个基因序列。
🔍总结一下:
遇到了很多困惑:
- 这道题的“图“没有直接显示出来,是隐式的:是对 start 操作的过程中,如果符合 bank的元素,那么二者就有一条边,节点就是字符串
- set的作用:我们都知道,边在两个节点之间是可逆的,A-B 也是 B-A,这也就是 set 的作用,避免此情况的发生,记录已访问过的节点
3.为什么不直接 poll 一个元素之后进行操作,而是先遍历队列的 size 呢?因为是 BFS ,也就是当前层处理完之后,我才能让 step + 1,去处理下一层,如果没有外层 size 循环,就是poll 一个元素,操作之后 + 1,那就变成了操作的节点数量而不是一层一层的step,附件:
,
,