当前位置: 首页 > news >正文

java 洛谷题单【数据结构1-4】图的基本应用

P5318 【深基18.例3】查找文献

解题思路

  1. 图的构建:使用邻接表存储每个文献的引用关系。读取输入后,对每个节点的邻接表进行排序和去重,以确保节点按升序排列。

  2. DFS遍历:使用栈来实现非递归遍历。每次从栈中弹出节点后,将其邻接节点逆序压入栈中,以确保优先处理编号较小的节点。

  3. BFS遍历:使用队列来实现广度优先搜索。按邻接表的顺序处理节点,确保先访问编号较小的节点。

  4. 结果输出:将DFS和BFS的遍历结果格式化为字符串并输出。

import java.util.*;
import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        // 使用 StreamTokenizer 读取输入
        StreamTokenizer st = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
        st.nextToken();
        int n = (int) st.nval; // 读取文章数量 n
        st.nextToken();
        int m = (int) st.nval; // 读取引用关系数量 m

        // 初始化邻接表,用于存储图的引用关系
        List<List<Integer>> adj = new ArrayList<>(n + 1);
        for (int i = 0; i <= n; i++) {
            adj.add(new ArrayList<>());
        }

        // 读取引用关系并存储到邻接表中
        for (int i = 0; i < m; i++) {
            st.nextToken();
            int X = (int) st.nval; // 文章 X
            st.nextToken();
            int Y = (int) st.nval; // 文章 Y
            adj.get(X).add(Y); // 表示 X 指向 Y
        }

        // 对每个邻接表进行排序和去重,保证编号较小的文章优先访问
        for (int i = 1; i <= n; i++) {
            List<Integer> list = adj.get(i);
            if (list.isEmpty()) continue; // 如果没有引用关系,跳过
            Collections.sort(list); // 对邻接表排序
            // 去重操作
            int j = 0;
            for (int k = 1; k < list.size(); k++) {
                if (!list.get(k).equals(list.get(j))) {
                    j++;
                    list.set(j, list.get(k));
                }
            }
            if (list.size() > j + 1) {
                list.subList(j + 1, list.size()).clear(); // 删除多余的元素
            }
        }

        // 深度优先搜索 (DFS)
        boolean[] visited = new boolean[n + 1]; // 访问标记数组
        List<Integer> dfsOrder = new ArrayList<>(); // 存储 DFS 遍历结果
        Deque<Integer> stack = new ArrayDeque<>(); // 使用栈实现 DFS
        stack.push(1); // 从编号为 1 的文章开始

        while (!stack.isEmpty()) {
            int u = stack.pop(); // 弹出栈顶元素
            if (visited[u]) continue; // 如果已经访问过,跳过
            visited[u] = true; // 标记为已访问
            dfsOrder.add(u); // 记录访问顺序
            List<Integer> neighbors = adj.get(u); // 获取当前节点的邻居
            // 倒序遍历邻居,保证编号较小的优先被访问
            for (int i = neighbors.size() - 1; i >= 0; i--) {
                int v = neighbors.get(i);
                if (!visited[v]) {
                    stack.push(v); // 将未访问的邻居压入栈
                }
            }
        }

        // 广度优先搜索 (BFS)
        visited = new boolean[n + 1]; // 重置访问标记数组
        List<Integer> bfsOrder = new ArrayList<>(); // 存储 BFS 遍历结果
        Queue<Integer> queue = new LinkedList<>(); // 使用队列实现 BFS
        queue.offer(1); // 从编号为 1 的文章开始
        visited[1] = true; // 标记起点为已访问

        while (!queue.isEmpty()) {
            int u = queue.poll(); // 取出队首元素
            bfsOrder.add(u); // 记录访问顺序
            for (int v : adj.get(u)) { // 遍历当前节点的邻居
                if (!visited[v]) {
                    visited[v] = true; // 标记为已访问
                    queue.offer(v); // 将未访问的邻居加入队列
                }
            }
        }

        // 输出结果
        StringBuilder sb = new StringBuilder();
        for (int num : dfsOrder) {
            sb.append(num).append(' '); // 拼接 DFS 遍历结果
        }
        sb.append('\n');
        for (int num : bfsOrder) {
            sb.append(num).append(' '); // 拼接 BFS 遍历结果
        }
        System.out.print(sb); // 输出最终结果
    }
}

P3916 图的遍历

解题思路

  • 图的表示

    • 使用邻接表表示有向图。由于题目要求找到从每个节点能到达的最大编号节点,因此可以构建图的反向边。
    • 在反向图中,如果存在一条边 (u, v),则在图中加入边 (v, u),这样我们可以从目标节点向源节点遍历。
  • 遍历策略

    • 为了计算每个节点的最大可达编号,我们可以采用深度优先搜索(DFS)。
    • 从每个节点开始 DFS 时,如果当前节点已经被访问过,直接返回已存储的结果;否则,记录从该节点出发的最大可达编号。
    • 为了确保每个节点的 DFS 都能得到准确的最大编号,可以从编号较大的节点开始 DFS。
  • 结果存储

    • 使用一个数组 num 来存储每个节点的最大可达编号,数组的索引对应节点编号。
    • 当遍历结束后,数组 num 中的每个元素即为所求结果。
