LeetCode 433:Minimum Genetic Mutation 题目理解与 BFS 思路详解

这道题要求:从一个起始基因串 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

  1. 写一个 can_mutations(from, to) 判断两个串是否只差一位。
  2. 遍历 bank 里的所有字符串,两两检查 can_mutations,如果能一位变异就连一条边。
  3. 再把能从 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 = 0queue.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
相关推荐
张3蜂1 小时前
SQL Server 数据库 的通信加密配置SSL安全连接
数据库·安全·ssl
卿雪1 小时前
Redis 数据过期删除和内存淘汰策略
数据库·redis·缓存
by__csdn2 小时前
第一章 (ASP.NET Core入门)第二节( 认识ASP.NET Core)
数据库·后端·c#·asp.net·.net·.netcore·f#
小妖6662 小时前
力扣(LeetCode)- 542. 01 矩阵
算法·leetcode·矩阵
爬山算法2 小时前
Redis(170)如何使用Redis实现分布式限流?
数据库·redis·分布式
CoderYanger2 小时前
第 479 场周赛Q2——3770. 可表示为连续质数和的最大质数
java·数据结构·算法·leetcode·职场和发展
JavaBoy_XJ2 小时前
Redis在 Spring Boot 项目中的完整配置指南
数据库·spring boot·redis·redis配置
KG_LLM图谱增强大模型2 小时前
SciDaSynth:基于大语言模型的科学文献交互式结构化数据提取系统
数据库·人工智能·大模型·知识图谱
凌盛羽2 小时前
用Python非常流行的openpyxl库对Excel(.xlsx格式)文件进行创建、读取、写入、显示等操作
数据库·python·链表·excel