一.题目

二.思路讲解
2.1 思路讲解
本题要求从起始基因序列 start 通过最少次数的单字符突变,变为目标序列 end,且每次突变后的序列必须在基因库 bank 中。这本质上是一个 无权最短路径问题 ,适合用 广度优先搜索(BFS) 解决,因为 BFS 能保证第一次到达目标时所用的步数最少。
核心步骤:
-
使用哈希表标记已访问状态 :用一个
unordered_set<string> vis记录已经搜索过的基因序列,避免重复入队,防止死循环。 -
快速判断基因是否在库中 :将基因库
bank存入unordered_set<string> hash,这样每次检查突变后的序列是否合法时,只需 O(1) 时间。 -
枚举所有可能的突变 :对于当前序列的 每个位置 (共 8 个),将其字符分别替换为
'A'、'C'、'G'、'T'中的另外三种,生成所有可能的相邻序列。 -
BFS 层序遍历 :将起点入队,并标记已访问。逐层处理:每一层开始时,步数加 1;遍历当前队列中的所有序列,对每个序列枚举所有突变,若突变后的序列存在于基因库且未被访问,则将其加入队列并标记。一旦遇到突变后的序列等于
end,立即返回当前步数。 -
若队列为空仍未找到,则返回 -1。
三.代码演示
cpp
class Solution {
public:
int minMutation(string startGene, string endGene, vector<string>& bank)
{
unordered_set<string> vis;//用来标记已经搜索过的状态
unordered_set<string>hash(bank.begin(),bank.end());//存储基因库里面的字符串
string change = "ACGT";
//如果开始和结束一样返回0
if(startGene == endGene) return 0;
//如果结束不在库里面那么就肯定变化不成功
if(!hash.count(endGene)) return -1;
queue<string> q;
q.push(startGene);
vis.insert(startGene);//标记一下
int ret = 0;//统计变化格式
while(q.size())
{
ret++;
int sz = q.size();
for(int f = 0;f < sz;f++)
{
string t = q.front();
q.pop();
for(int i = 0;i < 8;i++)
{
string tmp = t;//很重要
for(int j = 0;j < 4;j++)
{
tmp[i] = change[j];
//判断修改后的是否在库里面,且这个是第一次出现
if(hash.count(tmp) && !vis.count(tmp))
{
if(tmp == endGene) return ret;
q.push(tmp);
vis.insert(tmp);
}
}
}
}
}
return -1;
}
};
四.代码讲解
一、数据结构与初始化
-
vis:unordered_set<string>,用于记录已经搜索过的基因序列,避免重复访问,防止无限循环。 -
hash:unordered_set<string>,将基因库bank转换为哈希集合,便于 O(1) 判断一个序列是否在库中。 -
change:字符串"ACGT",存储所有可能出现的字符,用于枚举突变时替换原字符。 -
边界判断 :如果
startGene等于endGene,直接返回 0(无需变化)。如果endGene不在基因库中,则不可能通过合法突变得到,返回 -1。
二、BFS 队列初始化
-
创建队列
queue<string> q,将起始基因startGene入队。 -
将
startGene插入vis集合,标记为已访问。 -
定义步数变量
ret = 0,用于记录当前 BFS 层数(即已经完成的突变次数)。
三、BFS 层序遍历过程
当队列不为空时,进行以下操作:
-
进入新的一层 :
ret++,表示即将探索的是距离起点ret步的基因序列。 -
记录当前层节点数 :
int sz = q.size(),确保只处理当前层的节点。 -
遍历当前层所有节点:
-
取出队头基因序列
t,并弹出队列。 -
对序列的每一个字符位置(共 8 个位置),尝试所有可能的突变:
-
复制当前序列到
tmp(因为每个位置需要独立尝试不同的字符)。 -
对于
change中的每一个字符c,将tmp的该位置替换为c,得到新序列tmp。 -
检查新序列是否在基因库中(
hash.count(tmp))且未被访问过(!vis.count(tmp))。-
如果新序列就是目标
endGene,则直接返回ret(因为 BFS 第一次遇到目标时就是最短步数)。 -
否则,将新序列入队,并加入
vis集合标记已访问。
-
-
-
-
继续下一层 :当当前层的所有节点处理完毕后,回到循环开始,
ret++进入下一层。
四、BFS 结束与结果返回
- 如果 BFS 过程中从未遇到目标序列,则当队列为空时循环结束,返回 -1,表示无法通过合法突变得到
endGene。
五、关键细节
-
BFS 的层序性质 :BFS 按层扩散,第一次访问到目标序列时所经过的层数就是最少变化次数。因此,在扩展时一旦遇到
endGene立即返回当前步数。 -
避免重复访问 :通过
vis集合记录已访问的序列,防止同一序列多次入队,保证 BFS 的收敛性。 -
枚举所有突变:每个位置的字符有 4 种可能,但原字符本身不需要再尝试(但是为了保证方便,且vis会把重复的扣住)
-
复制字符串 :
string tmp = t;必须在每次位置变化前复制,因为后续会对tmp进行修改,不能影响原始序列t。
五、流程图