import java.util.*;

public class Main {
    static int MAXL = 100001;           // 最大点数
    static int n, m;                    // 节点数和边数
    static int[] num = new int[MAXL];      // 记录从每个节点出发到达的最大节点编号
    static List<List<Integer>> g = new ArrayList<>();          // 邻接表

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        // 读取点数和边数
        n = input.nextInt();
        m = input.nextInt();

        // 初始化邻接表
        for (int i = 0; i <= n; i++) {
            g.add(new ArrayList<>());   // 为每个节点创建一个列表
        }

        // 反向建边
        for (int i = 0; i < m; i++) {
            // 边的起点
            int u = input.nextInt();
            // 边的终点
            int v = input.nextInt();
            // 邻接表添加反向边
            g.get(v).add(u);
        }

        // 从每个节点进行DFS
        for (int i = n; i >= 1; i--) {
            dfs(i, i);
        }

        // 输出结果
        for (int i = 1; i <= n; i++) {
            System.out.print(num[i] + " ");
        }
        System.out.println();
    }

    private static void dfs(int x, int d) {
        if (num[x] != 0) return; // 如果访问过,则返回
        num[x] = d; // 记录从x出发到达的最大节点编号
        for (int neighbor : g.get(x)) {
            dfs(neighbor, d);
        }
    }
}

P1113 杂务 

解题思路(拓扑排序)

1. 问题建模

  • 每个杂务可以看作一个节点,依赖关系可以看作有向边。这是一个有向图的问题。
  • 输入中每个任务的完成时间和依赖的任务形成了图的结构。

2. 使用拓扑排序

  • 由于任务之间存在依赖关系,我们需要确保在计算一个任务的完成时间时,其所有依赖的任务都已经完成。
  • 我们可以使用拓扑排序的方法来处理这个问题。入度(依赖的任务数量)为0的任务可以立即开始执行。
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt(); // 任务数量
        int[] index = new int[n + 1]; // 入度数组
        int[] time = new int[n + 1]; // 完成时间数组
        int[] lim = new int[n + 1]; // 每个任务的时间
        List<List<Integer>> edge = new ArrayList<>(n + 1); // 邻接表

        // 初始化邻接表
        for (int i = 0; i <= n; i++) {
            edge.add(new ArrayList<>());
        }

        // 读取任务信息
        for (int i = 1; i <= n; i++) {
            int x = input.nextInt(); // 任务编号
            lim[x] = input.nextInt(); // 任务所需时间
            while (true) {
                int y = input.nextInt(); // 读取依赖的任务
                if (y == 0) break; // 结束读取依赖
                edge.get(y).add(x); // 记录依赖关系
                index[x]++; // 更新入度
            }
        }

        // 队列用于拓扑排序
        Queue<Integer> q = new LinkedList<>();

        // 将入度为0的任务入队
        for (int i = 1; i <= n; i++) {
            if (index[i] == 0) {
                q.add(i);
                time[i] = lim[i]; // 初始化完成时间
            }
        }

        // 进行拓扑排序
        while (!q.isEmpty()) {
            int rhs = q.poll(); // 取出队首任务
            // 遍历依赖该任务的所有任务
            for (int u : edge.get(rhs)) {
                index[u]--; // 入度减1
                // 如果入度为0,则入队
                if (index[u] == 0) {
                    q.add(u);
                }
                // 更新任务的完成时间
                time[u] = Math.max(time[u], time[rhs] + lim[u]);
            }
        }

        // 统计所有任务完成时间的最大值
        int ans = 0;
        for (int i = 1; i <= n; i++) {
            ans = Math.max(ans, time[i]);
        }

        // 输出结果
        System.out.println(ans);
    }
}

 解题思路(动态规划)

我们可以使用动态规划的方法来计算每个任务的完成时间。每个任务的完成时间依赖于它所有依赖的任务的完成时间。

  • 输入处理

    • 读取任务的数量 n,并初始化完成时间数组 ans 和最大完成时间 maxAns
    • 逐个读取每个任务的编号、所需时间以及依赖的任务,直到读取到 0 为止。
  • 计算完成时间

    • 对于每个任务,首先记录其依赖任务的最大完成时间 tmp
    • 将当前任务的完成时间 ans[i] 设置为 tmp 加上当前任务所需的时间。
    • 更新 maxAns 为当前任务的完成时间和已有的最大完成时间中的较大者。
import java.util.Scanner;

public class Main {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n = input.nextInt(); // 任务数量
        int[] ans = new int[10005]; // 完成时间数组
        int maxAns = 0; // 最大完成时间

