在计算机领域的面试中,图论相关的问题是常见的考察点,像岛屿数量、课程表问题以及最短路径问题,都涉及图论的知识。下面我们就来详细探讨这些问题的解法。

一、岛屿数量问题

应用场景

岛屿数量问题在很多实际场景中都有应用。比如在地理信息系统(GIS)中,我们可能需要统计一片海域中岛屿的数量;在图像处理里,也可以把图像中的连通区域看作“岛屿”来进行分析。

问题描述

给定一个由 '1'(陆地)和 '0'(水)组成的二维网格,计算网格中岛屿的数量。一个岛屿被水包围,并且它是通过水平或垂直方向上相邻的陆地连接而成的。你可以假设网格的四个边均被水包围。

解法思路

我们可以使用深度优先搜索(DFS)或者广度优先搜索(BFS)来解决这个问题。这里以 DFS 为例。

示例代码(Java 技术栈)

public class NumberOfIslands {
    // 定义四个方向:上、下、左、右
    private static final int[][] DIRECTIONS = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

    public int numIslands(char[][] grid) {
        if (grid == null || grid.length == 0) {
            return 0;
        }
        int rows = grid.length;
        int cols = grid[0].length;
        int count = 0;
        for (int i = 0; i < rows; i++) {
            for (int j = 0; j < cols; j++) {
                if (grid[i][j] == '1') {
                    // 遇到陆地,进行 DFS 标记
                    dfs(grid, i, j);
                    count++;
                }
            }
        }
        return count;
    }

    private void dfs(char[][] grid, int i, int j) {
        int rows = grid.length;
        int cols = grid[0].length;
        // 检查边界条件和是否为陆地
        if (i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] != '1') {
            return;
        }
        // 标记为已访问
        grid[i][j] = '0';
        for (int[] dir : DIRECTIONS) {
            int newRow = i + dir[0];
            int newCol = j + dir[1];
            // 递归调用 DFS
            dfs(grid, newRow, newCol);
        }
    }

    public static void main(String[] args) {
        char[][] grid = {
            {'1', '1', '0', '0', '0'},
            {'1', '1', '0', '0', '0'},
            {'0', '0', '1', '0', '0'},
            {'0', '0', '0', '1', '1'}
        };
        NumberOfIslands solution = new NumberOfIslands();
        int islands = solution.numIslands(grid);
        System.out.println("岛屿数量: " + islands);
    }
}

代码解释

  1. numIslands 方法:遍历整个二维网格,当遇到陆地('1')时,调用 dfs 方法进行深度优先搜索,并将岛屿数量加 1。
  2. dfs 方法:将当前陆地标记为已访问('0'),然后递归地对其上下左右相邻的陆地进行搜索。

技术优缺点

  • 优点:DFS 实现简单,代码量少,空间复杂度相对较低。
  • 缺点:在处理大规模数据时,可能会导致栈溢出,因为递归调用会使用系统栈。

注意事项

  • 要注意边界条件的判断,避免越界访问。
  • 标记已访问的节点,防止重复访问。

二、课程表问题

应用场景

课程表问题在学校的课程安排、项目任务调度等场景中非常有用。比如学校要安排一系列课程,有些课程有先修要求,我们需要判断是否能合理安排课程顺序。

问题描述

你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。判断是否可能完成所有课程的学习。

解法思路

这是一个典型的拓扑排序问题。我们可以使用 Kahn 算法或者深度优先搜索来解决。这里以 Kahn 算法为例。

示例代码(Java 技术栈)

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class CourseSchedule {
    public boolean canFinish(int numCourses, int[][] prerequisites) {
        // 存储每个节点的入度
        int[] inDegree = new int[numCourses];
        // 存储图的邻接表
        List<List<Integer>> adjList = new ArrayList<>();
        for (int i = 0; i < numCourses; i++) {
            adjList.add(new ArrayList<>());
        }
        // 构建图和入度数组
        for (int[] prerequisite : prerequisites) {
            int ai = prerequisite[0];
            int bi = prerequisite[1];
            adjList.get(bi).add(ai);
            inDegree[ai]++;
        }
        // 存储入度为 0 的节点
        Queue<Integer> queue = new LinkedList<>();
        for (int i = 0; i < numCourses; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }
        int count = 0;
        while (!queue.isEmpty()) {
            int node = queue.poll();
            count++;
            // 遍历该节点的所有邻接节点
            for (int neighbor : adjList.get(node)) {
                inDegree[neighbor]--;
                if (inDegree[neighbor] == 0) {
                    queue.offer(neighbor);
                }
            }
        }
        return count == numCourses;
    }

    public static void main(String[] args) {
        int numCourses = 2;
        int[][] prerequisites = {{1, 0}};
        CourseSchedule solution = new CourseSchedule();
        boolean canFinish = solution.canFinish(numCourses, prerequisites);
        System.out.println("是否能完成所有课程: " + canFinish);
    }
}

