文章目录
题目
标题和出处
标题:最小基因变化
出处:433. 最小基因变化
难度
5 级
题目描述
要求
基因序列可以表示为由 8 \texttt{8} 8 个字符组成的字符串,其中每个字符都是 'A' \texttt{`A'} 'A'、 'C' \texttt{`C'} 'C'、 'G' \texttt{`G'} 'G' 和 'T' \texttt{`T'} 'T' 之一。
假设我们需要调查从基因序列 start \texttt{start} start 变为 end \texttt{end} end 所发生的基因变化。一次基因变化定义为这个基因序列中的一个字符发生了变化。
- 例如, "AACCGGTT" → "AACCGGTA" \texttt{"AACCGGTT"} \rightarrow \texttt{"AACCGGTA"} "AACCGGTT"→"AACCGGTA" 是一次基因变化。
另有一个基因库 bank \texttt{bank} bank 记录了所有有效的基因变化,只有基因库中的基因才是有效的基因序列。
给定两个基因序列 start \texttt{start} start 和 end \texttt{end} end,以及一个基因库 bank \texttt{bank} bank,返回将 start \texttt{start} start 变化为 end \texttt{end} end 所需的最少变化次数。如果无法完成此基因变化,返回 -1 \texttt{-1} -1。
注意起始基因序列默认是有效的,因此它不一定会出现在基因库中。
示例
示例 1:
输入: start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"] \texttt{start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"]} start = "AACCGGTT", end = "AACCGGTA", bank = ["AACCGGTA"]
输出: 1 \texttt{1} 1
示例 2:
输入: start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"] \texttt{start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]} start = "AACCGGTT", end = "AAACGGTA", bank = ["AACCGGTA","AACCGCTA","AAACGGTA"]
输出: 2 \texttt{2} 2
示例 3:
输入: start = "AAAAACCC", end = "AACCCCCC", bank = ["AAAACCCC","AAACCCCC","AACCCCCC"] \texttt{start = "AAAAACCC", end = "AACCCCCC", bank = ["AAAACCCC","AAACCCCC","AACCCCCC"]} start = "AAAAACCC", end = "AACCCCCC", bank = ["AAAACCCC","AAACCCCC","AACCCCCC"]
输出: 3 \texttt{3} 3
数据范围
- start.length = 8 \texttt{start.length} = \texttt{8} start.length=8
- end.length = 8 \texttt{end.length} = \texttt{8} end.length=8
- 0 ≤ bank.length ≤ 10 \texttt{0} \le \texttt{bank.length} \le \texttt{10} 0≤bank.length≤10
- bank[i].length = 8 \texttt{bank[i].length} = \texttt{8} bank[i].length=8
- start \texttt{start} start、 end \texttt{end} end 和 bank[i] \texttt{bank[i]} bank[i] 仅由字符 ['A', 'C', 'G', 'T'] \texttt{[`A', `C', `G', `T']} ['A', 'C', 'G', 'T'] 组成
解法一
思路和算法
这道题要求计算将 start \textit{start} start 变化为 end \textit{end} end 所需的最少变化次数,可以使用广度优先搜索实现,广度优先搜索可以确保得到最短路径。
为了快速判断一个基因序列是否在基因库中,需要使用哈希集合存储基因库中的每个基因序列。只有当 end \textit{end} end 在基因库中时,才可能将 start \textit{start} start 变化为 end \textit{end} end,因此首先判断 end \textit{end} end 是否在基因库中,如果 end \textit{end} end 不在基因库中则不能将 start \textit{start} start 变化为 end \textit{end} end,返回 − 1 -1 −1。以下只考虑 end \textit{end} end 在基因库中的情况。
规定 start \textit{start} start 在第 0 0 0 层,从 start \textit{start} start 开始遍历,每次遍历同一层的全部基因序列,并得到下一层的全部基因序列。遍历过程中如果遇到 end \textit{end} end,则 end \textit{end} end 所在层为最少变化次数。如果遍历结束仍未发现 end \textit{end} end,则不能将 start \textit{start} start 变化为 end \textit{end} end。
广度优先搜索的过程中,需要使用哈希集合存储已访问的基因序列。初始时将 start \textit{start} start 添加到已访问的哈希集合,将 start \textit{start} start 入队列,将层数初始化为 − 1 -1 −1。
每一轮遍历时,首先将层数加 1 1 1,并得到队列内的基因序列个数,此时队列内的基因序列为同一层的全部基因序列,然后访问这些基因序列,获得下一层的全部基因序列并入队列。从当前基因序列获得下一层的基因序列的做法是,分别将当前基因序列的每个字符替换成其他可能的字符,得到与当前基因序列恰好有一个字符不同的新基因序列,如果新基因序列是未访问的基因序列,则新基因序列是下一层的基因序列。
一轮遍历结束之后,当前层的全部基因序列都已经出队列并被访问,此时队列内的元素为下一层的全部基因序列,下一轮遍历时即可访问下一层的全部基因序列。该做法可以确保每一轮遍历的基因序列为同一层的全部基因序列。
由于遍历过程中维护层数,因此当遇到 end \textit{end} end 时,当前层即为将 start \textit{start} start 变化为 end \textit{end} end 的最少变化次数,返回当前层。
如果遍历结束之后仍未遇到 end \textit{end} end,则不能将 start \textit{start} start 变化为 end \textit{end} end,返回 − 1 -1 −1。
代码
java
class Solution {
static final int GENE_LENGTH = 8;
static char[] choices = {'A', 'C', 'G', 'T'};
public int minMutation(String start, String end, String[] bank) {
Set<String> bankSet = new HashSet<String>();
for (String gene : bank) {
bankSet.add(gene);
}
if (!bankSet.contains(end)) {
return -1;
}
Set<String> visited = new HashSet<String>();
visited.add(start);
Queue<String> queue = new ArrayDeque<String>();
queue.offer(start);
int mutations = -1;
while (!queue.isEmpty()) {
mutations++;
int size = queue.size();
for (int i = 0; i < size; i++) {
String gene = queue.poll();
if (gene.equals(end)) {
return mutations;
}
List<String> mutatedGenes = getMutatedGenes(gene);
for (String mutated : mutatedGenes) {
if (bankSet.contains(mutated) && visited.add(mutated)) {
queue.offer(mutated);
}
}
}
}
return -1;
}
public List<String> getMutatedGenes(String gene) {
List<String> mutatedGenes = new ArrayList<String>();
char[] arr = gene.toCharArray();
for (int i = 0; i < GENE_LENGTH; i++) {
char original = arr[i];
for (char choice : choices) {
if (choice == original) {
continue;
}
arr[i] = choice;
mutatedGenes.add(new String(arr));
}
arr[i] = original;
}
return mutatedGenes;
}
}
复杂度分析
-
时间复杂度: O ( ∣ Σ ∣ × m × n ) O(|\Sigma| \times m \times n) O(∣Σ∣×m×n),其中 Σ \Sigma Σ 是基因字符集, m m m 是基因序列的长度, n n n 是基因库的大小,这道题中 Σ = { 'A' , 'C' , 'G' , 'T' } \Sigma = \{\text{`A'}, \text{`C'}, \text{`G'}, \text{`T'}\} Σ={'A','C','G','T'}, ∣ Σ ∣ = 4 |\Sigma| = 4 ∣Σ∣=4, m = 8 m = 8 m=8。广度优先搜索最多需要遍历每个基因序列一次,对于每个基因序列计算其下一层的基因序列的时间是 O ( ∣ Σ ∣ × m ) O(|\Sigma| \times m) O(∣Σ∣×m),因此时间复杂度是 O ( ∣ Σ ∣ × m × n ) O(|\Sigma| \times m \times n) O(∣Σ∣×m×n)。
-
空间复杂度: O ( m × n ) O(m \times n) O(m×n),其中 m m m 是基因序列的长度, n n n 是基因库的大小,这道题中 m = 8 m = 8 m=8。哈希集合和队列需要 O ( m × n ) O(m \times n) O(m×n) 的空间。
解法二
思路和算法
也可以使用双向广度优先搜索计算最少变化次数。
首先判断 end \textit{end} end 是否在基因库中,如果 end \textit{end} end 不在基因库中则不能将 start \textit{start} start 变化为 end \textit{end} end,返回 − 1 -1 −1。以下只考虑 end \textit{end} end 在基因库中的情况。
双向广度优先搜索分别从 start \textit{start} start 和 end \textit{end} end 开始搜索,需要对两个方向的广度优先搜索分别维护一个哈希集合与一个队列。为了减少每次遍历的基因序列数,每一轮遍历时首先比较两个方向的队列大小,选择较小的队列所在的方向执行广度优先搜索,如果两个方向的队列大小相同则选择从 start \textit{start} start 开始的方向执行广度优先搜索。
遍历过程中,如果发现当前方向的当前层的一个基因序列在另一个方向的已访问的哈希集合中,则两个方向的广度优先搜索相遇,可以将 start \textit{start} start 变化为 end \textit{end} end,最少变化次数为已经遍历的轮数。
如果遍历结束之后两个方向的广度优先搜索仍未相遇,则不能将 start \textit{start} start 变化为 end \textit{end} end,返回 − 1 -1 −1。
代码
java
class Solution {
static final int GENE_LENGTH = 8;
static char[] choices = {'A', 'C', 'G', 'T'};
Set<String> bankSet = new HashSet<String>();
public int minMutation(String start, String end, String[] bank) {
for (String gene : bank) {
bankSet.add(gene);
}
if (!bankSet.contains(end)) {
return -1;
}
Set<String> visitedStart = new HashSet<String>();
visitedStart.add(start);
Set<String> visitedEnd = new HashSet<String>();
visitedEnd.add(end);
Queue<String> queueStart = new ArrayDeque<String>();
queueStart.offer(start);
Queue<String> queueEnd = new ArrayDeque<String>();
queueEnd.offer(end);
int mutations = -1;
while (!queueStart.isEmpty() && !queueEnd.isEmpty()) {
mutations++;
boolean found;
int sizeStart = queueStart.size(), sizeEnd = queueEnd.size();
if (sizeStart <= sizeEnd) {
found = find(queueStart, visitedStart, visitedEnd);
} else {
found = find(queueEnd, visitedEnd, visitedStart);
}
if (found) {
return mutations;
}
}
return -1;
}
public boolean find(Queue<String> queue, Set<String> visited1, Set<String> visited2) {
int size = queue.size();
for (int i = 0; i < size; i++) {
String gene = queue.poll();
if (visited2.contains(gene)) {
return true;
}
List<String> mutatedGenes = getMutatedGenes(gene);
for (String mutated : mutatedGenes) {
if (bankSet.contains(mutated) && visited1.add(mutated)) {
queue.offer(mutated);
}
}
}
return false;
}
public List<String> getMutatedGenes(String gene) {
List<String> mutatedGenes = new ArrayList<String>();
char[] arr = gene.toCharArray();
for (int i = 0; i < GENE_LENGTH; i++) {
char original = arr[i];
for (char choice : choices) {
if (choice == original) {
continue;
}
arr[i] = choice;
mutatedGenes.add(new String(arr));
}
arr[i] = original;
}
return mutatedGenes;
}
}
复杂度分析
-
时间复杂度: O ( ∣ Σ ∣ × m × n ) O(|\Sigma| \times m \times n) O(∣Σ∣×m×n),其中 Σ \Sigma Σ 是基因字符集, m m m 是基因序列的长度, n n n 是基因库的大小,这道题中 Σ = { 'A' , 'C' , 'G' , 'T' } \Sigma = \{\text{`A'}, \text{`C'}, \text{`G'}, \text{`T'}\} Σ={'A','C','G','T'}, ∣ Σ ∣ = 4 |\Sigma| = 4 ∣Σ∣=4, m = 8 m = 8 m=8。广度优先搜索最多需要遍历每个基因序列一次,对于每个基因序列计算其下一层的基因序列的时间是 O ( ∣ Σ ∣ × m ) O(|\Sigma| \times m) O(∣Σ∣×m),因此时间复杂度是 O ( ∣ Σ ∣ × m × n ) O(|\Sigma| \times m \times n) O(∣Σ∣×m×n)。
-
空间复杂度: O ( m × n ) O(m \times n) O(m×n),其中 m m m 是基因序列的长度, n n n 是基因库的大小,这道题中 m = 8 m = 8 m=8。哈希集合和队列需要 O ( m × n ) O(m \times n) O(m×n) 的空间。