3.25 图论

This commit is contained in:
zhangsan 2025-03-25 15:57:54 +08:00
parent 96a39cd0db
commit 5dfb8008da
7 changed files with 411 additions and 0 deletions

View File

@ -0,0 +1,83 @@
package graph;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
/**
* 题目 207. 课程表 (canFinish)
* 描述你这个学期必须选修 numCourses 门课程记为 0 numCourses - 1
* 在选修某些课程之前需要一些先修课程 先修课程按数组 prerequisites 给出其中 prerequisites[i] = [ai, bi] 表示如果要学习课程 ai 必须 先学习课程 bi
* 例如先修课程对 [0, 1] 表示想要学习课程 0 你需要先完成课程 1
* 请你判断是否可能完成所有课程的学习如果可以返回 true 否则返回 false
* 链接https://leetcode.cn/problems/course-schedule/
*/
//思路知道代码不好写
/*
思路解析
构建图与入度数组
用一个邻接表 graph 表示课程之间的依赖关系
对于先修课程对 [ai, bi]表示修课程 ai 之前必须完成课程 bi因此建立一条从 bi ai 的边并将 ai 的入度加 1
初始化队列
将所有入度为 0 的课程即没有先修要求的课程加入队列作为拓扑排序的起点
拓扑排序
从队列中取出一个课程将其计入已完成课程数量 count
遍历该课程的所有后续课程将这些课程的入度减 1如果某个课程的入度减为 0则说明它所有的先修课程都已完成将其加入队列
判断是否存在环
如果最终能处理的课程数量等于总课程数则说明不存在环路所有课程都可以按要求完成否则说明存在循环依赖无法完成所有课程
*/
public class CanFinish {
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 构建图graph.get(i) 存储课程 i 的后续课程
List<List<Integer>> graph = new ArrayList<>();
// 记录每个课程的入度即需要先修课程的数量
int[] indegree = new int[numCourses];
// 初始化图
for (int i = 0; i < numCourses; i++) {
graph.add(new ArrayList<>());
}
// 构建图和入度数组
// 每个先修对 [ai, bi] 表示要修 ai 必须先修 bi
// 因此 bi 指向 ai所以 indegree[ai]++
for (int[] pre : prerequisites) {
int course = pre[0];
int preCourse = pre[1];
graph.get(preCourse).add(course);
indegree[course]++;
}
// 使用队列存储所有入度为 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 cur = queue.poll();
count++;
// 遍历当前课程的所有后续课程将它们的入度减 1
for (int next : graph.get(cur)) {
indegree[next]--;
// 如果入度减到 0说明该课程的先修都已完成
if (indegree[next] == 0) {
queue.offer(next);
}
}
}
// 如果所有课程都能修完 count 应等于 numCourses
return count == numCourses;
}
}

View File

@ -0,0 +1,37 @@
package graph;
/**
* 题目 695. 岛屿的最大面积 (maxAreaOfIsland)
* 描述给你一个大小为 m x n 的二进制矩阵 grid
* 岛屿 是由一些相邻的 1 (代表土地) 构成的组合这里的相邻要求两个 1 必须在 水平或者竖直的四个方向上 相邻你可以假设 grid 的四个边缘都被 0代表水包围着
* 岛屿的面积是岛上值为 1 的单元格的数目
* 计算并返回 grid 中最大的岛屿面积如果没有岛屿则返回面积为 0
* 链接https://leetcode.cn/problems/max-area-of-island/
*/
public class MaxAreaOfIsland {
int dfs(int[][] grid,int i,int j){
if(i<0 || i>=grid.length || j<0 || j>=grid[0].length || grid[i][j]==0) {
return 0;
}
int area = 1;
grid[i][j]=0;
area+=dfs(grid,i,j-1);
area+=dfs(grid,i,j+1);
area+=dfs(grid,i+1,j);
area+=dfs(grid,i-1,j);
return area;
}
public int maxAreaOfIsland(int[][] grid) {
int maxx=0;
for (int i = 0; i < grid.length; i++) {
for (int j = 0; j < grid[0].length; j++) {
if(grid[i][j]==1) {
int area = dfs(grid, i, j);
maxx = Math.max(maxx, area);
}
}
}
return maxx;
}
}

View File