代码解释

  1. 首先构建图的邻接表和入度数组。
  2. 将入度为 0 的节点加入队列。
  3. 不断从队列中取出节点,减少其邻接节点的入度,如果邻接节点的入度变为 0,则加入队列。
  4. 最后判断完成的课程数量是否等于总课程数。

技术优缺点

  • 优点:Kahn 算法时间复杂度为 O(V + E),其中 V 是节点数,E 是边数,效率较高。
  • 缺点:需要额外的空间来存储入度数组和邻接表。

注意事项

  • 要正确构建邻接表和入度数组。
  • 处理入度为 0 的节点时要注意顺序。

三、最短路径问题

应用场景

最短路径问题在地图导航、物流配送等领域有广泛应用。比如在地图导航中,我们需要找到从起点到终点的最短路径。

问题描述

给定一个带权有向图,找到从起点到终点的最短路径。

解法思路

常见的最短路径算法有 Dijkstra 算法和 Bellman - Ford 算法。这里以 Dijkstra 算法为例。

示例代码(Java 技术栈)

import java.util.*;

public class Dijkstra {
    private static final int INF = Integer.MAX_VALUE;

    public int[] dijkstra(int[][] graph, int start) {
        int n = graph.length;
        // 存储最短距离
        int[] dist = new int[n];
        // 标记节点是否已访问
        boolean[] visited = new boolean[n];
        Arrays.fill(dist, INF);
        dist[start] = 0;
        PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[1] - b[1]);
        pq.offer(new int[]{start, 0});
        while (!pq.isEmpty()) {
            int[] curr = pq.poll();
            int u = curr[0];
            if (visited[u]) {
                continue;
            }
            visited[u] = true;
            for (int v = 0; v < n; v++) {
                if (!visited[v] && graph[u][v] != 0 && dist[u] + graph[u][v] < dist[v]) {
                    dist[v] = dist[u] + graph[u][v];
                    pq.offer(new int[]{v, dist[v]});
                }
            }
        }
        return dist;
    }

    public static void main(String[] args) {
        int[][] graph = {
            {0, 4, 0, 0, 0, 0, 0, 8, 0},
            {4, 0, 8, 0, 0, 0, 0, 11, 0},
            {0, 8, 0, 7, 0, 4, 0, 0, 2},
            {0, 0, 7, 0, 9, 14, 0, 0, 0},
            {0, 0, 0, 9, 0, 10, 0, 0, 0},
            {0, 0, 4, 14, 10, 0, 2, 0, 0},
            {0, 0, 0, 0, 0, 2, 0, 1, 6},
            {8, 11, 0, 0, 0, 0, 1, 0, 7},
            {0, 0, 2, 0, 0, 0, 6, 7, 0}
        };
        Dijkstra solution = new Dijkstra();
        int[] dist = solution.dijkstra(graph, 0);
        for (int i = 0; i < dist.length; i++) {
            System.out.println("从节点 0 到节点 " + i + " 的最短距离: " + dist[i]);
        }
    }
}

代码解释

  1. 初始化距离数组 dist 和访问标记数组 visited
  2. 使用优先队列存储待处理的节点,按照距离从小到大排序。
  3. 不断从优先队列中取出距离最小的节点,更新其邻接节点的距离。

技术优缺点

  • 优点:Dijkstra 算法效率较高,时间复杂度为 O((V + E) log V)。
  • 缺点:不能处理负权边。

注意事项

  • 要正确初始化距离数组和优先队列。
  • 处理负权边时不能使用 Dijkstra 算法,需要使用 Bellman - Ford 算法。

四、文章总结

本文详细介绍了图论中岛屿数量、课程表问题和最短路径问题的解法。岛屿数量问题可以使用 DFS 或 BFS 来解决,课程表问题可以使用拓扑排序(如 Kahn 算法),最短路径问题可以使用 Dijkstra 算法(适用于无负权边的图)。这些问题在实际应用中都有广泛的场景,掌握它们的解法对于解决相关问题非常有帮助。在实际应用中,我们需要根据具体情况选择合适的算法,并注意算法的优缺点和使用条件。