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
相关推荐
素玥20 小时前
实训5 python连接mysql数据库
数据库·python·mysql
jnrjian20 小时前
text index 查看index column index定义 index 刷新频率 index视图
数据库·oracle
小白菜又菜20 小时前
Leetcode 2075. Decode the Slanted Ciphertext
算法·leetcode·职场和发展
瀚高PG实验室21 小时前
审计策略修改
网络·数据库·瀚高数据库
言慢行善21 小时前
sqlserver模糊查询问题
java·数据库·sqlserver
韶博雅21 小时前
emcc24ai
开发语言·数据库·python
有想法的py工程师21 小时前
PostgreSQL 分区表排序优化:Append Sort 优化为 Merge Append
大数据·数据库·postgresql
迷枫7121 天前
达梦数据库的体系架构
数据库·oracle·架构
夜晚打字声1 天前
9(九)Jmeter如何连接数据库
数据库·jmeter·oracle
Chasing__Dreams1 天前
Mysql--基础知识点--95--为什么避免使用长事务
数据库·mysql