@ -0,0 +1,50 @@
package graph;
import java.util.Map;
/**
* 题目 200. 岛屿数量 (numIslands)
* 描述给你一个由 '1'陆地 '0'组成的的二维网格请你计算网格中岛屿的数量
* 岛屿总是被水包围并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成
* 此外你可以假设该网格的四条边均被水包围
* 链接https://leetcode.cn/problems/number-of-islands/
*/
public class NumIslands {
void dfs(char[][] grid, int r, int c) {
int nr = grid.length;
int nc = grid[0].length;
if (r < 0 || c < 0 || r >= nr || c >= nc || grid[r][c] == '0') {
return;
}
grid[r][c] = '0';
dfs(grid, r - 1, c);
dfs(grid, r + 1, c);
dfs(grid, r, c - 1);
dfs(grid, r, c + 1);
}
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
dfs(grid, r, c);
}
}
}
return num_islands;
}
}

View File

@ -0,0 +1,73 @@
package graph;
import java.util.LinkedList;
import java.util.Queue;
/**
* 题目 994. 腐烂的橘子 (orangesRotting)
* 描述在给定的 m x n 网格 grid 每个单元格可以有以下三个值之一
* 0 代表空单元格
* 1 代表新鲜橘子
* 2 代表腐烂的橘子
* 每分钟腐烂的橘子 周围 4 个方向上相邻 的新鲜橘子都会腐烂
* 返回 直到单元格中没有新鲜橘子为止所必须经过的最小分钟数如果不可能返回 -1
* 链接https://leetcode.cn/problems/rotting-oranges/
*/
//思想会代码写不利索
public class OrangesRotting {
public int orangesRotting(int[][] grid) {
int rows = grid.length;
int cols = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
int fresh = 0; // 记录新鲜橘子的数量
// 将所有腐烂橘子的位置入队同时统计新鲜橘子的个数
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
if (grid[i][j] == 2) {
queue.offer(new int[]{i, j});
} else if (grid[i][j] == 1) {
fresh++;
}
}
}
// 如果一开始没有新鲜橘子则不需要任何时间
if (fresh == 0) {
return 0;
}
int minutes = 0;
// 四个方向
int[][] directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
// BFS 开始每一层代表一分钟
while (!queue.isEmpty()) {
int size = queue.size();
boolean rotted = false; // 当前这一分钟是否有新鲜橘子被腐烂
for (int i = 0; i < size; i++) {
int[] cur = queue.poll();
for (int[] dir : directions) {
int x = cur[0] + dir[0];
int y = cur[1] + dir[1];
// 检查边界条件和是否为新鲜橘子
if (x >= 0 && x < rows && y >= 0 && y < cols && grid[x][y] == 1) {
// 腐烂新鲜橘子并入队后续继续扩散
grid[x][y] = 2;
fresh--;
queue.offer(new int[]{x, y});
rotted = true;
}
}
}
// 如果这一分钟有橘子腐烂则分钟数加1
if (rotted) {
minutes++;
}
}
// 如果还有新鲜橘子说明无法全部腐烂返回 -1
return fresh == 0 ? minutes : -1;
}
}

View File

@ -0,0 +1,58 @@
package graph;
/**
* 题目 208. 实现 Trie (前缀树)
* 描述Trie发音类似 "try"或者说 前缀树 是一种树形数据结构用于高效地存储和检索字符串数据集中的键这一数据结构有相当多的应用情景例如自动补全和拼写检查
* 请你实现 Trie
* Trie() 初始化前缀树对象
* void insert(String word) 向前缀树中插入字符串 word
* boolean search(String word) 如果字符串 word 在前缀树中返回 true在检索之前已经插入否则返回 false
* boolean startsWith(String prefix) 如果之前已经插入的字符串 word 的前缀之一为 prefix 返回 true 否则返回 false
* 链接https://leetcode.cn/problems/implement-trie-prefix-tree/description/
*/
//没搞懂 学习一下
class Trie {
private Trie[] children;
private boolean isEnd;
public Trie() {
children = new Trie[26]; //这行代码只是 创建了一个长度为 26 的数组对应 a ~ z但是数组里的元素初始都是 null只有当需要用到某个字符时才会创建对应的 Trie 节点
isEnd = false;
}
public void insert(String word) {
Trie node = this;
for (int i = 0; i < word.length(); i++) {
char ch = word.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
node.children[index] = new Trie();
}
node = node.children[index];
}
node.isEnd = true;
}
public boolean search(String word) {
Trie node = searchPrefix(word);
return node != null && node.isEnd;
}
public boolean startsWith(String prefix) {
return searchPrefix(prefix) != null;
}
private Trie searchPrefix(String prefix) {
Trie node = this;
for (int i = 0; i < prefix.length(); i++) {
char ch = prefix.charAt(i);
int index = ch - 'a';
if (node.children[index] == null) {
return null;
}
node = node.children[index];
}
return node;
}
}

