图的广度优先搜索 | 433. 最小基因变化 + 127. 单词接龙


前言:「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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值