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和边数mn = 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,边的权重为wint u = 1, v = 2, w = 3;// 如果哈希表中不包含键u,初始化其值为一个新的ArrayListif (!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 拓扑排序
用于判断有向图中是否存在环
LeetCode 207. 课程表
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]的入度}// 初始化队列用于BFSQueue<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号节点,设置为-1ArrayList<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(); // 读取依赖的模块IDif(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); // 存在循环依赖,输出-1return;}}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]--; // 将后继任务的入度减1if (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。
样例:
输入:
552 21 0 2 1 02 2 1 2 00 0 1 0 02 1 1 0 01 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[]>() {@Overridepublic 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]);}}}// 如果有镜子不能接收到光芒,则返回-1if (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));}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;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;}} }