데이터 구조 및 알고리즘의 폭 우선 검색 다시 방문

머리말

Breadth First Search ( 줄여서 BFS )는 그래프 저장 구조를 순회하는 알고리즘으로 무방향 그래프와 유방향 그래프 모두에 적용할 수 있습니다.

BFS 알고리즘의 기본 아이디어는 시작 정점에서 시작하여 인접한 방문하지 않은 정점을 차례로 방문하여 대기열에 추가한 다음 대기열에서 정점을 새로운 시작 정점으로 가져오고 위의 과정을 반복하는 것입니다. 대기열이 비어 있거나 대상 정점을 찾았습니다.

BFS 알고리즘의 특징은 다음과 같습니다.

  • 꼭짓점에서 시작하여 계층적으로 인접 꼭짓점을 방문하고 모든 꼭짓점을 통과할 때까지 다음 계층의 꼭짓점을 방문합니다.
  • 대기열을 사용하여 액세스할 정점을 저장하고 선입선출 순서를 보장합니다.
  • 경로 찾기와 같은 문제에 적합한 최단 경로를 찾을 수 있습니다.

bfs 알고리즘의 장점은 다음과 같습니다.

  • 항상 문제에 대한 솔루션을 찾거나 솔루션이 여러 개인 경우 최적의 솔루션을 찾는 것이 보장됩니다.
  • 빠르게 실행되며 역추적 작업이 필요하지 않습니다.

BFS 알고리즘의 단점은 다음과 같습니다.

  • 각 레이어의 꼭짓점을 저장해야 하므로 많은 공간을 차지하고 메모리 제한을 초과할 수 있습니다.
  • 깊은 그래프의 경우 시간이 오래 걸릴 수 있습니다.

BFS 알고리즘에는 다음과 같은 많은 애플리케이션 시나리오가 있습니다.

  • 미로 길찾기, 지도 내비게이션 등 최단 경로 찾기
  • 두 정점이 연결되어 있는지 또는 동일한 연결된 구성 요소에 속하는지 확인합니다.
  • 그래프에서 주기를 찾거나 그래프가 이분형인지 확인합니다.
  • 폭 우선 크롤링, 로봇 탐색 등과 같은 인공 지능의 검색 전략
  • 최소 스패닝 트리.
  • 쓰레기 수거.
  • 네트워크 흐름.

1. 실현

1.1 핵심 단계 및 복잡성

BFS 알고리즘의 핵심 단계는 다음과 같습니다.

  • 시작 노드부터 시작하여 큐(queue)에 추가합니다.
  • 대기열이 비어 있거나 대상 노드를 찾을 때까지 다음을 반복합니다.
    • 대기열의 첫 번째 노드를 팝하고 방문하여 방문한 것으로 표시합니다.
    • 이 노드의 방문하지 않은 모든 이웃 노드를 대기열 끝에 추가합니다.
  • 방문한 노드 또는 찾은 대상 노드를 반환합니다.

BFS의 시간 복잡도는 O(V+E)입니다. 여기서 V는 정점의 수이고 E는 모서리의 수입니다. 각 정점은 최대 한 번 방문하고 각 가장자리도 최대 한 번 방문하므로 총 시간 복잡도는 O(V+E)입니다.

공간 복잡도는 방문할 정점과 액세스 상태를 저장하는 데이터 구조에 따라 다릅니다. 그래프의 인접 목록 표현의 경우 공간 복잡도는 O(V+E)입니다. 여기서 V는 정점의 수이고 E는 가장자리의 수입니다. 방문한 정점을 기록하는 컬렉션과 방문할 정점을 저장하는 큐를 사용해야 합니다. 최악의 경우 모든 정점을 방문하고 모든 정점을 대기열에 추가하므로 공간 복잡도는 O(V+E)입니다.

1.2 의사 코드 및 자바 예제

의사 코드는 다음과 같습니다.

# 伪码
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
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 애니메이션 예시

BFS

2. 신청

2.1 최단 경로 찾기

BFS 알고리즘은 그래프에서 두 정점 사이의 최단 경로, 즉 가장 적은 수의 에지를 통과하는 경로를 찾는 데 사용할 수 있습니다. 이는 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)를 토폴로지적으로 정렬하는 데에도 사용할 수 있습니다. v 앞에 있습니다.