        // 读取每个任务的信息
        for (int i = 1; i <= n; i++) {
            int taskId = input.nextInt(); // 任务编号
            int l = input.nextInt(); // 任务所需时间
            int tmp = 0; // 用于记录依赖任务的最大完成时间

            // 读取依赖项
            while (true) {
                int t = input.nextInt(); // 读取依赖的任务
                if (t == 0) break; // 结束读取依赖
                tmp = Math.max(ans[t], tmp); // 更新最大依赖完成时间
            }

            ans[taskId] = tmp + l; // 当前任务的完成时间
            maxAns = Math.max(ans[taskId], maxAns); // 更新最大完成时间
        }

        // 输出结果
        System.out.println(maxAns);
    }
}

P4017 最大食物链计数

解题思路

  1. 问题分析:题目要求计算从生产者(入度为0的节点)到顶级消费者(出度为0的节点)的所有路径数目之和。这类问题可以通过拓扑排序结合动态规划(DP)来解决。

  2. 图构建:使用邻接表存储每个节点的后继节点(即该节点被哪些生物捕食),并统计每个节点的入度和出度。

  3. 拓扑排序:初始化所有生产者节点的路径数为1,然后按照拓扑顺序处理每个节点,将其路径数累加到其后继节点的路径数中。

  4. 动态规划:每个节点的路径数表示以该节点为终点的路径数目。在拓扑排序过程中,逐步更新每个节点的路径数。

  5. 结果计算:遍历所有出度为0的节点(顶级消费者),将其路径数累加得到最终结果,并取模处理。

import java.io.*;
import java.util.*;

public class Main {
    static final int MOD = 80112002; // 定义取模常量

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] parts = br.readLine().split(" ");
        int n = Integer.parseInt(parts[0]); // 节点数
        int m = Integer.parseInt(parts[1]); // 边数

        // 邻接表表示图
        List<Integer>[] adj = new ArrayList[n + 1];
        for (int i = 1; i <= n; i++) {
            adj[i] = new ArrayList<>();
        }

        int[] inDegree = new int[n + 1]; // 入度数组
        int[] outDegree = new int[n + 1]; // 出度数组

        // 读取边信息并构建图
        for (int i = 0; i < m; i++) {
            parts = br.readLine().split(" ");
            int a = Integer.parseInt(parts[0]); // 起点
            int b = Integer.parseInt(parts[1]); // 终点
            adj[a].add(b); // 添加边 a -> b
            inDegree[b]++; // 增加 b 的入度
            outDegree[a]++; // 增加 a 的出度
        }

        int[] dp = new int[n + 1]; // dp[i] 表示从某个生产者到节点 i 的路径数
        Queue<Integer> queue = new LinkedList<>(); // 队列用于拓扑排序

        // 初始化生产者(入度为 0 的节点)
        for (int i = 1; i <= n; i++) {
            if (inDegree[i] == 0) {
                dp[i] = 1; // 生产者的路径数初始化为 1
                queue.offer(i); // 将生产者加入队列
            }
        }

        // 拓扑排序处理
        while (!queue.isEmpty()) {
            int u = queue.poll(); // 取出队首节点
            for (int v : adj[u]) { // 遍历 u 的所有邻接节点
                dp[v] = (dp[v] + dp[u]) % MOD; // 更新路径数,取模防止溢出
                if (--inDegree[v] == 0) { // 如果 v 的入度变为 0,加入队列
                    queue.offer(v);
                }
            }
        }

        // 计算所有顶级消费者的路径总和
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            if (outDegree[i] == 0) { // 顶级消费者(出度为 0 的节点)
                sum = (sum + dp[i]) % MOD; // 累加路径数,取模防止溢出
            }
        }

        System.out.print(sum); // 输出结果
    }
}

P1807 最长路

解题思路

  • 问题分析:我们需要求解从顶点1到顶点n的最长路径。由于这是一个有向无环图(DAG),可以使用拓扑排序来简化求解最长路径的问题。

  • 拓扑排序与动态规划

    • 首先,构建图的邻接表并计算每个节点的入度。
    • 使用拓扑排序从起点开始遍历图,确保每个节点只会在其前驱节点都处理完后才被处理,从而避免环。
    • 利用动态规划,设dist[i]表示从起点1到节点i的最长路径。初始化dist[1] = 0,其他节点的dist初始化为负无穷。
    • 在遍历每条边u -> v时,若dist[u] + w > dist[v],则更新dist[v] = dist[u] + w
  • 结果输出

    • 最后,若dist[n]仍为负无穷,表示1无法到达n,输出-1
    • 否则,输出dist[n]
import java.util.*;

public class Main {
    static final int INF = Integer.MIN_VALUE;
    static int[] dist, inDegree;
    static List<int[]>[] graph;
    static int n, m;

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        n = input.nextInt();
        m = input.nextInt();

        dist = new int[n + 1];
        Arrays.fill(dist, INF);  // 初始化距离
        dist[1] = 0;  // 起点的距离为0

        inDegree = new int[n + 1];
        graph = new ArrayList[n + 1];
        for (int i = 1; i <= n; i++) {
            graph[i] = new ArrayList<>();
        }

