記事ディレクトリ
序文
幅優先検索(略してBFS ) は、グラフ ストレージ構造を走査するためのアルゴリズムであり、無向グラフと有向グラフの両方に適用できます。
BFS アルゴリズムの基本的な考え方は、開始頂点から開始し、隣接する未訪問の頂点を順番に訪問してキューに追加し、キューから頂点を新しい開始頂点として取得し、上記のプロセスを繰り返すことです。キューが空であるか、ターゲットの頂点が見つかります。
BFS アルゴリズムの特徴は次のとおりです。
- 頂点から開始して、その隣接する頂点を階層的に訪問し、すべての頂点を通過するまで次のレイヤーの頂点を訪問します。
- キューを使用してアクセスする頂点を保存し、先入れ先出しの順序を確保します。
- 最短経路を見つけることができ、経路探索などの問題に適しています。
bfs アルゴリズムの利点は次のとおりです。
- 問題の解決策が見つかるか、解決策が複数ある場合は最適な解決策が見つかることが常に保証されます。
- 高速に実行され、バックトラッキング操作は必要ありません。
BFS アルゴリズムの欠点は次のとおりです。
- 各レイヤーの頂点を保存する必要があるため、多くのスペースが必要となり、メモリ制限を超える可能性があります。
- 深いグラフの場合、時間がかかる場合があります。
BFS アルゴリズムには、次のような多くのアプリケーション シナリオがあります。
- 迷路の探索、地図ナビゲーションなどの最短経路を見つけます。
- 2 つの頂点が接続されているか、または同じ接続コンポーネントに属しているかを判断します。
- グラフ内のサイクルを見つけるか、グラフが 2 部構成であるかどうかを判断します。
- 幅優先クローリング、ロボット探索などの人工知能の検索戦略。
- 最小スパニングツリー。
- ゴミ収集。
- ネットワークの流れ。
1. 実現
1.1 主要な手順と複雑さ
BFS アルゴリズムの中心となるステップは次のとおりです。
- 開始ノードから順番にキュー(キュー)に追加していきます。
- キューが空になるか、ターゲット ノードが見つかるまで、次の操作を繰り返します。
- キューの最初のノードをポップし、それにアクセスして、訪問済みとしてマークします。
- このノードのすべての未訪問の隣接ノードをキューの最後に追加します。
- 訪問したノードまたは見つかったターゲット ノードを返します。
BFS の時間計算量は O(V+E) です。ここで、V は頂点の数、E はエッジの数です。各頂点は最大 1 回訪問され、各エッジも最大 1 回訪問されるため、合計の時間計算量は O(V+E) になります。
空間の複雑さは、アクセス状態と訪問する頂点を格納するデータ構造に依存します。グラフの隣接リスト表現の場合、空間複雑度は O(V+E) です。ここで、V は頂点の数、E はエッジの数です。訪問した頂点を記録するにはコレクションを使用し、訪問する頂点を保存するにはキューを使用する必要があります。最悪の場合、すべての頂点が訪問され、すべての頂点がキューに入れられるため、空間の複雑さは O(V+E) になります。
1.2 疑似コードと Java の例
疑似コードは次のとおりです。
# 伪码
BFS(start, target):
# 创建一个队列
queue = new Queue()
# 创建一个集合,用于记录已访问的顶点
visited = new Set()
# 将起始顶点加入队列和集合
queue.enqueue(start)
visited.add(start)
# 循环直到队列为空或找到目标顶点
while queue is not empty:
# 弹出队列的第一个顶点
node = queue.dequeue()
# 访问该顶点
visit(node)
# 如果该顶点是目标顶点,返回
if node == target:
return node
# 遍历该顶点的所有未访问的邻居顶点
for neighbor in node.neighbors:
# 如果邻居顶点没有被访问过,将其加入队列和集合
if neighbor not in visited:
queue.enqueue(neighbor)
visited.add(neighbor)
# 如果没有找到目标顶点,返回空
return null
Java のサンプルコードは次のとおりです。
// Java
import java.util.*;
public class BFS {
// 定义图的顶点类
static class Node {
int val; // 顶点的值
List<Node> neighbors; // 顶点的邻居列表
public Node(int val) {
this.val = val;
this.neighbors = new ArrayList<>();
}
}
// BFS 算法
public static Node bfs(Node start, Node target) {
// 创建一个队列
Queue<Node> queue = new LinkedList<>();
// 创建一个集合,用于记录已访问的顶点
Set<Node> visited = new HashSet<>();
// 将起始顶点加入队列和集合
queue.offer(start);
visited.add(start);
// 循环直到队列为空或找到目标顶点
while (!queue.isEmpty()) {
// 弹出队列的第一个顶点
Node node = queue.poll();
// 访问该顶点
visit(node);
// 如果该顶点是目标顶点,返回
if (node == target) {
return node;
}
// 遍历该顶点的所有未访问的邻居顶点
for (Node neighbor : node.neighbors) {
// 如果邻居顶点没有被访问过,将其加入队列和集合
if (!visited.contains(neighbor)) {
queue.offer(neighbor);
visited.add(neighbor);
}
}
}
// 如果没有找到目标顶点,返回空
return null;
}
// 访问顶点的方法,打印顶点的值
public static void visit(Node node) {
System.out.println(node.val);
}
// 测试方法
public static void main(String[] args) {
// 创建一个图
Node n1 = new Node(1);
Node n2 = new Node(2);
Node n3 = new Node(3);
Node n4 = new Node(4);
Node n5 = new Node(5);
Node n6 = new Node(6);
n1.neighbors.add(n2);
n1.neighbors.add(n3);
n2.neighbors.add(n4);
n3.neighbors.add(n4);
n3.neighbors.add(n5);
n4.neighbors.add(n6);
n5.neighbors.add(n6);
// 调用 BFS 算法,从顶点 1 开始,寻找顶点 6
Node result = bfs(n1, n6);
// 打印结果
if (result != null) {
System.out.println("找到了目标顶点:" + result.val);
} else {
System.out.println("没有找到目标顶点");
}
}
}
1.3 アニメーションの例
2. 申請
2.1 最短経路を見つける
BFS アルゴリズムを使用すると、グラフ内の 2 つの頂点間の最短パス、つまり最小数のエッジを通過するパスを見つけることができます。これは、BFS アルゴリズムがレベルに従ってグラフを走査し、各レベルの頂点が開始頂点から同じ距離にあるため、ターゲットの頂点が見つかるとそれが最短パスになるためです。
最短パスを見つけるには、BFS アルゴリズムに基づいていくつかの変更を加える必要があります。
- 訪問した頂点を記録することに加えて、各頂点の前の頂点、つまりどの頂点から頂点に到達するかを記録する必要もあります。
- ターゲットの頂点が見つかったら、ターゲットの頂点から開始し、前の頂点のリンク リストに従い、逆の順序で最短パスを出力する必要があります。
疑似コードは次のとおりです。
# 伪码
BFS(start, target):
# 创建一个队列
queue = new Queue()
# 创建一个集合,用于记录已访问的顶点
visited = new Set()
# 创建一个字典,用于记录每个顶点的前驱顶点
prev = new Map()
# 将起始顶点加入队列和集合
queue.enqueue(start)
visited.add(start)
# 循环直到队列为空或找到目标顶点
while queue is not empty:
# 弹出队列的第一个顶点
node = queue.dequeue()
# 访问该顶点
visit(node)
# 如果该顶点是目标顶点,返回
if node == target:
# 创建一个列表,用于存储最短路径
path = new List()
# 从目标顶点开始,沿着前驱顶点的链表,逆序输出最短路径
while node != null:
# 将当前顶点加入路径的开头
path.insert(0, node)
# 更新当前顶点为其前驱顶点
node = prev[node]
# 返回最短路径
return path
# 遍历该顶点的所有未访问的邻居顶点
for neighbor in node.neighbors:
# 如果邻居顶点没有被访问过,将其加入队列和集合,并记录其前驱顶点
if neighbor not in visited:
queue.enqueue(neighbor)
visited.add(neighbor)
prev[neighbor] = node
# 如果没有找到目标顶点,返回空
return null
2.2 トポロジカルソート
BFS アルゴリズムは、有向非巡回グラフ (DAG) をトポロジー的にソートするためにも使用できます。つまり、頂点の依存関係に従って頂点を線形シーケンスに配置し、任意の有向エッジ (u, v) に対して、u v の前にいます。
トポロジカルソートを実行するには、BFS アルゴリズムに基づいていくつかの変更を加える必要があります。
- 各頂点の入次数、つまり、この頂点を指すエッジの数を記録する必要があります。
- 入次数が 0 の頂点から開始してキューに追加し、次に頂点をキューに 1 つずつポップし、それをトポロジ シーケンスに追加し、その近傍すべての入次数を次のように減らす必要があります。 1 つは、隣接する頂点の次数が 0 になる場合、それもキューに入れられます。
- 最終的なトポロジ シーケンスの頂点の数がグラフの頂点の数と等しい場合、トポロジ ソートは成功します。そうでない場合、グラフ内にサイクルが存在し、トポロジ ソートは実行できません。
疑似コードは次のとおりです。
# 伪码
BFS(graph):
# 创建一个队列
queue = new Queue()
# 创建一个列表,用于存储拓扑序列
topo = new List()
# 创建一个字典,用于记录每个顶点的入度
indegree = new Map()
# 遍历图中的每个顶点,初始化其入度
for node in graph.nodes:
indegree[node] = 0
# 遍历图中的每条边,更新每个顶点的入度
for edge in graph.edges:
indegree[edge.to] += 1
# 遍历图中的每个顶点,将入度为 0 的顶点加入队列
for node in graph.nodes:
if indegree[node] == 0:
queue.enqueue(node)
# 循环直到队列为空
while queue is not empty:
# 弹出队列的第一个顶点
node = queue.dequeue()
# 将该顶点加入拓扑序列
topo.append(node)
# 遍历该顶点的所有邻居顶点,将其入度减一,如果入度为 0,将其加入队列
for neighbor in node.neighbors:
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.enqueue(neighbor)
# 如果拓扑序列中的顶点数等于图中的顶点数,返回拓扑序列,否则返回空
if len(topo) == len(graph.nodes):
return topo
else:
return null
2.3 最小スパニングツリー
BFS アルゴリズムは、無向接続グラフ、つまり、エッジの重みの合計が最小化されるようにグラフ内のすべての頂点を含む非巡回サブグラフの最小スパニング ツリー (MST) を解くために使用することもできます。
最小スパニング ツリーを解決するには、BFS アルゴリズムに基づいていくつかの変更を加える必要があります。
- 各エッジに重みを割り当てて、エッジの長さまたはコストを示す必要があります。
- 任意の頂点から開始してキューに追加し、次にキュー内の頂点を 1 つずつポップアップし、それを最小スパニング ツリーの頂点セットに追加し、すべての隣接頂点のエッジを優先順位に追加する必要があります。エッジの重みに従ってキューを作成し、大きなソートに進みます。
- プライオリティ キューが空になるか、最小スパニング ツリーの頂点セットがグラフ内の頂点セットと等しくなるまでループする必要があります。エッジの 2 つの頂点がすでに最小スパニング ツリーの頂点セットに含まれており、このエッジがリングを形成することを示します。このエッジをスキップします。そうでない場合は、このエッジを最小スパニング ツリーのエッジ セットに追加し、このエッジの別の頂点をキューに追加します。
- 最終的な最小スパニングツリーの頂点セットがグラフ内の頂点セットと等しい場合、最小スパニングツリーが正常に構築されたことを意味しますが、そうでない場合は、グラフが切断され、最小スパニングツリーを構築できません。
疑似コードは次のとおりです。
# 伪码
BFS(graph):
# 创建一个队列
queue = new Queue()
# 创建一个优先队列,用于存储边,按照权值从小到大排序
pq = new PriorityQueue()
# 创建一个集合,用于存储最小生成树的顶点
mst_nodes = new Set()
# 创建一个列表,用于存储最小生成树的边
mst_edges = new List()
# 从图中的任意一个顶点开始,将其加入队列和最小生成树的顶点集合
start = graph.nodes[0]
queue.enqueue(start)
mst_nodes.add(start)
# 循环直到队列为空或者最小生成树的顶点集合等于图中的顶点集合
while queue is not empty and len(mst_nodes) < len(graph.nodes):
# 弹出队列的第一个顶点
node = queue.dequeue()
# 遍历该顶点的所有邻居顶点,将其边加入优先队列
for neighbor in node.neighbors:
edge = get_edge(node, neighbor) # 获取两个顶点之间的边
pq.enqueue(edge)
# 循环直到优先队列为空或者找到一条合适的边
while pq is not empty:
# 从优先队列中取出最小的边
edge = pq.dequeue()
# 如果该边的两个顶点都已经在最小生成树的顶点集合中,跳过该边
if edge.from in mst_nodes and edge.to in mst_nodes:
continue
# 否则将该边加入最小生成树的边集合
mst_edges.append(edge)
# 并将该边的另一个顶点加入队列和最小生成树的顶点集合
if edge.from in mst_nodes:
queue.enqueue(edge.to)
mst_nodes.add(edge.to)
else:
queue.enqueue(edge.from)
mst_nodes.add(edge.from)
# 跳出循环
break
# 如果最后最小生成树的顶点集合等于图中的顶点集合,返回最小生成树的边集合,否则返回空
if len(mst_nodes) == len(graph.nodes):
return mst_edges
else:
return null
3. LeetCodeの実戦
3.1 二分木のレベル順序走査
バイナリ ツリーのルート ノードを指定すると、
root
そのノード値のレベル順の走査を返します。(つまり、レイヤーごとに、左から右にすべてのノードを訪問します)。
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> ans = new ArrayList<>();
if (root == null) return ans;
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); // 将根节点入队
queue.offer(null); // 将空元素入队作为分隔符
List<Integer> tmp = new ArrayList<>(); // 存储每一层的节点值
while (!queue.isEmpty()) {
TreeNode node = queue.poll(); // 出队一个节点
if (node == null) {
// 如果节点是空元素,说明一层结束了
ans.add(tmp); // 将当前层的列表添加到结果中
tmp = new ArrayList<>(); // 重置当前层的列表
if (!queue.isEmpty()) queue.offer(null); // 如果队列不为空,再次添加一个空元素作为分隔符
} else {
// 如果节点不是空元素,说明还在当前层
tmp.add(node.val); // 将节点值添加到当前层的列表中
if (node.left != null) queue.offer(node.left); // 将左子节点入队
if (node.right != null) queue.offer(node.right); // 将右子节点入队
}
}
return ans;
}
3.2 ツリーの左下隅の値を見つける
バイナリ ツリーのルート ノードが 与えられた場合
root
、バイナリ ツリーの最下端の左端のノードの値を見つけます。バイナリ ツリーに少なくとも 1 つのノードがあると仮定します。
public int findBottomLeftValue(TreeNode root) {
int leftmost = root.val; // 初始化最左边的节点值为根节点值
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root); // 将根节点入队
while (!queue.isEmpty()) {
int size = queue.size(); // 记录当前层的节点个数
for (int i = 0; i < size; i++) {
TreeNode node = queue.poll(); // 出队一个节点
if (i == 0) leftmost = node.val; // 如果是当前层的第一个节点,更新最左边的节点值
if (node.left != null) queue.offer(node.left); // 将左子节点入队
if (node.right != null) queue.offer(node.right); // 将右子节点入队
}
}
return leftmost; // 返回最左边的节点值
}
3.3 ワードソリティア
単語および辞書
wordList
からの変換シーケンスは、次の仕様に従って形成されるシーケンスbeginWord
です。endWord
beginWord -> s1 -> s2 -> ... -> sk
- 隣接する単語の各ペアは 1 文字だけ異なります。
- については
1 <= i <= k
、それぞれにsi
ありますwordList
。beginWord
にある必要はないことに注意してくださいwordList
。sk == endWord
beginWord
2 つの単語とendWord
および辞書が与えられた場合wordList
、から までの最短遷移シーケンス内の単語の数beginWord
endWord
を返します。そのような変換シーケンスが存在しない場合に戻ります0
。
単語セット内の各単語はノードであり、位置文字が異なる単語を 1 つだけ接続できます。始点と終点の間の最短パス長を見つけることは、無向グラフで最短パスを見つけることと同じです。検索が最も適しています。終点が見つかったら、それが最短パスである必要があります。広域探索はスタート地点の中心から周囲に広がる探索だからです。さらに注意すべき点が 2 つあります。
- 無向グラフでは、ノードが通過したかどうかをマークするためにマーカー ビットを使用する必要があります。そうしないと、無限ループが発生します。
- コレクションは配列型なので Set 構造に変換でき、検索が高速になります。
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
int ans = 0;
Set<String> words = new HashSet<>(wordList); // 单词列表转为Set
if (!words.contains(endWord)) {
return 0; // 如果目标单词不在单词集合中,直接返回0
}
Set<String> nextLevel = new HashSet<>();
nextLevel.add(beginWord); // 将起始单词加入到第一个队列中
ans++; // 初始化路径长度为1
while (!nextLevel.isEmpty()) {
// 使用一个循环,直到队列为空
ans++; // 路径长度加一
Set<String> tmpSet = new HashSet<>(); // 创建一个临时队列,用于存储下一层的搜索路径
for (String s: nextLevel) {
// 遍历第一个队列中的每个单词
char [] chs = s.toCharArray(); // 将单词转换为字符数组
for (int i = 0; i < chs.length; i++) {
// 遍历每个字符的位置
char old = chs[i]; // 保存原来的字符,避免重复创建字符数组
for (char ch = 'a'; ch <= 'z'; ch++) {
// 遍历每个可能的字符
chs[i] = ch; // 直接修改字符数组,避免创建新的字符串
String tmp = new String(chs); // 将字符数组转换为新的单词
if (nextLevel.contains(tmp)) {
continue; // 如果第一个队列中包含了新的单词,跳过
}
if (words.contains(tmp)) {
// 如果单词集合中包含了新的单词
tmpSet.add(tmp); // 将它加入到临时队列中
words.remove(tmp); // 从单词集合中移除已经访问过的单词,避免重复访问
}
if (tmp.equals(endWord)) {
// 如果新的单词等于目标单词,说明找到了一个最短路径,返回路径长度
return ans;
}
}
chs[i] = old; // 恢复原来的字符
}
}
nextLevel = tmpSet; // 将临时队列赋值给第一个队列,作为下一层的搜索路径
}
return 0; // 如果循环结束,说明没有找到路径,返回0
}