《LeetCode 433 最小基因变化 单源BFS解法》

一.题目

433. 最小基因变化 - 力扣(LeetCode)

二.思路讲解

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;
    }
};

四.代码讲解

一、数据结构与初始化
  • visunordered_set<string>,用于记录已经搜索过的基因序列,避免重复访问,防止无限循环。

  • hashunordered_set<string>,将基因库 bank 转换为哈希集合,便于 O(1) 判断一个序列是否在库中。

  • change :字符串 "ACGT",存储所有可能出现的字符,用于枚举突变时替换原字符。

  • 边界判断 :如果 startGene 等于 endGene,直接返回 0(无需变化)。如果 endGene 不在基因库中,则不可能通过合法突变得到,返回 -1。

二、BFS 队列初始化
  • 创建队列 queue<string> q,将起始基因 startGene 入队。

  • startGene 插入 vis 集合,标记为已访问。

  • 定义步数变量 ret = 0,用于记录当前 BFS 层数(即已经完成的突变次数)。

三、BFS 层序遍历过程

当队列不为空时,进行以下操作:

  1. 进入新的一层ret++,表示即将探索的是距离起点 ret 步的基因序列。

  2. 记录当前层节点数int sz = q.size(),确保只处理当前层的节点。

  3. 遍历当前层所有节点

    • 取出队头基因序列 t,并弹出队列。

    • 对序列的每一个字符位置(共 8 个位置),尝试所有可能的突变:

      • 复制当前序列到 tmp(因为每个位置需要独立尝试不同的字符)。

      • 对于 change 中的每一个字符 c,将 tmp 的该位置替换为 c,得到新序列 tmp

      • 检查新序列是否在基因库中(hash.count(tmp))且未被访问过(!vis.count(tmp))。

        • 如果新序列就是目标 endGene,则直接返回 ret(因为 BFS 第一次遇到目标时就是最短步数)。

        • 否则,将新序列入队,并加入 vis 集合标记已访问。

  4. 继续下一层 :当当前层的所有节点处理完毕后,回到循环开始,ret++ 进入下一层。

四、BFS 结束与结果返回
  • 如果 BFS 过程中从未遇到目标序列,则当队列为空时循环结束,返回 -1,表示无法通过合法突变得到 endGene
五、关键细节
  • BFS 的层序性质 :BFS 按层扩散,第一次访问到目标序列时所经过的层数就是最少变化次数。因此,在扩展时一旦遇到 endGene 立即返回当前步数。

  • 避免重复访问 :通过 vis 集合记录已访问的序列,防止同一序列多次入队,保证 BFS 的收敛性。

  • 枚举所有突变:每个位置的字符有 4 种可能,但原字符本身不需要再尝试(但是为了保证方便,且vis会把重复的扣住)

  • 复制字符串string tmp = t; 必须在每次位置变化前复制,因为后续会对 tmp 进行修改,不能影响原始序列 t

五、流程图