        for (int i = 0; i < m; i++) {
            int u = input.nextInt();
            int v = input.nextInt();
            int w = input.nextInt();
            graph[u].add(new int[]{v, w});
            inDegree[v]++;
        }

        if (topoSortAndLongestPath()) {
            System.out.println(dist[n]);
        } else {
            System.out.println(-1);  // 无法到达n
        }
    }

    static boolean topoSortAndLongestPath() {
        Queue<Integer> queue = new LinkedList<>();

        // 初始化拓扑排序队列
        for (int i = 1; i <= n; i++) {
            if (inDegree[i] == 0) {
                queue.offer(i);
            }
        }

        int visitedCount = 0;
        while (!queue.isEmpty()) {
            int u = queue.poll();
            visitedCount++;

            for (int[] edge : graph[u]) {
                int v = edge[0], w = edge[1];
                // 更新最长路径
                if (dist[u] != INF) {
                    dist[v] = Math.max(dist[v], dist[u] + w);
                }
                // 更新入度并检查是否可以入队
                if (--inDegree[v] == 0) {
                    queue.offer(v);
                }
            }
        }

        return visitedCount == n && dist[n] != INF;
    }
}

P1127 词链

解题思路

  • 输入与初始化

    • 首先读取字符串数量 n 和每个字符串。利用 ind 数组记录每个字符作为字符串结束字符的数量,利用 rnd 数组记录每个字符作为字符串开始字符的数量。
  • 邻接表构建

    • 对字符串进行字典序排序。通过两层循环检查每个字符串对,如果一个字符串的最后一个字符与另一个字符串的第一个字符相同,则在邻接表中记录这一边。
  • 欧拉路径搜索

    • 欧拉路径要求图的入度和出度特定匹配。通常,起点的出度应大于入度一,而其他节点的入度与出度应相等。因此,使用 indrnd 数组来找到合适的起点。
    • 使用 DFS 方法搜索所有可能的路径。如果找到了包含所有字符串的路径,则输出结果并结束程序。
  • 处理边界条件

    • 如果没有找到任何有效路径,程序输出 "***" 表示无法形成有效的欧拉路径。
import java.util.*;

public class Main {
    static int n; // 字符串数量
    static String[] a = new String[1001]; // 存储输入的字符串
    static List<Integer>[] e = new ArrayList[1001]; // 邻接表,用于存储边
    static int[] ind = new int[1001]; // 入度数组,记录每个字符作为结束字符的字符串数量
    static int[] outd = new int[1001]; // 出度数组,记录每个字符作为开始字符的字符串数量
    static boolean[] used = new boolean[1001]; // 标记字符串是否已经使用

    // 深度优先搜索函数
    static void dfs(int s, String curr, int count) {
        // 当找到一个完整的路径时
        if (count == n) {
            System.out.println(curr.substring(0, curr.length() - 1)); // 去掉最后的点
            System.exit(0); // 找到答案后退出程序
        }
        // 遍历当前字符串可以连接的下一个字符串
        for (int next : e[s]) {
            if (!used[next]) { // 如果下一个字符串未被使用
                used[next] = true; // 标记为已使用
                // 递归调用,继续深度优先搜索
                dfs(next, curr + a[next] + ".", count + 1);
                used[next] = false; // 回溯,取消使用标记
            }
        }
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        n = input.nextInt(); // 读取字符串数量
        // 读取字符串并初始化数据结构
        for (int i = 1; i <= n; ++i) {
            a[i] = input.next(); // 存储字符串
            ind[a[i].charAt(0)]++; // 更新入度
            outd[a[i].charAt(a[i].length() - 1)]++; // 更新出度
            e[i] = new ArrayList<>(); // 初始化邻接表
        }

        Arrays.sort(a, 1, n + 1); // 按字典序排序字符串
        // 建立邻接关系
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j <= n; ++j) {
                // 如果当前字符串的最后一个字符与下一个字符串的第一个字符相同
                if (i != j && a[i].charAt(a[i].length() - 1) == a[j].charAt(0)) {
                    e[i].add(j); // 记录边
                }
            }
        }

        // 尝试以每个合适的字符串作为起始点
        for (int i = 1; i <= n; ++i) {
            if (ind[a[i].charAt(0)] == outd[a[i].charAt(0)] + 1) {
                used[i] = true; // 标记当前字符串为已使用
                dfs(i, a[i] + ".", 1); // 从当前字符串开始 DFS
                used[i] = false; // 回溯
            }
        }

        // 如果没有找到合适的起始点,默认从第一个字符串开始
        used[1] = true;
        dfs(1, a[1] + ".", 1); // 从第一个字符串开始 DFS
        used[1] = false; // 回溯

        // 如果没有找到路径,输出 "***"
        System.out.println("***");
    }
}

P2853 [USACO06DEC] Cow Picnic S