토폴로지 정렬을 수행하려면 BFS 알고리즘을 기반으로 몇 가지 수정이 필요합니다.

  • 우리는 각 정점의 진입 차수, 즉 이 정점을 가리키는 가장자리 수를 기록해야 합니다.
  • in-degree가 0인 정점에서 시작하여 큐에 추가한 다음 큐의 정점을 하나씩 팝하고 토폴로지 시퀀스에 추가하고 모든 인접 정점의 in-degree를 줄여야 합니다. 하나씩 이웃 정점의 in-degree가 0이 되면 역시 enqueue된다.
  • 최종 위상수열의 꼭짓점 개수와 그래프의 꼭짓점 개수가 같으면 위상정렬이 성공한 것이고, 그렇지 않으면 그래프에 주기가 있어 위상정렬을 할 수 없다는 뜻이다.

의사 코드는 다음과 같습니다.

# 伪码
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 알고리즘을 기반으로 몇 가지 수정이 필요합니다.

  • 가장자리의 길이 또는 비용을 나타내는 가중치를 각 가장자리에 할당해야 합니다.
  • 임의의 정점에서 시작하여 큐에 추가한 다음 큐의 정점을 하나씩 팝업하고 최소 스패닝 트리의 정점 세트에 추가하고 모든 인접 정점의 가장자리를 우선 순위에 추가해야 합니다. 에지의 무게에 따라 대기열 큰 정렬로.
  • 우선 순위 큐가 비어 있거나 최소 스패닝 트리의 정점 집합이 그래프의 정점 집합과 같을 때까지 반복해야 하며, 가장자리의 두 정점이 다음과 같은 경우 우선 순위 큐에서 가장 작은 가장자리를 가져올 때마다 이미 최소 스패닝 트리의 꼭짓점 집합에 있으므로 이 가장자리가 링을 형성할 것임을 나타냅니다. 이 가장자리를 건너뛰고 그렇지 않으면 이 가장자리를 최소 신장 트리의 가장자리 집합에 추가하고 이 가장자리의 다른 꼭짓점을 대기열에 추가합니다.
  • 최종 최소 신장 트리의 정점 집합이 그래프에 설정된 정점과 같으면 최소 신장 트리가 성공적으로 구성되었음을 의미하고, 그렇지 않으면 그래프가 연결 해제되어 최소 신장 트리를 구성할 수 없음을 의미합니다.

의사 코드는 다음과 같습니다.

# 伪码
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 이진 트리의 레벨 순회

102. 이진 트리의 레벨 순회

이진 트리의 루트 노드가 주어지면 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 트리의 왼쪽 하단에서 값 찾기

513. 트리의 왼쪽 하단에서 값 찾기

이진 트리의 루트 노드가 주어지면 이진 트리의 가장 왼쪽 맨 아래root 노드 값을 찾으십시오 .

이진 트리에 적어도 하나의 노드가 있다고 가정합니다.

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 워드 솔리테어

127. 워드 솔리테어

wordList단어 및 사전 의 변환 시퀀스는 다음 사양에 따라 형성된 시퀀스 beginWord입니다 .endWordbeginWord -> s1 -> s2 -> ... -> sk

  • 인접한 단어의 각 쌍은 한 글자만 다릅니다.
  • 에 대해 1 <= i <= k각각은 에 si있습니다 wordList. 에 있을 필요 는 beginWord없습니다 wordList.
  • sk == endWord

두 단어와 사전이 주어 beginWord지면 endWord에서 까지 가장 짧은 전환 시퀀스단어 수를wordList 반환합니다 . 그러한 변환 순서가 존재하지 않는 경우 반환됩니다 .beginWordendWord0

단어 집합의 각 단어는 노드이며 위치 문자가 다른 한 단어만 연결할 수 있으며 시작점과 끝점 사이의 최단 경로 길이를 찾는 것은 무방향 그래프에서 최단 경로를 찾는 것과 동일할 수 있습니다 . 검색이 가장 적합합니다. 끝점을 찾으면 최단 경로여야 합니다 . 넓은 탐색은 시작점의 중심에서 주변으로 퍼지는 탐색이기 때문입니다. 주목해야 할 두 가지 사항이 더 있습니다.

  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
}

참고

  1. https://leetcode.cn/tag/breadth-first-search/problemset/

Supongo que te gusta

Origin blog.csdn.net/qq_23091073/article/details/129521662
Recomendado
Clasificación