前言:「433. 最小基因变化」和「127. 单词接龙」都能够被转化为「图的广度优先搜索」来做。
1 433. 最小基因变化
解题思路:
- 把一个基因序列视作图中的一个结点;
- 若两个基因序列之间能够互相转换,则它们之间有边;
从而将问题转换为:寻找 s t a r t G e n e \mathrm{startGene} startGene 结点到 e n d G e n e \mathrm{endGene} endGene 结点的最短路径长度。
思路说明图:
根据题意可得,能够相互转换的两个基因序列之间只能有一个字符不同。由于基因序列 A A C C G G T T \mathrm{AACCGGTT} AACCGGTT 和基因序列 A A C C G G T A \mathrm{AACCGGTA} AACCGGTA 只相差一个字符,即可以相互转换,因此两者之间有边。其他结点同理。
说明:假设基因 A \mathrm{A} A 可以向基因 B \mathrm{B} B 转换,那么必然有基因 B \mathrm{B} B 可以向基因 A \mathrm{A} A 转换,因此图中的边应该是双向边,只是这里省略成了无向边。除此之外,相同颜色的箭头表示同属于一轮的遍历, 1 \mathrm{1} 1 表示两个基因序列之间只相差一个字符。
1.1 代码细节
Step1:基础的判断
if (startGene == endGene)
return 0;
unordered_set<string> genes;
for (auto & gene : bank)
genes.insert(gene);
if (!genes.count(endGene))
return -1;
- 如果 s t a r t G e n e = e n d G e n e \mathrm{startGene=endGene} startGene=endGene,那么不需要转换;
- 如果 b a n k \mathrm{bank} bank 中没有 e n d G e n e \mathrm{endGene} endGene,那么 s t a r t G e n e \mathrm{startGene} startGene 无法转换为 e n d G e n e \mathrm{endGene} endGene。
Step2:构造图
由于题目没有给出 b a n k \mathrm{bank} bank 中各个基因序列之间的转换关系,因此需要我们自己去构建。代码如下:
int m = startGene.size();
int n = bank.size();
vector<vector<int>> adj(n);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
int differ = 0;
for (int k = 0; k < m; ++k) {
if (bank[i][k] != bank[j][k])
++differ;
}
if (differ == 1) {
adj[i].push_back(j);
adj[j].push_back(i);
}
}
}
其中 a d j \mathrm{adj} adj 数组用于存储基因序列之间的转换关系, a d j [ i ] \mathrm{adj[i]} adj[i] 表示基因序列 i \mathrm{i} i 能够转换得到的所有基因序列。
说明:把 b a n k \mathrm{bank} bank 中的基因序列视作一个个的结点,该步骤实际上就是为它们寻找自己的相邻结点。
Step3:初始化图的遍历
在二叉树的广度优先搜索中,我们每次都会把当前结点的子结点压入队列中,以便在下一轮遍历中弹出并访问,这对于图的广度优先搜索也不例外。在本题中,由于不知道 s t a r t G e n e \mathrm{startGene} startGene 的子结点都有哪些,因此在进行图的广度优先搜索之前,我们需要先找出 s t a r t G e n e \mathrm{startGene} startGene 的子结点。代码如下:
queue<int> q;
vector<int> visited(n);
for (int i = 0; i < n; ++i) {
int differ = 0;
for (int k = 0; k < m; ++k) {
if (startGene[k] != bank[i][k])
++differ;
}
if (differ == 1) {
q.emplace(i);
visited[i] = 1;
}
}
由于 b a n k \mathrm{bank} bank 包含了所有有效的转换结果,因此我们在 b a n k \mathrm{bank} bank 中查找 s t a r t G e n e \mathrm{startGene} startGene 可能的子结点即可。
说明:由于图的广度优先搜索与二叉树的广度优先搜索不同,它可能遍历到先前已经被遍历过的结点,因此设置 v i s i t e d \mathrm{visited} visited 数组来记录已经被遍历过的结点。
Step4:图的遍历
图的广度优先搜索如下:
int step = 1;
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; ++i) {
int p = q.front();
q.pop();
if (bank[p] == endGene)
return step;
// 遍历当前基因的所有邻接基因
for (auto & next : adj[p]) {
if (visited[next])
continue;
// 若未被访问过,则插入到队列中
q.emplace(next);
visited[next] = 1;
}
}
++step;
}
1.2 完整代码
int minMutation(string startGene, string endGene, vector<string>& bank) {
// 基础的判断
if (startGene == endGene)
return 0;
unordered_set<string> genes;
for (auto & gene : bank)
genes.insert(gene);
if (!genes.count(endGene))
return -1;
// 计算每个基因的邻接基因
int m = startGene.size();
int n = bank.size();
vector<vector<int>> adj(n);
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
int differ = 0;
for (int k = 0; k < m; ++k) {
if (bank[i][k] != bank[j][k])
++differ;
}
if (differ == 1) {
adj[i].push_back(j);
adj[j].push_back(i);
}
}
}
// 计算startGene的邻接基因
queue<int> q;
vector<int> visited(n);
for (int i = 0; i < n; ++i) {
int differ = 0;
for (int k = 0; k < m; ++k) {
if (startGene[k] != bank[i][k])
++differ;
}
if (differ == 1) {
q.emplace(i);
visited[i] = 1;
}
}
int step = 1;
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; ++i) {
int p = q.front();
q.pop();
if (bank[p] == endGene)
return step;
// 遍历当前基因的所有邻接基因
for (auto & next : adj[p]) {
if (visited[next])
continue;
// 若未被访问过,则插入到队列中
q.emplace(next);
visited[next] = 1;
}
}
++step;
}
return -1;
}
2 127. 单词接龙
与「433. 最小基因变化」类似,本题认为能够相互转换的单词只能相差一个字母,因此照搬「433. 最小基因变化」的解法即可。思路说明图如下:
说明:由于在图的广度优先搜索中设置了 v i s i t e d \mathrm{visited} visited 数组来记录已经被遍历过的结点,因此可以看到上图中没有任何结点被遍历过两次,即同时被两个箭头所指向。
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
// 基础的判断
if (beginWord == endWord)
return 0;
unordered_set<string> words;
for (auto & word : wordList)
words.insert(word);
if (!words.count(endWord))
return 0;
// 计算每个单词的邻接单词
int m = beginWord.size();
int n = wordList.size();
vector<vector<int>> adj(n);
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
int differ = 0;
for (int k = 0; k < m; ++k) {
if (wordList[i][k] != wordList[j][k])
++differ;
}
if (differ == 1) {
adj[i].push_back(j);
adj[j].push_back(i);
}
}
}
// 计算beginWord的邻接单词
queue<int> q;
vector<int> visited(n);
for (int i = 0; i < n; ++i) {
int differ = 0;
for (int k = 0; k < m; ++k) {
if (beginWord[k] != wordList[i][k])
++differ;
}
if (differ == 1) {
q.emplace(i);
visited[i] = 1;
}
}
int step = 2;
while (!q.empty()) {
int size = q.size();
for (int i = 0; i < size; ++i) {
int p = q.front();
q.pop();
if (wordList[p] == endWord)
return step;
for (auto & next : adj[p]) {
if (visited[next])
continue;
q.emplace(next);
visited[next] = 1;
}
}
++step;
}
return 0;
}