解题思路

  • 构建图

    • 使用邻接表表示有向图,其中每个牧场指向可以到达的其他牧场。
    • 反向图也很重要,用于检查某个牧场是否可以被所有奶牛到达。
  • BFS 遍历

    • 对每头奶牛使用 BFS 遍历,从奶牛所在的牧场出发,找出所有可达的牧场,记录这些牧场。
    • 使用一个集合来存储所有可达的牧场。
  • 反向 BFS 检查

    • 对于每个在可达集合中的牧场,使用反向 BFS 来检查从该牧场能否到达所有奶牛的牧场。
    • 如果从当前牧场出发,可以访问到所有奶牛的起始牧场,则该牧场是可供聚餐的地点。
  • 结果统计

    • 统计所有能被所有奶牛到达的牧场数量。
import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        // 读取输入值
        int K = input.nextInt(); // 奶牛数量
        int N = input.nextInt(); // 牧场数量
        int M = input.nextInt(); // 路径数量

        int[] cowPositions = new int[K]; // 存储每头奶牛所在的牧场
        for (int i = 0; i < K; i++) {
            cowPositions[i] = input.nextInt();
        }

        List<List<Integer>> graph = new ArrayList<>(); // 牧场的有向图
        List<List<Integer>> reverseGraph = new ArrayList<>(); // 牧场的反向图

        for (int i = 0; i <= N; i++) {
            graph.add(new ArrayList<>());
            reverseGraph.add(new ArrayList<>());
        }

        // 读取有向路径并构建图
        for (int i = 0; i < M; i++) {
            int A = input.nextInt();
            int B = input.nextInt();
            graph.get(A).add(B); // A 到 B 的有向边
            reverseGraph.get(B).add(A); // 反向边
        }

        // 步骤 1:找出所有奶牛可以到达的牧场
        Set<Integer> reachableFromCows = new HashSet<>(); // 可达的牧场集合
        boolean[] visited = new boolean[N + 1]; // 访问标记

        // 对每头奶牛进行 BFS,找到可达的牧场
        for (int cow : cowPositions) {
            if (!visited[cow]) {
                bfs(cow, graph, visited, reachableFromCows);
            }
        }

        // 步骤 2:检查哪些可达牧场能够被所有奶牛到达
        int count = 0; // 可供进食的牧场计数
        for (int pasture : reachableFromCows) {
            visited = new boolean[N + 1]; // 重置访问标记
            int reachableCount = 0; // 记录可以到达的奶牛数量

            // 反向 BFS,检查当前牧场是否可以到达所有奶牛的牧场
            bfsReverse(pasture, reverseGraph, visited);

            // 统计可以到达的奶牛数量
            for (int cow : cowPositions) {
                if (visited[cow]) {
                    reachableCount++;
                }
            }

            // 如果当前牧场可以到达所有奶牛,计数加一
            if (reachableCount == K) {
                count++;
            }
        }

        // 输出结果
        System.out.println(count);
    }

    // BFS 方法,找出从起始牧场可达的所有牧场
    private static void bfs(int start, List<List<Integer>> graph, boolean[] visited, Set<Integer> reachable) {
        Queue<Integer> queue = new LinkedList<>();
        queue.add(start);
        visited[start] = true;

        while (!queue.isEmpty()) {
            int current = queue.poll(); // 取出队首
            reachable.add(current); // 记录可达牧场

            // 遍历相邻牧场
            for (int neighbor : graph.get(current)) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true; // 标记为已访问
                    queue.add(neighbor); // 入队
                }
            }
        }
    }

    // 反向 BFS 方法,找出能到达当前牧场的奶牛数量
    private static void bfsReverse(int start, List<List<Integer>> reverseGraph, boolean[] visited) {
        Queue<Integer> queue = new LinkedList<>();
        queue.add(start);
        visited[start] = true;

        while (!queue.isEmpty()) {
            int current = queue.poll(); // 取出队首

            // 遍历反向相邻牧场
            for (int neighbor : reverseGraph.get(current)) {
                if (!visited[neighbor]) {
                    visited[neighbor] = true; // 标记为已访问
                    queue.add(neighbor); // 入队
                }
            }
        }
    }
}

P1363 幻象迷宫

解题思路

解题代码修改自https://www.luogu.com.cn/record/206017952。

1. 输入处理

  • 迷宫大小:通过输入读取迷宫的行数 n 和列数 m。
  • 迷宫地图:读取迷宫的每一行数据,存储到二维字符数组 s 中。
  • 起点查找:遍历迷宫,找到起点 'S' 的位置 (startX, startY)

2. 广度优先搜索 (BFS)

  • 队列初始化:使用队列 Queue<Point> 存储当前访问的点,起点首先入队。
  • 状态标记:使用二维数组 book 记录每个位置的访问状态,状态通过自定义哈希函数 f(x, y) 计算。
  • 方向扩展:定义四个方向的移动向量 ne,分别表示右、下、左、上。
