图论
1 图论
1.1 图的建立
1.1.1 领接表边权建图
import java.util.ArrayList; import java.util.List; import java.util.Scanner; public class Main { // 定义图的邻接表表示 static List<int[]>[] g; // 节点数 static int n; // 保存某种状态或结果的数组 static int[] f; public static void main(String[] args) { Scanner scanner = new Scanner(System.in); // 读取节点数 n = scanner.nextInt(); // 初始化图的邻接表 g = new ArrayList[n + 1]; for (int i = 1; i <= n; i++) { g[i] = new ArrayList<>(); } // 初始化状态数组 f = new int[n + 1]; // 读取边的信息并构建图 for (int i = 1; i < n; i++) { int a = scanner.nextInt(); // 边的一个端点 int b = scanner.nextInt(); // 边的另一个端点 int c = scanner.nextInt(); // 边的权重 // 添加无向边到邻接表 g[a].add(new int[]{b, c}); g[b].add(new int[]{a, c}); } } // 深度优先搜索(DFS)遍历图 static void dfs(int u, int fa) { // 遍历节点u的所有邻接节点 for (int[] pair : g[u]) { int x = pair[0]; // 邻接节点 int w = pair[1]; // 边的权重 // 如果邻接节点是父节点,则跳过(避免走回头路) if (x == fa) continue; // 递归遍历邻接节点 dfs(x, u); // 这里可以添加其他的逻辑处理代码 } } }
1.1.2 领接表点权建图
import java.util.Scanner; import java.util.ArrayList; public class Main { // 定义常量N为节点数的上限 static final int N = 100010; // 用于存储图的邻接表表示 static ArrayList<Integer>[] g = new ArrayList[N]; // 用于存储节点的权值 static int[] w = new int[N]; // 深度优先搜索(DFS)遍历图 static void dfs(int u, int fa) { // 处理节点u的操作 // 遍历节点u的所有邻接节点 for (int x : g[u]) { // 如果邻接节点是父节点,则跳过(避免走回头路) if (x == fa) continue; // 递归遍历邻接节点 dfs(x, u); // 处理子节点返回后的操作 } } public static void main(String[] args) { int n, m; Scanner scanner = new Scanner(System.in); // 读取节点数n和边数m n = scanner.nextInt(); m = scanner.nextInt(); // 初始化邻接表 for (int i = 1; i <= n; i++) { g[i] = new ArrayList<>(); } // 读取边的信息并构建图 for (int i = 0; i < m; i++) { int a, b; a = scanner.nextInt(); b = scanner.nextInt(); g[a].add(b); // 添加一条从a到b的边 } // 读取每个节点的权值 for (int i = 1; i <= n; i++) { w[i] = scanner.nextInt(); } // 可以在这里调用dfs进行深度优先搜索,例如从节点1开始 // dfs(1, -1); } }
1.1.3 离散化建图
- 点的范围很大(例如 [−109,109]),或者含有负数的点。
- 点不是数值,而是字符串,字符串与字符串之间存在相互转换的关系。
import java.util.*; public class Main { public static void main(String[] args) { // 定义一个常量N,表示可能的最大节点数 int N = 100010; // 定义一个哈希表,用于存储节点之间的边及其权重 // 键是节点,值是一个包含目标节点及边权的列表 Map<Integer, List<Map.Entry<Integer, Integer>>> path = new HashMap<>(); // 示例:添加从节点u到节点v,边的权重为w int u = 1, v = 2, w = 3; // 如果哈希表中不包含键u,初始化其值为一个新的ArrayList if (!path.containsKey(u)) { path.put(u, new ArrayList<>()); } // 将边(v, w)添加到节点u的列表中 path.get(u).add(new AbstractMap.SimpleEntry<>(v, w)); // 示例:遍历节点node的所有相邻节点和边权 int node = 1; // 如果哈希表中包含键node,进行遍历 if (path.containsKey(node)) { for (Map.Entry<Integer, Integer> entry : path.get(node)) { int target = entry.getKey(); // 相邻节点 int weight = entry.getValue(); // 边的权重 // 进行处理,例如打印或其他逻辑 System.out.println("从节点 " + node + " 到节点 " + target + " 的边权是 " + weight); } } } }
1.2 拓扑排序
用于判断有向图中是否存在环
a
必须要先学习课程b
,则有b->a
这一条有向边。如果可以完成所有课程,则说明存在拓扑排序。import java.util.*; class Solution { public boolean canFinish(int numCourses, int[][] prerequisites) { // 创建图的邻接表表示 List<List<Integer>> g = new ArrayList<>(); for (int i = 0; i < numCourses; i++) { g.add(new ArrayList<>()); // 为每个节点初始化邻接表 } // 创建数组存储每个节点的入度 int[] d = new int[numCourses]; // 构建图并计算每个节点的入度 for (int[] p : prerequisites) { g.get(p[1]).add(p[0]); // 添加从节点p[1]到节点p[0]的边 d[p[0]]++; // 增加节点p[0]的入度 } // 初始化队列用于BFS Queue<Integer> q = new LinkedList<>(); int cnt = 0; // 计数器,记录可以遍历的节点数量 // 将所有入度为0的节点添加到队列中 for (int i = 0; i < numCourses; i++) { if (d[i] == 0) { q.offer(i); cnt++; } } // 执行BFS处理节点 while (!q.isEmpty()) { int t = q.poll(); // 从队列中取出一个节点 for (int v : g.get(t)) { // 遍历所有依赖于t的节点 d[v]--; // 减少依赖节点的入度 if (d[v] == 0) { // 如果依赖节点的入度变为0,将其加入队列 q.offer(v); cnt++; } } } // 如果处理的节点数量等于总节点数,则表示可以遍历所有节点,否则不能 return cnt == numCourses; } }
华为实习2023042601
开发一个新的3D引擎,包含多个模块,每个模块负责不同的功能,比如渲染、物理、音效、网络等。 为了提高性能和稳定性,需要在游戏启动时初始化这些模块。但是不同的模块之间存在依赖关系,比如渲染模块依赖于物理模块,音效模块依赖于网络模块等。如果不按照正确的顺序初始化这些模块,就会导致错误或崩溃。需要开发一个代码分析工具,分析代码模块之间的依赖关系,确定模块的初始化顺序,判断是否有循环依赖等问题。 工具可以一次初始化一个或多个模块,只要它们之间没有依赖关系。这个过程称为引擎模块初始化。 已经得到了一组模块间的依赖关系,需要计算引擎模块初始化的次数。
输入描述
- 第一行是一个整数 `n`,表示模块总数。
- 接下来的 `n` 行表示模块 `1` 到 `n` 的依赖关系。
- 每行的第一个数是一个整数 `m`,表示依赖的模块数量,之后的数字表示当前模块依赖的模块ID。
- 每一行的数字按空格分隔。
约束条件:
- 1 ≤ m ≤ n ≤ 1000
输出描述
- 输出批量初始化次数。
- 若有循环依赖无法完成初始化,则输出 `-1`
样例
样例一
输入
5 3 2 3 4 1 5 1 5 1 5 0
输出
3
解释
共 5 个模块。
模块 `1` 依赖模块 `2`、`3`、`4`;
模块 `2` 依赖模块 `5`;
模块 `3` 依赖模块 `5`;
模块 `4` 依赖模块 `5`;
模块 `5` 没有依赖。
批量初始化顺序为 `{5} -> {2, 3, 4} -> {1}`,共需 3 次批量初始化。
代码
import java.util.*; public class Main { public void solution(){ Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); // 读取模块总数 int[] inDegree = new int[n + 1]; // 入度数组,用于记录每个模块的依赖计数 inDegree[0] = -1; // 虚拟的0号节点,设置为-1 ArrayList<Integer>[] outDegree = new ArrayList[n + 1]; // 出度表,用于记录每个模块的被依赖关系 // 读取每个模块的依赖信息 for(int i = 1; i <= n; ++i){ int m = scanner.nextInt(); // 读取当前模块的依赖数量 inDegree[i] += m; // 更新入度 for(int j = 0; j < m; ++j){ int from = scanner.nextInt(); // 读取依赖的模块ID if(outDegree[from] == null) outDegree[from] = new ArrayList<>(); outDegree[from].add(i); // 将当前模块添加到依赖列表中 } } scanner.close(); int res = 0; // 记录批量初始化次数 boolean check; // 用于判断当前批次是否有可初始化的模块 Queue<Integer> queue = new LinkedList<>(); // 队列用于BFS // 执行拓扑排序 do { check = false; // 找出所有入度为0的模块 for(int i = 1; i < inDegree.length; ++i){ if(inDegree[i] == 0){ inDegree[i] = -1; // 标记为已处理 check = true; queue.offer(i); // 将模块加入队列 } } if(check) res += 1; // 增加批次计数 // 处理队列中的模块 while(!queue.isEmpty()){ int from = queue.poll(); // 取出队首模块 if(outDegree[from] == null) continue; // 减少依赖模块的入度 for(int i = 0; i < outDegree[from].size(); ++i) --inDegree[outDegree[from].get(i)]; } }while(check); // 检查是否存在未处理的模块 for(int i = 1; i < inDegree.length; ++i){ if(inDegree[i] != -1){ System.out.println(-1); // 存在循环依赖,输出-1 return; } } System.out.println(res); // 输出批量初始化次数 } public static void main(String[] args) { Main m = new Main(); m.solution(); // 调用解决方案 } }
华为秋招20230830
系统由n个任务组成,任务运行有依赖关系,前序任务执行完毕才可以启动后续任务。任务在启动前申请内存,执行完毕后释放,内存释放后可用于其他任务使用。
解除依赖后的任务会直接由操作系统调度,分配内存,进入运行状态。每个任务的运行时间相等。请计算系统所有任务执行所需要的最小内存。
输入
第1行为1个正整数n,表示任务个数,n<20
第2行为n个正整数,表示每个任务所需要的内存大小,0<内存<10000
第3行为n个取值为0或1的数,表示任务0对其他任务的依赖关系,0表示不依赖,1表示依赖
....
第3+n行为n个取值为0或1的数,表示任务n−1对其他任务的依赖关系,0表示不依赖,1表示依赖
输出
输出系统任务执行所需要的最小内存
样例
输入
9 50 50 80 40 40 40 60 60 60 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 1 0
输出
120
解释
样例解释
第一行:9
,表示有 9 个任务
第二行:50 50 80 40 40 40 60 60 60
,表示每个任务所需要的内存大小
t0
需要 50t1
需要 50t2
需要 80t3
需要 40t4
需要 40t5
需要 40t6
需要 60t7
需要 60t8
需要 60
第三行:0 0 0 0 0 0 0 0 0
,表示 t0
不依赖任何任务
第四行:1 0 0 0 0 0 0 0 0
,表示 t1
依赖 t0
第五行:0 1 0 0 0 0 0 0 0
,表示 t2
依赖 t1
...依此类推
任务的关系用图表示:
- 执行
t0
,分配m0 = 50
,占用空间[0, 50)
,最大访问地址为 50 - 执行
t1
,分配m1 = 50
,占用空间[0, 50)
,最大访问地址为 50 - 并发执行
t2
和t5
,分配m2 = 80
,m5 = 40
,占用空间[0, 120)
,最大访问地址为 120 - 执行
t3
,分配m3 = 40
,占用空间[0, 40)
,最大访问地址为 40 - 并发执行
t4
和t7
,分配m4 = 40
,m7 = 60
,占用空间[0, 100)
,最大访问地址为 100 - 执行
t6
,分配m6 = 60
,占用空间[0, 60)
,最大访问地址为 60 - 执行
t8
,分配m8 = 60
,占用空间[0, 60)
,最大访问地址为 60
输出系统需要的最小内存为 120
思路:拓扑排序+贪心
通过拓扑排序,我们可以确保每个任务按其依赖关系的顺序执行。每一轮计算并行执行的任务所需的总内存,并取这些总内存的最大值作为最终答案。这个方法能有效地解决问题并保证计算出的内存需求最小化。
import java.util.*; public class Main { // 定义常量和变量 static final int M = 20; // 最大任务数量 static int[] in = new int[M]; // 记录每个任务的入度 static int[] a = new int[M]; // 记录每个任务所需的内存 public static void main(String[] args) { Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); // 读取任务数 ArrayList<Integer>[] e = new ArrayList[M]; // 邻接表,存储依赖关系 // 读取每个任务的内存需求并初始化邻接表 for (int i = 1; i <= n; i++) { a[i] = scanner.nextInt(); e[i] = new ArrayList<>(); } // 读取依赖关系矩阵并构建图 for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { int t = scanner.nextInt(); if (t != 0) { in[i]++; // 增加任务i的入度 e[j].add(i); // 添加一条从j到i的边 } } } // 初始化队列,用于拓扑排序 Queue<Integer> q = new LinkedList<>(); Queue<Integer> tmp = new LinkedList<>(); // 临时队列,用于保存本轮入度为0的任务 for (int i = 1; i <= n; i++) { if (in[i] == 0) { q.add(i); // 将初始入度为0的任务加入队列 } } int ans = 0; // 保存最终的最小内存需求 // 开始拓扑排序 while (!q.isEmpty()) { int sum = 0; // 记录当前轮次所有任务的内存总和 while (!q.isEmpty()) { int u = q.poll(); sum += a[u]; // 累加当前任务的内存需求 // 遍历当前任务的所有后继任务 for (int i = 0; i < e[u].size(); i++) { int v = e[u].get(i); in[v]--; // 将后继任务的入度减1 if (in[v] == 0) { tmp.add(v); // 如果后继任务的入度为0,加入临时队列 } } } ans = Math.max(ans, sum); // 更新最大内存需求 q.addAll(tmp); // 将临时队列中的任务加入主队列,进行下一轮处理 tmp.clear(); // 清空临时队列 } // 输出最终计算出的最小内存需求 System.out.println(ans); } }
1.3 Dijkstra
求解从某个起点到达图上任意一点的最短路径(BFS 只能求解边权值为1)
import java.util.*; import java.io.*; class Main { static int N = 100010; // 定义常量,最大点数 static int n, m, idx; // n为点数,m为边数,idx为边的编号 static int[] h = new int[N]; // h数组,邻接表头结点 static int[] w = new int[N]; // w数组,边权重 static int[] e = new int[N]; // e数组,边指向的节点 static int[] ne = new int[N]; // ne数组,邻接表下一个节点 static int[] dist = new int[N]; // dist数组,记录1号点到各点的最短距离 static boolean[] st = new boolean[N]; // st数组,记录每个点是否已确定最短路 // 添加边的函数 private static void add(int a, int b, int c) { e[idx] = b; // 边的终点 w[idx] = c; // 边的权重 ne[idx] = h[a]; // 当前边的下一个边 h[a] = idx++; // 更新头结点 } public static void main(String[] args) throws Exception { // 使用BufferedReader和BufferedWriter进行输入输出 BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); BufferedWriter log = new BufferedWriter(new OutputStreamWriter(System.out)); // 读取第一个输入行 String[] values = br.readLine().split("\\s+"); n = Integer.parseInt(values[0]); m = Integer.parseInt(values[1]); // 初始化邻接表头结点 Arrays.fill(h, -1); // 读取所有边的信息 while (m-- > 0) { values = br.readLine().split("\\s+"); int a = Integer.parseInt(values[0]); int b = Integer.parseInt(values[1]); int c = Integer.parseInt(values[2]); add(a, b, c); } // 计算1号点到n号点的最短路径 int ret = dijkstra(n); // 输出结果 log.write(ret + "\n"); log.flush(); log.close(); br.close(); } // 堆优化的Dijkstra算法 private static int dijkstra(int n) { // 优先队列,按照到达节点的最短距离排序 PriorityQueue<int[]> pq = new PriorityQueue<>(n, (a, b) -> a[1] - b[1]); int INF = 1 << 30; // 定义一个无限大值 Arrays.fill(dist, INF); // 初始化距离数组,全部设为无穷大 pq.offer(new int[]{1, 0}); // 将起点1加入优先队列 dist[1] = 0; // 起点到自己的距离为0 // 主循环 while (!pq.isEmpty()) { int[] cur = pq.poll(); int ver = cur[0]; // 当前处理的节点 int distance = cur[1]; // 当前节点的最短距离 // 如果该节点已被处理,则跳过 if (st[ver]) continue; st[ver] = true; // 标记该节点为已处理 // 遍历当前节点的所有邻边 for (int i = h[ver]; i != -1; i = ne[i]) { int j = e[i]; // 目标节点 // 如果找到更短的路径,则更新 if (dist[j] > distance + w[i]) { dist[j] = distance + w[i]; pq.offer(new int[]{j, dist[j]}); // 将更新后的节点加入优先队列 } } } // 如果n号点的最短路径仍为无穷大,说明不可达 return dist[n] == INF ? -1 : dist[n]; } }
华为2023082303
有一些镜子,能够吸收光芒并在一定时间后散射。
镜子分为一级镜和二级镜,一级镜散射速度为1ms,二级镜为2ms。
将镜子放在一个二维矩阵中,每个镜子的坐标为整数。
现在给某个镜子一道光芒,最早什么时候所有镜子都能吸收到光芒?
注:矩阵的下标从0开始。
输入描述:
矩阵的列数 n ( n≤500 )
矩阵的行数 m ( m ≤ 500 )
最初获得光芒的镜子的坐标 (i, j)
接下来 m 行,每行 n 个数字,代表该位置镜子的等级:
- 0 表示该位置是一堵密不透光的墙
- 1 表示该位置的镜子散射耗时1ms
- 2 表示该位置的镜子散射耗时2ms
输出描述:
一个数字代表最小时间。如果有镜子不能够吸收到光芒,输出-1。
样例:
输入:
5 5 2 2 1 0 2 1 0 2 2 1 2 0 0 0 1 0 0 2 1 1 0 0 1 1 1 1 1
输出:
6
思路:
找到所有镜子接收到光芒的最早时间=>单源最短路算法:Dijkstra:
-
每一步,从队列中取出时间最早的镜子。
-
更新其四个方向镜子的接收时间。
-
如果新的接收时间比当前的接收时间早,则更新接收时间,并将新的镜子加入队列。
import java.util.*; public class Main { public static void main(String[] args) { Scanner in = new Scanner(System.in); // 读取输入,直到没有更多的整数输入 while (in.hasNextInt()) { int n = in.nextInt(); // 矩阵的行数 int m = in.nextInt(); // 矩阵的列数 int[][] mirrors = new int[n][m]; // 存储镜子等级的矩阵 int initI = in.nextInt(); // 初始光芒镜子的行坐标 int initJ = in.nextInt(); // 初始光芒镜子的列坐标 // 读取矩阵中的镜子等级 for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { mirrors[i][j] = in.nextInt(); } } int[][] times = new int[n][m]; // 存储每个镜子接收到光芒的时间 // 初始化时间矩阵,初始值设为一个较大的数 for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { times[i][j] = Integer.MAX_VALUE; } } // 优先队列,用于进行Dijkstra算法 Queue<int[]> queue = new PriorityQueue<>(new Comparator<int[]>() { @Override public int compare(int[] o1, int[] o2) { return o1[2] - o2[2]; // 按时间排序 } }); int[][] visited = new int[n][m]; // 标记每个镜子是否已经接收到光芒 queue.offer(new int[]{initI, initJ, 0}); // 将初始镜子加入队列,时间设为0 // Dijkstra算法 while (!queue.isEmpty()) { int[] poll = queue.poll(); // 取出时间最早的镜子 int x = poll[0]; int y = poll[1]; int time = poll[2]; // 边界条件检查 if (x < 0 || x >= mirrors.length || y < 0 || y >= mirrors[0].length) { continue; } // 如果是墙则跳过 if (mirrors[x][y] == 0) { continue; } // 如果已经访问过则跳过 if (visited[x][y] == 1) { continue; } visited[x][y] = 1; // 标记为已访问 times[x][y] = time; // 更新当前镜子的时间 // 将四个方向的镜子加入队列,并更新时间 queue.offer(new int[]{x - 1, y, time + mirrors[x][y]}); queue.offer(new int[]{x + 1, y, time + mirrors[x][y]}); queue.offer(new int[]{x, y - 1, time + mirrors[x][y]}); queue.offer(new int[]{x, y + 1, time + mirrors[x][y]}); } int max = 0; // 用于找出接收到光芒的最晚时间 for (int i = 0; i < n; i++) { for (int j = 0; j < m; j++) { // 只考虑不是墙的镜子 if (mirrors[i][j] != 0) { max = Math.max(max, times[i][j]); } } } // 如果有镜子不能接收到光芒,则返回-1 if (max == Integer.MAX_VALUE) { System.out.println(-1); } else { System.out.println(max); // 输出最早时间 } } } }
饿了么2023081703
有一个迷宫,有 `n` 个地点,通过 `m` 条道路联通。每次需要从起点(1号节点)取一面旗帜送到指定地点,然后返回起点,直到将所有旗帜送完。
你需要知道送完所有旗帜的最短路程是多少。
输入描述
- 第一行输入三个整数 `n`, `m`, `q`,分别表示地点数、路径数和需要送达旗帜的地点数。
- 接下来 `m` 行,每行输入三个整数 `u`, `v`, `w`,表示地点 `u` 和地点 `v` 之间有一条长度为 `w` 的道路。
- 最后一行输入 `q` 个整数,表示需要送达旗帜的 `q` 个地点。
输出描述
一个整数表示最短总路程。
示例
输入
4 3 3
1 2 1
2 3 2
3 4 3
2 3 4
输出
20
说明
从 1 号点到 2 号点再回来,路程距离为 2。
再从 1 号点到 3 号点再回来,路程距离为 6。
最后从 1 号点到 4 号点再回来,路程距离为 12。
思路:
利用Dijkstra,初始化从起点到其余n-1个节点最短路径dist,最后遍历并累加 dist[qi]*2即可。
import java.util.*; public class Main { public static void main(String[] args) { final int INF = 0x3f3f3f3f; // 定义无穷大 Scanner scanner = new Scanner(System.in); int n = scanner.nextInt(); // 地点数 int m = scanner.nextInt(); // 路径数 int T = scanner.nextInt(); // 需要送达旗帜的地点数 List<List<PII>> g = new ArrayList<>(); // 图的邻接表表示 for (int i = 0; i < n; i++) { g.add(new ArrayList<>()); } // 读取图的边 for (int i = 0; i < m; i++) { int u = scanner.nextInt() - 1; // 起点 int v = scanner.nextInt() - 1; // 终点 int w = scanner.nextInt(); // 边的权重 g.get(u).add(new PII(v, w)); g.get(v).add(new PII(u, w)); } // 优先队列,用于Dijkstra算法 PriorityQueue<PII> q = new PriorityQueue<>(Comparator.comparingInt(PII::getFirst)); int[] st = new int[n]; // 标记数组,记录每个节点是否已确定最短路径 int[] dist = new int[n]; // 最短路径数组 Arrays.fill(dist, INF); // 初始化最短路径为无穷大 dist[0] = 0; // 起点的最短路径为0 q.offer(new PII(0, 0)); // 将起点加入队列 while (!q.isEmpty()) { PII top = q.poll(); // 取出优先队列中的最小元素 int u = top.getSecond(); // 当前节点 if (st[u] == 1) continue; // 如果该节点已经确定最短路径,则跳过 st[u] = 1; // 标记该节点 for (PII p : g.get(u)) { // 遍历邻接节点 int v = p.getFirst(); // 邻接节点 int w = p.getSecond(); // 边的权重 if (dist[v] > dist[u] + w) { // 更新最短路径 dist[v] = dist[u] + w; q.offer(new PII(dist[v], v)); // 将更新后的节点加入队列 } } } long ans = 0; // 记录总的最短路程 while (T-- > 0) { // 读取每个需要送达旗帜的地点 int x = scanner.nextInt() - 1; ans += dist[x]; // 累加从起点到该地点的最短路径 } System.out.println(ans * 2); // 输出总的往返路程 } // 辅助类,存储节点及其对应的权重 static class PII { private final int first; private final int second; public PII(int first, int second) { this.first = first; this.second = second; } public int getFirst() { return first; } public int getSecond() { return second; } } }