java 洛谷题单【数据结构1-4】图的基本应用
P5318 【深基18.例3】查找文献
解题思路
图的构建:使用邻接表存储每个文献的引用关系。读取输入后,对每个节点的邻接表进行排序和去重,以确保节点按升序排列。
DFS遍历:使用栈来实现非递归遍历。每次从栈中弹出节点后,将其邻接节点逆序压入栈中,以确保优先处理编号较小的节点。
BFS遍历:使用队列来实现广度优先搜索。按邻接表的顺序处理节点,确保先访问编号较小的节点。
结果输出:将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 最大食物链计数
解题思路
问题分析:题目要求计算从生产者(入度为0的节点)到顶级消费者(出度为0的节点)的所有路径数目之和。这类问题可以通过拓扑排序结合动态规划(DP)来解决。
图构建:使用邻接表存储每个节点的后继节点(即该节点被哪些生物捕食),并统计每个节点的入度和出度。
拓扑排序:初始化所有生产者节点的路径数为1,然后按照拓扑顺序处理每个节点,将其路径数累加到其后继节点的路径数中。
动态规划:每个节点的路径数表示以该节点为终点的路径数目。在拓扑排序过程中,逐步更新每个节点的路径数。
结果计算:遍历所有出度为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
数组记录每个字符作为字符串开始字符的数量。邻接表构建:
- 对字符串进行字典序排序。通过两层循环检查每个字符串对,如果一个字符串的最后一个字符与另一个字符串的第一个字符相同,则在邻接表中记录这一边。
欧拉路径搜索:
- 欧拉路径要求图的入度和出度特定匹配。通常,起点的出度应大于入度一,而其他节点的入度与出度应相等。因此,使用
ind
和rnd
数组来找到合适的起点。- 使用 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 的核心逻辑:
- 从队列中取出当前点
(x, y)
。- 遍历四个方向,计算新位置
(nx, ny)
。- 无限地图映射:将无限地图的坐标
(nx, ny)
映射到有限范围(tx, ty)
。- 状态判断:
- 如果新位置是墙壁
'#'
,跳过。- 如果新位置未被访问过,标记状态并加入队列。
- 如果新位置已被访问过,但状态不同,说明存在循环路径,返回
true
。- 如果队列为空且未发现循环路径,返回
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的节点),则标记
finished
为false
,表示当前状态下可能有多种排序方式。- 每当处理完一个节点后,更新其相邻节点的入度,并检查这些相邻节点是否可以入栈。
- 如果最终排序结果中的节点数量少于总节点数量,表示图中存在环,返回
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);
}
}