BFS 的核心逻辑:
  1. 从队列中取出当前点 (x, y)
  2. 遍历四个方向,计算新位置 (nx, ny)
  3. 无限地图映射:将无限地图的坐标 (nx, ny) 映射到有限范围 (tx, ty)
  4. 状态判断
    • 如果新位置是墙壁 '#',跳过。
    • 如果新位置未被访问过,标记状态并加入队列。
    • 如果新位置已被访问过,但状态不同,说明存在循环路径,返回 true
  5. 如果队列为空且未发现循环路径,返回 false

3. 自定义哈希函数

  • 函数 f(x, y) 用于计算每个位置的状态值,确保在无限地图中不同的坐标映射到有限范围时仍能区分状态。
  • 公式:

    ((x + n * 4) / n) * 131 + ((y + m * 4) / m) * 13

    • (x + n * 4) / n 和 (y + m * 4) / m 确保坐标映射到有限范围。
    • 乘以不同的常数(131 和 13)避免哈希冲突。

4. 无限地图映射

  • 迷宫是无限重复的,通过以下公式将无限坐标 (nx, ny) 映射到有限范围 (tx, ty)

    tx = (nx + n * 10) % n;

    ty = (ny + m * 10) % m;

    • + n * 10 和 + m * 10 确保坐标为正数。
    • % n 和 % m 将坐标限制在迷宫的行列范围内。
import java.util.*;

public class Main {
    // 定义四个方向的移动向量:右、下、左、上
    static int[][] ne = {{0,1}, {1,0}, {0,-1}, {-1,0}};
    static int[][] book; // 记录每个位置的状态
    static char[][] s;   // 存储迷宫地图
    static int n, m;     // 迷宫的行数和列数

    // 定义一个点的类,用于存储坐标
    static class Point {
        int x, y;
        Point(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }

    // 自定义哈希函数,用于标记每个位置的状态
    static int f(int x, int y) {
        return ((x + n * 4) / n) * 131 + ((y + m * 4) / m) * 13;
    }

    // 广度优先搜索 (BFS) 判断是否存在循环路径
    static boolean bfs(int x, int y) {
        Queue<Point> q = new LinkedList<>(); // 队列用于存储当前访问的点
        q.offer(new Point(x, y)); // 将起点加入队列
        book[x][y] = f(x, y); // 标记起点的状态

        while (!q.isEmpty()) {
            Point u = q.poll(); // 取出队首元素
            for (int[] dir : ne) { // 遍历四个方向
                int nx = u.x + dir[0]; // 新的 x 坐标
                int ny = u.y + dir[1]; // 新的 y 坐标

                // 处理无限地图的坐标映射
                int tx = (nx + n * 10) % n; // 映射到迷宫内的行
                int ty = (ny + m * 10) % m; // 映射到迷宫内的列
                int t = f(nx, ny); // 计算新的状态

                // 如果当前位置是墙壁,跳过
                if (s[tx][ty] == '#') continue;

                // 如果当前位置未被访问过
                if (book[tx][ty] == 0) {
                    book[tx][ty] = t; // 标记状态
                    q.offer(new Point(nx, ny)); // 加入队列
                }
                // 如果当前位置被访问过,但状态不同,说明存在循环
                else if (book[tx][ty] != t) {
                    return true;
                }
            }
        }
        return false; // 如果搜索结束没有发现循环,返回 false
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String temp = scanner.nextLine().trim(); // 读取迷宫的大小
            if (temp.isEmpty()) break; // 输入结束条件
            n = Integer.parseInt(temp.split(" ")[0]); // 行数
            m = Integer.parseInt(temp.split(" ")[1]); // 列数

            s = new char[n][m]; // 初始化迷宫地图
            book = new int[n][m]; // 初始化标记数组

            // 读取地图数据
            for (int i = 0; i < n; i++) {
                String l = scanner.nextLine().trim();
                while (l.isEmpty()) { // 跳过空行
                    l = scanner.nextLine().trim();
                }
                s[i] = l.toCharArray(); // 将每行数据存入地图
            }

            // 查找起点 'S'
            int startX = 0, startY = 0;
            outerloop:
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < m; j++) {
                    if (s[i][j] == 'S') { // 找到起点
                        startX = i;
                        startY = j;
                        break outerloop; // 跳出双重循环
                    }
                }
            }

            // 输出结果:是否存在循环路径
            System.out.println(bfs(startX, startY) ? "Yes" : "No");
        }
        scanner.close();
    }
}

P1347 排序

解题思路

  • 输入处理

    • 首先读取节点数量 n 和关系数量 m
    • 根据输入的边(关系),更新邻接表和入度数组,建立图的结构。
  • 拓扑排序逻辑

    • 通过 topo(int r) 函数进行拓扑排序。该函数尝试构造一个拓扑排序的结果。
    • 维护一个临时入度数组 t,并将入度为0的节点入栈(代表可以先访问的节点)。
    • 在处理每个节点时,如果发现有多个节点可以入栈(即有多个入度为0的节点),则标记 finishedfalse,表示当前状态下可能有多种排序方式。
    • 每当处理完一个节点后,更新其相邻节点的入度,并检查这些相邻节点是否可以入栈。
    • 如果最终排序结果中的节点数量少于总节点数量,表示图中存在环,返回 false
  • 判断和输出结果

    • 在每次添加新关系后,都执行一次拓扑排序,如果发现不一致(如存在环),则输出错误信息并终止。
    • 如果成功完成排序且找到了有效的排序顺序,则打印排序结果。
    • 如果在处理完所有关系后仍未找到有效排序,则输出“无法确定排序顺序”。