View File

@ -0,0 +1,47 @@
package tree;
import java.util.HashMap;
import java.util.HashSet;
/**
* 题目 236. 二叉树的最近公共祖先 (lowestCommonAncestor)
* 描述给定一个二叉树, 找到该树中两个指定节点的最近公共祖先
* 百度百科中最近公共祖先的定义为对于有根树 T 的两个节点 pq最近公共祖先表示为一个节点 x满足 x pq 的祖先且 x 的深度尽可能大一个节点也可以是它自己的祖先
* 链接https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/
*/
public class LowestCommonAncestor {
void helper(HashMap<TreeNode, TreeNode> map, TreeNode father, TreeNode cur) {
if (cur == null)
return;
map.put(cur, father);
helper(map, cur, cur.left);
helper(map, cur, cur.right);
}
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
HashMap<TreeNode, TreeNode> map = new HashMap<>();
HashSet<TreeNode> set = new HashSet<>();
map.put(root, null);
helper(map, root, root.left);
helper(map, root, root.right);
// p 以及其所有祖先加入 set
TreeNode tp = p;
while (tp != null) {
set.add(tp);
tp = map.get(tp);
}
// q 开始找到第一个出现在 p 祖先链中的节点
tp = q;
while (tp != null) {
if (set.contains(tp))
return tp;
tp = map.get(tp);
}
return null; // 理论上不可能执行到这里因为肯定存在公共祖先
}
}

View File

@ -0,0 +1,63 @@
package tree;
import java.util.HashMap;
import java.util.Map;
/**
* 题目 437. 路径总和 III (pathSum)
* 描述给定一个二叉树的根节点 root 和一个整数 targetSum 求该二叉树里节点值之和等于 targetSum 路径 的数目
* 路径 不需要从根节点开始也不需要在叶子节点结束但是路径方向必须是向下的只能从父节点到子节点
* 链接https://leetcode.cn/problems/path-sum-iii/
*/
//前缀和想到了代码不会写
public class PathSum {
// 主函数用于计算路径和等于 targetSum 的路径个数
public int pathSum(TreeNode root, int targetSum) {
// 使用一个哈希表来记录前缀和出现的次数
// key从根节点到当前节点的累计和前缀和
// value该前缀和出现的次数
Map<Long, Integer> prefix = new HashMap<Long, Integer>();
// 初始化前缀和为 0 的情况出现 1 方便处理从根节点开始的情况
prefix.put(0L, 1);
// 从根节点开始递归查找满足条件的路径
return dfs(root, prefix, 0, targetSum);
}
// dfs函数递归遍历树并统计满足路径和为 targetSum 的路径个数
// 参数说明
// root当前节点
// prefix记录前缀和及其出现次数的哈希表
// curr当前节点的累计前缀和从根节点到当前节点的和
// targetSum目标路径和
public int dfs(TreeNode root, Map<Long, Integer> prefix, long curr, int targetSum) {
// 终止条件当前节点为空返回0条路径
if (root == null) {
return 0;
}
int ret = 0;
// 更新当前累计前缀和加上当前节点的值
curr += root.val;
// 检查是否存在某个前缀和使得
// 当前累计和 - 该前缀和 = targetSum
// 从那个节点到当前节点的路径和等于 targetSum
ret = prefix.getOrDefault(curr - targetSum, 0);
// 更新哈希表将当前前缀和的出现次数加1
prefix.put(curr, prefix.getOrDefault(curr, 0) + 1);
// 递归遍历左子树和右子树
ret += dfs(root.left, prefix, curr, targetSum);
ret += dfs(root.right, prefix, curr, targetSum);
// 回溯操作在返回父节点之前去掉当前节点对前缀和的贡献
// 防止当前路径的前缀和影响到其他分支的计算
prefix.put(curr, prefix.getOrDefault(curr, 0) - 1);
return ret;
}
}