图论
1.导论
- 图:有向图和无向图
- 解题步骤:
(1)建图:建什么样的图,怎么建图
(2)然后具体问题具体分析,比如使用BFS、贪心算法等对图进行操作
- 并查集
//并查集(管理一系列不想交的集合),具有查询和合并的功能
class DisjointSetUnion {
int[] f;//存储每个元素的根节点
int[] rank;//记录每个根节点对应树的深度
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.rank = new int[n];//记录每个根节点对应树的深度
this.f = new int[n];//存储每个元素的父节点
for (int i = 0; i < n; i++) {
this.rank[i] = 1;//初始值设为1
this.f[i] = i;//一开始将父节点设为自己
}
}
//压缩路径查询方法,递归实现
//一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
//合并方法
public boolean unionSet(int x, int y) {
int fx = find(x), fy = find(y);//找到两个结点的根节点
if (fx == fy) {//两个结点的根节点相同不用合并
return false;
}
if (rank[fx] < rank[fy]) {//以fx为根节点的树的深度小于以fy为根节点的树的深度
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
2.编程题
2.1 127. 单词接龙
字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:
序列中第一个单词是 beginWord 。
序列中最后一个单词是 endWord 。
每次转换只能改变一个字母。
转换过程中的中间单词必须是字典 wordList 中的单词。
给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。
1 <= beginWord.length <= 10
endWord.length == beginWord.length
1 <= wordList.length <= 5000
wordList[i].length == beginWord.length
beginWord、endWord 和 wordList[i] 由小写英文字母组成
beginWord != endWord
wordList 中的所有字符串 互不相同
- 创建图
/*
1.给每个单词分配一个Id,使用hashmap来存储
2.建图
3.广度优先搜索
*/
class Solution {
Map<String,Integer> wordId = new HashMap<>();//保存单词的Id
List<List<Integer>> edge = new ArrayList<>();//保存图的边
int nodeNum = 0;//单词id
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
//创建图
for(String word : wordList){
addEdge(word);
}
addEdge(beginWord);
//不存在
if(!wordId.containsKey(endWord)) return 0;
int[] dis = new int[nodeNum];//起始点到每个结点的距离
Arrays.fill(dis,Integer.MAX_VALUE);//初始化为最大值,搜索的时候如果当前值为最大值,说明此结点还没有访问过
int beginId = wordId.get(beginWord);
int endId = wordId.get(endWord);
dis[beginId] = 0;//起始点的距离为0
//广度优先搜索
Queue<Integer> que = new LinkedList<>();
que.offer(beginId);
while(!que.isEmpty()){
int cur = que.poll();
if(cur == endId){
return dis[endId] / 2 + 1;
/*
因为添加了虚拟节点,所以我们得到的距离为实际最短路径长度的两倍。
同时我们并未计算起点对答案的贡献,所以我们应当返回距离的一半再加一的结果。
*/
}
for(int curNext : edge.get(cur)){
if(dis[curNext] == Integer.MAX_VALUE){
dis[curNext] = dis[cur] + 1;
que.offer(curNext);
}
}
}
return 0;
}
//创建边
//比如hot 连接 : *ot、h*t、ho*
private void addEdge(String word){
addWord(word);
int id1 = wordId.get(word);
char[] array = word.toCharArray();
int length = array.length;
for(int i=0;i<length;++i){
char temp = array[i];
array[i] = '*';
String newWord = new String(array);
addWord(newWord);
int id2 = wordId.get(newWord);
edge.get(id1).add(id2);
edge.get(id2).add(id1);
array[i] = temp;
}
}
//添加单词id
private void addWord(String word){
if(!wordId.containsKey(word)){
wordId.put(word,nodeNum++);
edge.add(new ArrayList<Integer>());
}
}
}
- 优化:双向广度优先搜索
class Solution {
Map<String, Integer> wordId = new HashMap<String, Integer>();
List<List<Integer>> edge = new ArrayList<List<Integer>>();
int nodeNum = 0;
public int ladderLength(String beginWord, String endWord, List<String> wordList) {
for (String word : wordList) {
addEdge(word);
}
addEdge(beginWord);
if (!wordId.containsKey(endWord)) {
return 0;
}
int[] disBegin = new int[nodeNum];
Arrays.fill(disBegin, Integer.MAX_VALUE);
int beginId = wordId.get(beginWord);
disBegin[beginId] = 0;
Queue<Integer> queBegin = new LinkedList<Integer>();
queBegin.offer(beginId);
int[] disEnd = new int[nodeNum];
Arrays.fill(disEnd, Integer.MAX_VALUE);
int endId = wordId.get(endWord);
disEnd[endId] = 0;
Queue<Integer> queEnd = new LinkedList<Integer>();
queEnd.offer(endId);
while (!queBegin.isEmpty() && !queEnd.isEmpty()) {
int queBeginSize = queBegin.size();
for (int i = 0; i < queBeginSize; ++i) {
int nodeBegin = queBegin.poll();
if (disEnd[nodeBegin] != Integer.MAX_VALUE) {
return (disBegin[nodeBegin] + disEnd[nodeBegin]) / 2 + 1;
}
for (int it : edge.get(nodeBegin)) {
if (disBegin[it] == Integer.MAX_VALUE) {
disBegin[it] = disBegin[nodeBegin] + 1;
queBegin.offer(it);
}
}
}
int queEndSize = queEnd.size();
for (int i = 0; i < queEndSize; ++i) {
int nodeEnd = queEnd.poll();
if (disBegin[nodeEnd] != Integer.MAX_VALUE) {
return (disBegin[nodeEnd] + disEnd[nodeEnd]) / 2 + 1;
}
for (int it : edge.get(nodeEnd)) {
if (disEnd[it] == Integer.MAX_VALUE) {
disEnd[it] = disEnd[nodeEnd] + 1;
queEnd.offer(it);
}
}
}
}
return 0;
}
public void addEdge(String word) {
addWord(word);
int id1 = wordId.get(word);
char[] array = word.toCharArray();
int length = array.length;
for (int i = 0; i < length; ++i) {
char tmp = array[i];
array[i] = '*';
String newWord = new String(array);
addWord(newWord);
int id2 = wordId.get(newWord);
edge.get(id1).add(id2);
edge.get(id2).add(id1);
array[i] = tmp;
}
}
public void addWord(String word) {
if (!wordId.containsKey(word)) {
wordId.put(word, nodeNum++);
edge.add(new ArrayList<Integer>());
}
}
}
2.2 207. 课程表
你这个学期必须选修 numCourses 门课程,记为 0 到 numCourses - 1 。
在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites 给出,其中 prerequisites[i] = [ai, bi] ,表示如果要学习课程 ai 则 必须 先学习课程 bi 。
例如,先修课程对 [0, 1] 表示:想要学习课程 0 ,你需要先完成课程 1 。
请你判断是否可能完成所有课程的学习?如果可以,返回 true ;否则,返回 false 。
1 <= numCourses <= 105
0 <= prerequisites.length <= 5000
prerequisites[i].length == 2
0 <= ai, bi < numCourses
prerequisites[i] 中的所有课程对 互不相同
//拓扑排序
class Solution {
List<List<Integer>> edges = new ArrayList<>();//存储有向图
int[] indeg;//存储当前结点的入度值,即当前结点父节点的个数
public boolean canFinish(int numCourses, int[][] prerequisites) {
// 建图
// 共创建了与课程个数相等的列表数
// 每个列表的下标对应的就是每个课程
// 每个列表里面存储的值,就是该课程指向的下一个课程
for (int i = 0; i< numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
// 创建入度表
// 数组下标对应的是相应的课程,里面的值是该课程的入度值
indeg = new int[numCourses];
for (int[] prerequisite : prerequisites) {
edges.get(prerequisite[1]).add(prerequisite[0]);
++indeg[prerequisite[0]];
}
// 广度优先搜索
Queue<Integer> que = new LinkedList<>();
// 入度值为0的结点先入队列,也就是学习了这些课程才能学习接下来的课程
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0){
que.offer(i);
}
}
int visited = 0;//记录已访问结点的个数
while (!que.isEmpty()) {
++visited;
int course = que.poll();//获取入度值为0的结点
for (int courseNext : edges.get(course)) {
--indeg[courseNext];
if (indeg[courseNext] == 0){//入度值为0加入队列
que.offer(courseNext);
}
}
}
//如果访问结点的个数等与课程数,说明能够修完所有课程
return visited == numCourses;
}
}
2.3 210. 课程表 II
现在你总共有 numCourses 门课需要选,记为 0 到 numCourses - 1。给你一个数组 prerequisites ,其中 prerequisites[i] = [ai, bi] ,表示在选修课程 ai 前 必须 先选修 bi 。
例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示:[0,1] 。
返回你为了学完所有课程所安排的学习顺序。可能会有多个正确的顺序,你只要返回 任意一种 就可以了。如果不可能完成所有课程,返回 一个空数组 。
1 <= numCourses <= 2000
0 <= prerequisites.length <= numCourses * (numCourses - 1)
prerequisites[i].length == 2
0 <= ai, bi < numCourses
ai != bi
所有[ai, bi] 互不相同
//拓扑排序
class Solution {
List<List<Integer>> edges = new ArrayList<>();//存储有向图
int[] indeg;//存储当前结点的入度值,即当前结点父节点的数量
public int[] findOrder(int numCourses, int[][] prerequisites) {
// 建图
// 共创建了与课程个数相等的列表数
// 每个列表的下标对应的就是每个课程
// 每个列表里面存储的值,就是该课程指向的下一个课程
for (int i = 0; i< numCourses; ++i) {
edges.add(new ArrayList<Integer>());
}
// 创建入度表
// 数组下标对应的是相应的课程,里面的值是该课程的入度值
indeg = new int[numCourses];
for (int[] prerequisite : prerequisites) {
edges.get(prerequisite[1]).add(prerequisite[0]);
++indeg[prerequisite[0]];
}
// 广度优先搜索
Queue<Integer> que = new LinkedList<>();
// 入度值为0的结点先入队列,也就是学习了这些课程才能学习接下来的课程
for (int i = 0; i < numCourses; ++i) {
if (indeg[i] == 0){
que.offer(i);
}
}
int[] ans = new int[numCourses];
int index = 0;
while (!que.isEmpty()) {
int course = que.poll();//获取入度值为0的结点
ans[index++] = course;
for (int courseNext : edges.get(course)) {
--indeg[courseNext];
if (indeg[courseNext] == 0){//入度值为0加入队列
que.offer(courseNext);
}
}
}
if (index != numCourses) {//说明不能学完所有课程
return new int[0];
}
return ans;
}
}
2.4 399. 除法求值
给你一个变量对数组 equations 和一个实数值数组 values 作为已知条件,其中 equations[i] = [Ai, Bi] 和 values[i] 共同表示等式 Ai / Bi = values[i] 。每个 Ai 或 Bi 是一个表示单个变量的字符串。
另有一些以数组 queries 表示的问题,其中 queries[j] = [Cj, Dj] 表示第 j 个问题,请你根据已知条件找出 Cj / Dj = ? 的结果作为答案。
返回 所有问题的答案 。如果存在某个无法确定的答案,则用 -1.0 替代这个答案。如果问题中出现了给定的已知条件中没有出现的字符串,也需要用 -1.0 替代这个答案。
注意:输入总是有效的。你可以假设除法运算中不会出现除数为 0 的情况,且不存在任何矛盾的结
1 <= equations.length <= 20
equations[i].length == 2
1 <= Ai.length, Bi.length <= 5
values.length == equations.length
0.0 < values[i] <= 20.0
1 <= queries.length <= 20
queries[i].length == 2
1 <= Cj.length, Dj.length <= 5
Ai, Bi, Cj, Dj 由小写英文字母与数字组成
/*
1.字符映射为整数
2.建图(无向图)
3.遍历问题,对每个问题进行广度优先搜索
*/
class Solution {
public double[] calcEquation(List<List<String>> equations, double[] values, List<List<String>> queries) {
int nvars = 0;//字符映射为的整数
Map<String,Integer> variables = new HashMap<>();
int n = equations.size();
//将equations数组中的不同的字符串映射为整数
for (int i = 0; i < n; ++i) {
if (!variables.containsKey(equations.get(i).get(0))) {
variables.put(equations.get(i).get(0),nvars++);
}
if (!variables.containsKey(equations.get(i).get(1))) {
variables.put(equations.get(i).get(1),nvars++);
}
}
//建图
List<List<Pair>> edges = new ArrayList<>();//对于每个点,存储其直接连接到的所有点及对应的权值
for (int i = 0; i < nvars; ++i) {
edges.add(new ArrayList<Pair>());
}
for ( int i = 0; i < n; ++i) {
int va = variables.get(equations.get(i).get(0));//被除数所映射的整数
int vb = variables.get(equations.get(i).get(1));//除数所映射的整数
edges.get(va).add(new Pair(vb,values[i]));
edges.get(vb).add(new Pair(va,1.0/values[i]));
}
int queriesCount = queries.size();
double[] ret = new double[queriesCount];//存储结果
//遍历问题进行求解
for (int i = 0; i < queriesCount; ++i) {
List<String> query = queries.get(i);
double result = -1.0;
if (variables.containsKey(query.get(0)) && variables.containsKey(query.get(1))) {//如果问题中的字符存在于map中
int ia = variables.get(query.get(0));
int ib = variables.get(query.get(1));
if (ia == ib) {//如果被除数和除数相等,商为1.0
result = 1.0;
} else { // 不相等
//广度优先搜索
Queue<Integer> points = new LinkedList<>();
points.offer(ia);
double[] ratios = new double[nvars];//存储ia/i的值
Arrays.fill(ratios,-1.0);//初始化为-1.0,因为有些答案无法确定
ratios[ia] = 1.0;// ia/ia = 1.0
while (!points.isEmpty() && ratios[ib] < 0) {
int x = points.poll();
for (Pair pair : edges.get(x)) {
int y = pair.index;
double val = pair.value;// 权重,即x/y的值
if (ratios[y] < 0) {
ratios[y] = ratios[x] * val;// 即 ia/y = (ia/x) * (x/y);
points.offer(y);
}
}
}
result = ratios[ib];
}
}
ret[i] = result;
}
return ret;
}
}
//带权值结点
class Pair {
int index;//当前结点的索引
double value;//权重:父节点索引与当前结点的索引的比值
Pair(int index,double value) {
this.index = index;
this.value = value;
}
}
2.5 1584. 连接所有点的最小费用
给你一个points 数组,表示 2D 平面上的一些点,其中 points[i] = [xi, yi] 。
连接点 [xi, yi] 和点 [xj, yj] 的费用为它们之间的 曼哈顿距离 :|xi - xj| + |yi - yj| ,其中 |val| 表示 val 的绝对值。
请你返回将所有点连接的最小总费用。只有任意两点之间 有且仅有 一条简单路径时,才认为所有点都已连接。
1 <= points.length <= 1000
-106 <= xi, yi <= 106
所有点 (xi, yi) 两两不同。
//Kruskal算法
class Solution {
public int minCostConnectPoints(int[][] points) {
int n = points.length;
DisjointSetUnion dsu = new DisjointSetUnion(n);
//建图,所有的点进行连接
List<Edge> edges = new ArrayList<Edge>();
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
edges.add(new Edge(dist(points, i, j), i, j));
}
}
//按照两点之间的距离,从小到大排序,贪心算法:每次选最小边加入
Collections.sort(edges, new Comparator<Edge>() {
public int compare(Edge edge1, Edge edge2) {
return edge1.len - edge2.len;
}
});
int ret = 0, num = 1;
for (Edge edge : edges) {
int len = edge.len, x = edge.x, y = edge.y;
if (dsu.unionSet(x, y)) {
ret += len;
num++;
if (num == n) {
break;
}
}
}
return ret;
}
//计算两点间的距离
public int dist(int[][] points, int x, int y) {
return Math.abs(points[x][0] - points[y][0]) + Math.abs(points[x][1] - points[y][1]);
}
}
//并查集(管理一系列不想交的集合),具有查询和合并的功能
class DisjointSetUnion {
int[] f;//存储每个元素的父节点
int[] rank;//记录每个根节点对应树的深度
int n;
public DisjointSetUnion(int n) {
this.n = n;
this.rank = new int[n];//记录每个根节点对应树的深度
this.f = new int[n];//存储每个元素的父节点
for (int i = 0; i < n; i++) {
this.rank[i] = 1;//初始值设为1
this.f[i] = i;//一开始将父节点设为自己
}
}
//压缩路径查询方法,递归实现
//一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
//合并方法
public boolean unionSet(int x, int y) {
int fx = find(x), fy = find(y);//找到两个结点的根节点
if (fx == fy) {//两个结点的根节点相同不用合并
return false;
}
if (rank[fx] < rank[fy]) {//以fx为根节点的树的深度小于以fy为根节点的树的深度
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
}
//边的类
class Edge {
//len表示x和y两点间的距离
int len, x, y;
public Edge(int len, int x, int y) {
this.len = len;
this.x = x;
this.y = y;
}
}
2.6 1631. 最小体力消耗路径
你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。
rows == heights.length
columns == heights[i].length
1 <= rows, columns <= 100
1 <= heights[i][j] <= 106
class Solution {
public int minimumEffortPath(int[][] heights) {
int m = heights.length;
int n = heights[0].length;
// 建图
// 数组中的值为{当前结点头顶/左边结点的编号,当前结点编号,权值}
List<int[]> edges = new ArrayList<>();
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
int id = i * n + j;//对每个点进行编号
if (i > 0) {
edges.add(new int[]{id-n, id, Math.abs(heights[i][j] - heights[i-1][j])});
}
if (j > 0) {
edges.add(new int[]{id-1, id, Math.abs(heights[i][j] - heights[i][j-1])});
}
}
}
//按照权值进行从小到大进行排序
Collections.sort(edges, (edge1,edge2) -> edge1[2] - edge2[2]);
DisjointSetUnion dis = new DisjointSetUnion(m*n);
int ans= 0;//记录最小体力消耗值
for (int[] edge : edges) {
int x = edge[0];
int y = edge[1];
int v = edge[2];
dis.unionSet(x,y);
// 当加入一条权值为v的边后,如果左上角和右下角从非连通状态变为连通状态,那么v即为答案
if(dis.connected(0,m*n-1)){
ans = v;
break;
}
}
return ans;
}
}
// 并查集
class DisjointSetUnion {
int[] f;
int[] rank;
int n;
public DisjointSetUnion(int n) {
this.n = n;
f = new int[n];
rank = new int[n];
for (int i = 0; i < n; ++i) {
f[i] = i;
rank[i] = 1;
}
}
public int find(int x) {
return f[x] == x ? x : (f[x] = find(f[x]));
}
public boolean unionSet(int x,int y) {
int fx = find(x);
int fy = find(y);
if(fx == fy) return false;
if (rank[fx] < rank[fy]) {
f[fx] = fy;
} else if (rank[fx] > rank[fy]) {
f[fy] = fx;
} else {
f[fx] = fy;
rank[fy] += 1;
}
return true;
}
// 判断两个点是否连接
public boolean connected (int x,int y) {
x = find(x);
y = find(y);
return x == y;
}
}
2.7 2101. 引爆最多的炸弹
给你一个炸弹列表。一个炸弹的 爆炸范围 定义为以炸弹为圆心的一个圆。
炸弹用一个下标从 0 开始的二维整数数组 bombs 表示,其中 bombs[i] = [xi, yi, ri] 。xi 和 yi 表示第 i 个炸弹的 X 和 Y 坐标,ri 表示爆炸范围的 半径 。
你需要选择引爆 一个 炸弹。当这个炸弹被引爆时,所有 在它爆炸范围内的炸弹都会被引爆,这些炸弹会进一步将它们爆炸范围内的其他炸弹引爆。
给你数组 bombs ,请你返回在引爆 一个 炸弹的前提下,最多 能引爆的炸弹数目。
1 <= bombs.length <= 100
bombs[i].length == 3
1 <= xi, yi, ri <= 105
class Solution {
List<List<Integer>> edges;
int n;
public int maximumDetonation(int[][] bombs) {
n = bombs.length;
//根据引爆关系建图
edges = new ArrayList<>();
for (int i = 0; i < n; ++i) {
edges.add(new ArrayList<Integer>());
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (i != j && isConnected(i, j, bombs)) {
edges.get(i).add(j);
}
}
}
//逐个搜索,求出最大值
int ans = 0;
for (int i = 0; i < n; ++i) {
ans = Math.max(ans, bfs(i));
}
return ans;
}
//广度优先搜索,计算该炸弹可以引爆的数目
public int bfs(int i) {
int count = 1;
boolean[] visited = new boolean[n];
Queue<Integer> queue = new LinkedList<>();
queue.offer(i);
visited[i] = true;
while (!queue.isEmpty()) {
int index = queue.poll();
if(edges.get(index).size() != 0) {
for (int bomb : edges.get(index)) {
if (!visited[bomb]) {
++count;
queue.offer(bomb);
visited[bomb] = true;
}
}
}
}
return count;
}
//判断是否连接
public boolean isConnected(int i, int j, int[][] bombs) {
long dx = bombs[i][0] - bombs[j][0];
long dy = bombs[i][1] - bombs[j][1];
return (long)bombs[i][2] * bombs[i][2] >= dx * dx + dy * dy;
}
}
// 使用Map建图,加快索引
class Solution {
Map<Integer,List<Integer>> edges = new HashMap<>();
int n;
public int maximumDetonation(int[][] bombs) {
n = bombs.length;
//根据引爆关系建图
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (i != j && isConnected(i, j, bombs)) {
List<Integer> edge = edges.getOrDefault(i,new ArrayList<Integer>());
edge.add(j);
edges.put(i,edge);
}
}
}
//逐个搜索,求出最大值
int ans = 0;
for (int i = 0; i < n; ++i) {
ans = Math.max(ans, bfs(i));
}
return ans;
}
//广度优先搜索,计算该炸弹可以引爆的数目
public int bfs(int i) {
int count = 1;
boolean[] visited = new boolean[n];
Queue<Integer> queue = new LinkedList<>();
queue.offer(i);
visited[i] = true;
while (!queue.isEmpty()) {
int index = queue.poll();
if(edges.getOrDefault(index,new ArrayList<Integer>()).size() != 0) {
for (int bomb : edges.get(index)) {
if (!visited[bomb]) {
++count;
queue.offer(bomb);
visited[bomb] = true;
}
}
}
}
return count;
}
//判断是否连接
public boolean isConnected(int i, int j, int[][] bombs) {
long dx = bombs[i][0] - bombs[j][0];
long dy = bombs[i][1] - bombs[j][1];
return (long)bombs[i][2] * bombs[i][2] >= dx * dx + dy * dy;
}
}
2.8 DD3 地下迷宫(牛客网)
小青蛙有一天不小心落入了一个地下迷宫,小青蛙希望用自己仅剩的体力值P跳出这个地下迷宫。为了让问题简单,假设这是一个n*m的格子迷宫,迷宫每个位置为0或者1,0代表这个位置有障碍物,小青蛙达到不了这个位置;1代表小青蛙可以达到的位置。小青蛙初始在(0,0)位置,地下迷宫的出口在(0,m-1)(保证这两个位置都是1,并且保证一定有起点到终点可达的路径),小青蛙在迷宫中水平移动一个单位距离需要消耗1点体力值,向上爬一个单位距离需要消耗3个单位的体力值,向下移动不消耗体力值,当小青蛙的体力值等于0的时候还没有到达出口,小青蛙将无法逃离迷宫。现在需要你帮助小青蛙计算出能否用仅剩的体力值跳出迷宫(即达到(0,m-1)位置)。
输入描述:
输入包括n+1行:
第一行为三个整数n,m(3 <= m,n <= 10),P(1 <= P <= 100)
接下来的n行:
每行m个0或者1,以空格分隔
输出描述:
如果能逃离迷宫,则输出一行体力消耗最小的路径,输出格式见样例所示;如果不能逃离迷宫,则输出"Can not escape!"。 测试数据保证答案唯一
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n = sc.nextInt();
int m = sc.nextInt();
int P = sc.nextInt();
int[][] grid = new int[n][m];
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
grid[i][j] = sc.nextInt();
}
}
// 构建邻接矩阵
int[][] graph = new int[n*m][n*m];
// 初始化:最初默认为不连通,都设置为最大值
for(int i = 0; i < n*m; ++i) {
Arrays.fill(graph[i],Integer.MAX_VALUE);
}
for (int i = 0; i < n; ++i) {
for (int j = 0; j < m; ++j) {
if (i > 0 && grid[i-1][j] == 1) {
graph[i * m + j][(i - 1) * m + j] = 3;
}
if (i < n - 1 && grid[i + 1][j] == 1) {
graph[i * m + j][(i + 1) * m + j] = 0;
}
if (j > 0 && grid[i][j - 1] == 1) {
graph[i * m + j][i * m + j - 1] = 1;
}
if (j < m - 1 && grid[i][j + 1] == 1) {
graph[i * m + j][i * m + j + 1] = 1;
}
}
}
int[] prenode = new int[n*m]; // 前驱结点
int[] mindist = new int[n*m]; // 最短路径
boolean[] found = new boolean[n*m];// 该节点是否已经找到最短路径
// 初始化
for (int i = 0; i < n*m; ++i) {
prenode[i] = 0;
mindist[i] = graph[0][i];
}
found[0] = true;
for (int i = 1; i < n*m; ++i) {
int near = 0;// 用来保存最近的结点位置
int min = Integer.MAX_VALUE;// 用来保存最近的距离
for (int j = 0; j < n*m; ++j) {
if (!found[j] && mindist[j] < min) {
min = mindist[j];
near = j;
}
}
if (min == Integer.MAX_VALUE) {// 找不到连通路径
break;
}
found[near] = true;
// 根据找到最近结点near 修正前驱结点和最短距离
for (int j = 0; j < n*m; ++j) {
if (!found[j] && graph[near][j] != Integer.MAX_VALUE && (min + graph[near][j] < mindist[j])) {
prenode[j] = near;
mindist[j] = min + graph[near][j];
}
}
}
// 输出(0,0)到(0,m-1)的路径
if (mindist[m - 1] > P) {
System.out.println("Can not escape!");
}else {
Stack<int[]> path = new Stack<>();
for (int i = m-1; i != 0; i = prenode[i]) {
path.push(new int[]{i/m,i%m});
}
System.out.print("[0,0]");
while (!path.isEmpty()) {
int[] t = path.pop();;
System.out.print(",[" + t[0] + "," +t[1] + "]");
}
}
}
}