import java.util.*;

public class Main {
    static final int MAXN = 30; // 最大节点数

    static int n, m; // n 为节点数量,m 为关系数量
    static List<Integer>[] e = new ArrayList[MAXN]; // 邻接表
    static int[] degree = new int[MAXN]; // 入度数组
    static int[] a = new int[MAXN]; // 排序结果
    static Stack<Integer> s = new Stack<>(); // 用于拓扑排序的栈
    static boolean[] vis = new boolean[MAXN]; // 访问标记
    static int mrk = 0; // 标记是否成功排序

    // 拓扑排序函数,返回值为真表示成功
    static boolean topo(int r) {
        int sz = 0; // 记录排序结果的大小
        boolean finished = true; // 标记是否存在多个入度为0的节点
        int[] t = new int[MAXN]; // 用于临时存储入度

        // 初始化入度和入度为0的节点
        for (int i = 0; i < n; i++) {
            t[i] = degree[i];
            if (t[i] == 0) {
                s.push(i);
                vis[i] = true; // 标记为已访问
            }
        }

        while (!s.isEmpty()) {
            if (s.size() > 1) finished = false; // 存在多个入度为0的节点
            int k = s.pop(); // 取出栈顶元素
            a[sz++] = k; // 记录排序结果
            // 更新邻接节点的入度
            for (int v : e[k]) {
                t[v]--;
            }
            // 查找新的入度为0的节点
            for (int i = 0; i < n; i++) {
                if (t[i] == 0 && !vis[i]) {
                    s.push(i);
                    vis[i] = true; // 标记为已访问
                }
            }
        }

        if (sz < n) return false; // 如果排序结果小于节点数,表示有循环
        if (finished && mrk == 0) mrk = r; // 更新成功标记
        return true;
    }

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        n = input.nextInt(); // 输入节点数量
        m = input.nextInt(); // 输入关系数量

        // 初始化邻接表
        for (int i = 0; i < MAXN; i++) {
            e[i] = new ArrayList<>();
        }

        // 输入边并构建图
        for (int i = 1; i <= m; i++) {
            String c = input.next(); // 读取边
            int x = c.charAt(0) - 'A'; // 起点
            int y = c.charAt(2) - 'A'; // 终点
            e[x].add(y); // 添加边
            degree[y]++; // 更新入度

            if (mrk > 0) {
                break; // 如果已经成功排序,跳过后续输入(记录成环的特殊情况)
            }

            // 执行拓扑排序
            if (!topo(i)) {
                System.out.println("Inconsistency found after " + i + " relations.");
                return; // 如果存在矛盾,终止程序
            }
            Arrays.fill(vis, false); // 重置访问标记
        }

        // 输出排序结果
        if (mrk > 0) {
            System.out.print("Sorted sequence determined after " + mrk + " relations: ");
            for (int j = 0; j < n; j++) {
                System.out.print((char)(a[j] + 'A')); // 输出排序的字母
            }
            System.out.println(".");
        } else {
            System.out.println("Sorted sequence cannot be determined.");
        }
    }
}

P1983 [NOIP2013 普及组] 车站分级

解题思路

  • 图的表示

    • 使用 Node 类表示图中的每个节点。每个节点包含:
      • level: 表示该节点的层级,初始值为 1。
      • in_d: 表示该节点的入度(指向该节点的边的数量)。
      • adj: 使用 BitSet 来存储该节点的邻接节点(即哪些节点指向它)。
  • 输入处理

    • 使用 StreamTokenizer 进行高效的输入处理,以便快速读取大量数据。
    • 首先读取节点数 N,然后读取边的数目 M
  • 构建图

    • 对于每条边,首先记录每个相关节点的入度。
    • 遍历节点范围,将未被访问的节点标记,并建立它们与当前边的邻接关系。
  • 拓扑排序

    • 使用队列(ArrayDeque)来存储入度为 0 的节点。
    • 在遍历队列时,处理当前节点的所有邻接节点,减少它们的入度。如果某个邻接节点的入度变为 0,则将其加入队列。
    • 同时更新每个邻接节点的层级,确保每个节点的层级总是反映其在图中的位置。
  • 计算最大层级

    • 遍历所有节点,找到最大层级值,输出结果。
import java.io.*;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.BitSet;

public class Main {
    // 定义节点类,保存每个节点的深度和入度
    private static class Node {
        short level;  // 当前节点的层级
        short in_d = 0;  // 该节点的入度
        BitSet adj;  // 邻接表,用 BitSet 存储连接的节点

        Node() {
            level = 1;  // 默认层级为 1
        }
    }

    public static void main(String[] args) throws IOException {
        // 使用 StreamTokenizer 进行高效的输入处理
        StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));

        // 读取节点数量 N
        in.nextToken();
        int N = (int) in.nval;

        // 创建节点数组
        Node[] nodes = new Node[N + 1];
        boolean[] vis = new boolean[N + 1];  // 记录是否访问过
        short[] st = new short[N + 1];  // 存储临时数据

        // 初始化节点
        for (int i = 1; i < N + 1; i++) nodes[i] = new Node();

        // 读取边的数量 M
        in.nextToken();
        int M = (int) in.nval;

        // 处理每条边
        for (int i = 0; i < M; i++) {
            in.nextToken();  // 读取当前边的节点数
            st[0] = (short) in.nval;  // st[0] 存储当前边的节点数

            // 读取当前边的所有节点
            for (int j = 1; j <= st[0]; j++) {
                in.nextToken();
                st[j] = (short) in.nval;  // 将节点存入 st 数组
                vis[st[j]] = true;  // 标记节点为已访问
            }

            // 遍历节点范围,设置入度和邻接关系
            for (int j = st[1]; j <= st[st[0]]; j++) {
                if (vis[j]) continue;  // 如果节点已经访问过,跳过

                // 初始化邻接表
                if (nodes[j].adj == null) nodes[j].adj = new BitSet();

                // 更新入度和邻接关系
                for (int k = 1; k <= st[0]; k++) {
                    if (!nodes[j].adj.get(st[k])) {  // 如果未连接
                        nodes[st[k]].in_d++;  // 增加入度
                        nodes[j].adj.set(st[k]);  // 记录邻接关系
                    }
                }
            }
            Arrays.fill(vis, false);  // 清空访问标记
        }

        // 使用队列进行拓扑排序
        ArrayDeque<Node> deque = new ArrayDeque<>();
        // 将入度为 0 的节点加入队列
        for (int i = 1; i <= N; i++) if (nodes[i].in_d == 0) deque.add(nodes[i]);

        // 拓扑排序过程
        while (!deque.isEmpty()) {
            Node cur = deque.poll();  // 取出队首节点
            if (cur.adj == null) continue;  // 如果没有邻接节点,跳过

            // 遍历当前节点的所有邻接节点
            int i = cur.adj.nextSetBit(0);
            while (i >= 0) {
                // 减少邻接节点的入度
                if (--nodes[i].in_d == 0) {
                    deque.add(nodes[i]);  // 如果入度为 0,则加入队列
                    // 更新邻接节点的层级
                    if (cur.level + 1 > nodes[i].level) nodes[i].level = (short) (cur.level + 1);
                }
                i = cur.adj.nextSetBit(i + 1);  // 获取下一个邻接节点
            }
        }

        // 计算最大层级
        int ans = 0;
        for (int i = 1; i <= N; i++) ans = Math.max(ans, nodes[i].level);

        // 输出结果
        System.out.println(ans);
    }
}

相关文章:

  • 15:00开始面试,15:08就出来了,问的问题有点变态。。。
  • 射频功率放大器保护电路简略
  • 消息中间件对比与选型指南:Kafka、ActiveMQ、RabbitMQ与RocketMQ
  • Oracle数据库数据编程SQL<3.6 PL/SQL 包(Package)>
  • 25.4.1学习总结【Java】
  • 嵌入式EMC设计面试题及参考答案
  • 汇编学习之《移位指令》
  • Citus源码(2)分布式读流程分析与基础概念梳理(shardid、placementid、groupid)
  • 【QT】QT的多界面跳转以及界面之间传递参数
  • 【超详细】一文解决更新小米澎湃2.0后LSPose失效问题
  • 使用 Less 实现 PC 和移动端样式适配
  • Java基础-27-多态-多态好处和存在的问题
  • win server2022 限制共享文件夹d
  • PWA 进阶教程(二): 如何在 PWA 中实现推送通知
  • Linux系统调用编程
  • LeetCode102.二叉树的层序遍历
  • 【操作系统】Linux进程管理和调试
  • QML Book 学习基础6(定位/布局元素)
  • 【浏览器的渲染原理】
  • uniapp微信小程序开发工具本地获取指定页面二维码
  • 摄影师|伊莎贝尔·穆尼奥斯:沿着身体进行文化溯源
  • 首次带人形机器人走科技节红毯,傅利叶顾捷:机器人行业没包袱,很多事都能从零开始
  • “GoFun出行”订单时隔7年扣费后续:平台将退费,双方已和解
  • 雅典卫城上空现“巨鞋”形状无人机群,希腊下令彻查
  • 嫩黑线货物列车脱轨致1名路外人员死亡,3人被采取刑事强制措施
  • 赡养纠纷个案推动类案监督,检察机关保障特殊群体胜诉权