图论算法三
一、并查集算法的应用
再来回顾下并查集算法的模板:
class UF {
// 连通分量个数
int count;
// 存储若干棵树
int[] parent;
// 记录每棵树的重量
int[] size;
// 构造函数
public UF(int n) {
this.count = n;
parent = new int[n];
size = new int[n];
// 图中的每个节点指向自己
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
// 找 x节点的根节点
public int find(int x) {
// 用路径压缩
while (parent[x] != x) {
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
// 连通p、q节点
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
// 根节点相同,说明本来就是连通的
if (rootP == rootQ) {
return;
}
// 小树接在大树下,更平衡
if (size[rootP] > size[rootQ]) {
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
} else {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}
// 连通分量个数--
count--;
}
// 判断 p 和 q 是否连通
public boolean connected(int p, int q) {
return find(p) == find(q);
}
// 返回连通分量个数
public int count() {
return count;
}
}
算法的关键点有 3 个:
1、用 parent 数组记录每个节点的父节点,相当于指向父节点的指针,所以 parent 数组内实际存储着一个森林(若干棵多叉树)。
2、用 size 数组记录着每棵树的重量,目的是让 union 后树依然拥有平衡性,而不会退化成链表,影响操作效率。
3、在 find 函数中进行路径压缩,保证任意树的高度保持在常数,使得 union 和 connected API 时间复杂度为 O(1)。
别忘了路径压缩,小树接在大树后面,更平衡。
等式方程的可满足性(中等)
前文说过,动态连通性其实就是一种等价关系,具有「自反性」「传递性」和「对称性」,其实 == 关系也是一种等价关系,具有这些性质。所以这个问题用 Union-Find 算法就很自然。
核心思想是,将 equations 中的算式根据 == 和 != 分成两部分,先处理 == 算式,使得他们通过相等关系各自勾结成门派(连通分量);然后处理 != 算式,检查不等关系是否破坏了相等关系的连通性。
class Solution {
public boolean equationsPossible(String[] equations) {
// 26个英文字母
UF uf = new UF(26);
// 先让相等的字母形成连通分量 ==
for (String eq : equations) {
if (eq.charAt(1) == '=') {
char x = eq.charAt(0);
char y = eq.charAt(3);
uf.union(x - 'a', y - 'a');
}
}
// check不等关系是否会打破相等关系的连通性
// 如果没有打破,说明这个不等关系是不会影响其它等式的判断
// a != b,打破就是说 a 与 b 原来是连通的,现在不等式又连通了,那肯定false
for (String eq : equations) {
if (eq.charAt(1) == '!') {
char x = eq.charAt(0);
char y = eq.charAt(3);
// 如果相等关系成立(也就是之前已经连通了),则逻辑冲突
if (uf.connected(x - 'a', y - 'a')) return false;
}
}
return true;
}
}
class UF {
// 连通分量个数
int count;
// 每棵树
int[] parent;
// 每棵树的节点数
int[] size;
UF(int n) {
this.count = n;
this.parent = new int[n];
this.size = new int[n];
// 图中的每个节点指向自己
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
// 找根节点
public int find(int x) {
while (parent[x] != x) {
// 路径压缩
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
// 判断是否连通
public boolean connected(int p, int q) {
return find(p) == find(q);
}
// 连通两个节点
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
// 本来就联通
if (rootQ == rootP) return;
if (size[rootP] > size[rootQ]) {
// 小树接在大树后
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
} else {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}
// 连通分量--
count--;
}
// 返回连通分量个数
public int count() {
return count;
}
}
一定要学会把问题转换为并查集的思路,这样能够帮助解决很多问题。
二、Kruskal最小生成树
如果一幅图没有环,完全可以拉伸成一棵树的模样。说的专业一点,树就是「无环连通图」。
那么什么是图的「生成树」呢,其实按字面意思也好理解,就是在图中找一棵包含图中的所有节点的树。专业点说,生成树是含有图中所有顶点的「无环连通子图」。
容易想到,一幅图可以有很多不同的生成树,比如下面这幅图,红色的边就组成了两棵不同的生成树
对于加权图,每条边都有权重,所以每棵生成树都有一个权重和。比如上图,右侧生成树的权重和显然比左侧生成树的权重和要小。
那么最小生成树很好理解了,所有可能的生成树中,权重和最小的那棵生成树就叫「最小生成树」。
这里需要使用Union-Find 并查集算法,来保证图中生成的是树(不包环)。
并查集算法是如何做到的?先来看看这道题:
给你输入编号从 0 到 n - 1 的 n 个结点,和一个无向边列表 edges(每条边用节点二元组表示),请你判断输入的这些边组成的结构是否是一棵树。
这些边构成的是一颗树,应该返回true:
对于这道题,我们可以思考一下,什么情况下加入一条边会使得树变成图(出现环)?
显然,像下面这样添加边会出现环:
而下面这样添加就不会出现环:
而判断两个节点是否连通(是否在同一个连通分量中)就是 Union-Find 算法的拿手绝活,所以这道题的解法代码如下:
// 判断输入的若干条边是否能构造出一棵树结构
boolean validTree(int n, int[][] edges) {
// 初始化 0...n-1 共 n 个节点
UF uf = new UF(n);
// 遍历所有边,将组成边的两个节点进行连接
for (int[] edge : edges) {
int u = edge[0];
int v = edge[1];
// 若两个节点已经在同一连通分量中,会产生环
if (uf.connected(u, v)) {
return false;
}
// 这条边不会产生环,可以是树的一部分
uf.union(u, v);
}
// 要保证最后只形成了一棵树,即只有一个连通分量
return uf.count() == 1;
}
class UF {
// 见上文代码实现
}
Kruskal 算法
所谓最小生成树,就是图中若干边的集合(我们后文称这个集合为 mst,最小生成树的英文缩写),你要保证这些边:
1、包含图中的所有节点,就是整个图的连通分量 = 1(全部节点连起来了)
2、形成的结构是树结构(即不存在环),就是用并查集的connected函数查看是否连通(相同根节点)。
3、权重和最小。
有之前题目的铺垫,前两条其实可以很容易地利用 Union-Find 算法做到,关键在于第 3 点,如何保证得到的这棵生成树是权重和最小的?
这里就用到了贪心思路:
将所有边按照权重从小到大排序,从权重最小的边开始遍历,如果这条边和mst中的其它边不会形成环,则这条边是最小生成树的一部分,将它加入mst集合;否则,这条边不是最小生成树的一部分,不要把它加入mst集合。
最低成本联通所有城市(中等)
就是找最小生成树!每座城市相当于图中的节点,连通城市的成本相当于边的权重,连通所有城市的最小成本即是最小生成树的权重之和。
二维数组的排序(选择第几个元素为排序依据)
int[][] nums = new int[][]{{1,4,5},{2,3,5},{7,8,5}};
// 按照0 1 2,第2个元素排序,从小到大
// 如果第2个元素相同,那就按第1个元素排序
Arrays.sort(nums, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
if (o1[2] == o2[2]) {
// 按第1个元素排序(默认从小到大)
return o1[1] - o2[1];
} else {
return o1[2] - o2[2];
}
}
});
class Solution {
int minimumCost(int n, int[][] connections) {
// 城市编号 1...n,所以初始化大小为n + 1
UF uf = new UF(n + 1);
// 对所有边按权重排序
Arrays.sort(connections, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[2] - o2[2];
}
});
// 记录最小生成树的权重之和
int mst = 0;
for (int[] edge : connections) {
int u = edge[0];
int v = edge[1];
int weight = edge[2];
// 如果这条边会产生环,则不能加入mst
if (uf.connected(u, v)) {
continue;
}
// 如果不会产生环,那就可以加入mst,属于最小生成树
mst += weight;
uf.union(u, v);
}
// 保证所有节点被连通,就是连通分量count = 1
// 但是由于题目下标从1开始,节点0自身就是独立的
// 所以count应该=2
return uf.count() == 2 ? mst : -1;
}
}
class UF {
// 连通分量个数
int count;
// 每棵树
int[] parent;
// 每棵树的节点数
int[] size;
UF(int n) {
this.count = n;
this.parent = new int[n];
this.size = new int[n];
// 图中的每个节点指向自己
for (int i = 0; i < n; i++) {
parent[i] = i;
size[i] = 1;
}
}
// 找根节点
public int find(int x) {
while (parent[x] != x) {
// 路径压缩
parent[x] = parent[parent[x]];
x = parent[x];
}
return x;
}
// 判断是否连通
public boolean connected(int p, int q) {
return find(p) == find(q);
}
// 连通两个节点
public void union(int p, int q) {
int rootP = find(p);
int rootQ = find(q);
// 本来就联通
if (rootQ == rootP) return;
if (size[rootP] > size[rootQ]) {
// 小树接在大树后
parent[rootQ] = rootP;
size[rootP] += size[rootQ];
} else {
parent[rootP] = rootQ;
size[rootQ] += size[rootP];
}
// 连通分量--
count--;
}
// 返回连通分量个数
public int count() {
return count;
}
}
有了上面的代码,可以发现,并查集也可以用于无向图中环的检测。
连接所有点的最小费用(中等)
比如题目给的例子:
points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
算法应该返回 20,按如下方式连通各点:
很显然这也是一个标准的最小生成树问题:每个点就是无向加权图中的节点,边的权重就是曼哈顿距离,连接所有点的最小费用就是最小生成树的权重和。
本题相当于把边的权重换一种方式进行告诉。
class Solution {
int minCostConnectPoints(int[][] points) {
int n = points.length;
// 生成所有边和权重
List<int[]> edges = new ArrayList<>();
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int xi = points[i][0], yi = points[i][1];
int xj = points[j][0], yj = points[j][1];
// 添加边、权重
edges.add(new int[] {
i, j, Math.abs(xi - xj) + Math.abs(yi - yj)
});
}
}
// 将边按照权重从小到大排序
Collections.sort(edges, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o1[2] - o2[2];
}
});
// 执行Kruskal算法
int mst = 0;
UF uf = new UF(n);
for (int[] edge : edges) {
int u = edge[0];
int v = edge[1];
int weight = edge[2];
// 如果这条边会产生环,就不能加入mst
if (uf.connected(u, v)) {
continue;
}
// 不会产生环,就属于最小生成树
mst += weight;
uf.union(u, v);
}
return mst;
}
}
// UF类实现见上文代码
三、Dijkstra 算法框架
首先,我们先看一下 Dijkstra 算法的签名:
// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离
int[] dijkstra(int start, List<Integer>[] graph);
// 输出一个记录最短路径权重的数组
其次,我们也需要一个 State 类来辅助算法的运行:
class State {
// 图节点的 id
int id;
// 从 start 节点到当前节点的距离
int distFromStart;
State(int id, int distFromStart) {
this.id = id;
this.distFromStart = distFromStart;
}
}
-
普通 BFS 算法中,根据 BFS 的逻辑和无权图的特点,第一次遇到某个节点所走的步数就是最短距离,所以用一个 visited 数组防止走回头路,每个节点只会经过一次。
搜索算法中,找最小步数也是用BFS算法,因为它可以保证第一次到达的就是最小的步数。
-
加权图中的 Dijkstra 算法和无权图中的普通 BFS 算法不同,在 Dijkstra 算法中,你第一次经过某个节点时的路径权重,不见得就是最小的(这也好理解,BFS只看节点,不会关注权重),所以对于同一个节点,我们可能会经过多次,而且每次的 distFromStart 可能都不一样,所以不会使用visited数组防止走回头路,因为需要走回头路,比如下图:
我会经过节点 5 三次,每次的 distFromStart 值都不一样,那我取 distFromStart 最小的那次,不就是从起点 start 到节点 5 的最短路径权重了么?
好了,明白上面的几点,我们可以来看看 Dijkstra 算法的代码模板。
其实,Dijkstra 可以理解成一个带 dp table(或者说备忘录)的 BFS 算法,伪码如下:
class State {
// 图节点的id
int id;
// 从 start 节点到当前节点的距离
int distFromStart;
State(int id, int distFromStart) {
this.distFromStart = distFromStart;
this.id = id;
}
}
// 返回节点 from 到节点 to 之间的边的权重(看实际情况,可能也不需要)
int weight(int from, int to);
// 输入节点 s 返回 s 的相邻节点(看实际情况,可能也不需要)
List<Integer> adj(int s);
// 输入一幅图和一个起点 start,计算 start 到其他节点的最短距离
int[] dijkstra(int start, List<Integer>[] graph) {
// 图中节点的个数
int V = graph.length;
// 记录最短路径的权重,你可以理解为 dp table
// 定义:distTo[i] 的值就是节点 start 到达节点 i 的最短路径权重
int[] distTo = new int[V];
// 求最小值,所以 dp table 初始化为正无穷
Arrays.fill(distTo, Integer.MAX_VALUE);
// base case,start 到 start 的最短距离就是 0
distTo[start] = 0;
// 优先级队列,distFromStart 较小的排在前面
// 普通的链表也可以,使用预先队列是为了提高效率
Queue<State> pq = new PriorityQueue<>((a, b) -> {
return a.distFromStart - b.distFromStart;
});
// 从起点 start 开始进行 BFS
pq.offer(new State(start, 0));
while (!pq.isEmpty()) {
State curState = pq.poll();
int curNodeID = curState.id;
int curDistFromStart = curState.distFromStart;
// 存在更短路径,那就continue
if (curDistFromStart > distTo[curNodeID]) {
// 已经有一条更短的路径到达 curNode 节点了
continue;
}
// 将 curNode 的相邻节点装入队列
for (int nextNodeID : adj(curNodeID)) {
// 看看从 curNode 达到 nextNode 的 “权值和” (距离)是否会更短
int distToNextNode = distTo[curNodeID] + weight(curNodeID, nextNodeID);
// 从起点经过 curNode 达到 nextNode的 “权值和” 更短,那就需要更新
if (distTo[nextNodeID] > distToNextNode) {
// 更新 dp table
distTo[nextNodeID] = distToNextNode;
// 将这个节点以及距离放入队列
pq.offer(new State(nextNodeID, distToNextNode));
}
}
}
return distTo;
}
上面的代码会计算出start起点,到任意节点的最短路径,返回结果为数组distTo,distTo[2]就是起点start到第2个节点的最短路径。
学习自:https://labuladong.gitee.io/algo/2/19/43/
对比普通的 BFS 算法,你可能会有以下疑问:
这里需要说明的是,Dijkstra算法求解最短路径,只能求解非负权值的问题,如果有负权值(准确的说是负环),则不能够求解!
如果用dijkstra算法来计算最短路径,一定会出错。
因为按照dijkstra算法,它的原则是基于贪心进行查找,所以它的查找顺序应该是:BEDC。
造成的结果就是:找到第二个节点时,就会直接判断E的最短路径为-3.并且后续节点的查找也不会改变这个值。
但是显然,我们知道E的最短路径为-4.
出现这种情况的根本原因是,负环(含负值的环)出现。导致贪心的查找逻辑无法继续成立。用通俗的话来说,就是面对负权值的环时,算法在查找时容易”鼠目寸光“,很简单的下定结论。
在这种情况下,djikstra算法就能正常工作,并且不受负权值影响。
它的查找顺序应该是:BCDE。而CD由于没有与E形成环,所以负权值就不再会破坏算法平衡,成为了一个表示大小的数字。
最后的结果显然是0.虽然这只是一个例子,但是我们可以很容易的想到,只要不出现负环,任何一个负权值图我们都可以用这种方法计算。
解决负环问题需要使用Bellman-Ford算法实现,注意!有些同学可能会想,通过偏移,使得权值全部大于0不就行了吗?这种想法是错误的,不能够解决负环的问题,不然也不会有专门的算法来解决负环问题。
// 输入起点 start 和终点 end,计算起点到终点的最短距离
int dijkstra(int start, int end, List<Integer>[] graph) {
// ...
while (!pq.isEmpty()) {
State curState = pq.poll();
int curNodeID = curState.id;
int curDistFromStart = curState.distFromStart;
// 在这里加一个判断就行了,其他代码不用改
if (curNodeID == end) {
return curDistFromStart;
}
if (curDistFromStart > distTo[curNodeID]) {
continue;
}
// ...
}
// 如果运行到这里,说明从 start 无法走到 end
return Integer.MAX_VALUE;
}
因为优先级队列自动排序的性质,每次从队列里面拿出来的都是 distFromStart 值最小的,所以当你第一次从队列中拿出终点 end 时,此时的 distFromStart 对应的值就是从 start 到 end 的最短距离。(相当于是实现普通BFS算法中,第一次到达终点的一定是距离最小的方案)
这个算法较之前的实现提前 return 了,所以效率有一定的提高。
网络延迟时间(中等)
需要注意的是,最后的答案应该是起点到所有顶点的最大的最短距离,这样才能保证所有的节点接收到信号。
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
// 先建图
List<int[]>[] graph = new LinkedList[n + 1];
// 也可以写成:
// List<Integer>[] graph = new LinkedList[n];
// List数组记得对每一个List进行初始化,才能使用
// 注意节点从1开始,数组大小要开成:n + 1
for (int i = 1; i <= n; i++) {
graph[i] = new LinkedList<>();
}
for (int i = 0; i < times.length; i++) {
int from = times[i][0];
int to = times[i][1];
int weight = times[i][2];
// from -> List<(to, weight)>
// 邻接表存储图结构,同时存储权重信息weight
graph[from].add(new int[] {to, weight});
}
// 开始dijkstra算法,k为起点
int[] distTo = dijkstra(k, graph);
// 找到最长的那一条最短路径就是答案
int res = 0;
for (int i = 1; i < distTo.length; i++) {
if (distTo[i] == Integer.MAX_VALUE) {
// 有节点到达不了
return -1;
}
res = Math.max(res, distTo[i]);
}
return res;
}
// 类中类
class State {
// 图节点的id
int id;
// 从 start 节点到当前节点的最短路径
int distFromStart;
State(int id, int distFromStart) {
this.id = id;
this.distFromStart = distFromStart;
}
}
int[] dijkstra(int start, List<int[]>[] graph) {
// distTo[i],就是start到i节点的最短路径
// distTo也是需要返回的结果数组
int[] distTo = new int[graph.length];
Arrays.fill(distTo, Integer.MAX_VALUE);
// base case
distTo[start] = 0;
// 优先队列加快速度
Queue<State> pq = new PriorityQueue<>(new Comparator<State>() {
@Override
public int compare(State o1, State o2) {
// 更小的距离优先
return o1.distFromStart - o2.distFromStart;
}
});
// 起点入队
pq.offer(new State(start, 0));
while (!pq.isEmpty()) {
State tmp = pq.poll();
int curId = tmp.id;
int curDist = tmp.distFromStart;
// 当前节点的最短路径已经小于目前遍历的路径
if (curDist > distTo[curId]) {
continue;
}
// 遍历当前节点的相邻节点
// 存储结构是:graph[start]中存储了多个[to, weight]数组
for (int[] curNode : graph[curId]) {
int nextNode = curNode[0];
int weight = curNode[1];
// 更新最短路径
if (distTo[nextNode] > weight + curDist) {
distTo[nextNode] = weight + curDist;
pq.offer(new State(nextNode, weight + curDist));
}
}
}
return distTo;
}
}
鉴于Dijkstra最短路径算法不能处理负环问题,但作为一种经典算法,需要学习其基本思想和实现方法。更好的算法,更具有包容性的算法是SPFA(Shortest Path Faster Algorithm)
※四、SPFA算法框架
spfa就是BellmanFord的一种实现方式,其具体不同在于,对于处理松弛操作时,采用了队列(先进先出方式)操作,从而大大提高了时间复杂度。
大概思路和dijkstra相似但是又大有不同,这里队列里存放的不再是1.距离最近且2.未被使用这两个条件同时存在的点了,
而是只保留一个条件:未被使用的点!所以又需要多的一个used数组来存储每个点是否被使用。
- 初始化处理,邻接表头数组h(初始化为-1 ),距离数组dist(初始化为无穷大)
- 将源点的序号加入队列中,dist[start] = 0,并将其使用状态(used)标记为true
- 在队列不为空的前提下,抛出表头元素,并将其使用状态(used)标记为false
- 用表头元素更新与它直接相连的点,若发生跟新则将其加入队列中,并将其使用状态(used)改为true
- 结束之后判断是否能够到达目标节点,若可以直接返回,不可以返回题目给定内容(一般是顺便判定负环)
有负环,不可能有最短路径,因为会一直更新下去,Dijkstra不能检测负环,但是SPFA可以,这就是为什么更推荐SPFA算法的原因。
判断负环的方法:统计当前节点的最短路径包含的边的条数,如果 == n,就存在负环。
class Solution {
public int networkDelayTime(int[][] times, int n, int k) {
// 先建图
List<int[]>[] graph = new LinkedList[n + 1];
// 也可以写成:
// List<Integer>[] graph = new LinkedList[n];
// List数组记得对每一个List进行初始化,才能使用
// 注意节点从1开始,数组大小要开成:n + 1
for (int i = 1; i <= n; i++) {
graph[i] = new LinkedList<>();
}
for (int[] time : times) {
int from = time[0];
int to = time[1];
int weight = time[2];
// from -> List<(to, weight)>
// 邻接表存储图结构,同时存储权重信息weight
graph[from].add(new int[]{to, weight});
}
// 记录开始节点到任一节点的最短路径
int[] distTo = new int[graph.length];
Arrays.fill(distTo, Integer.MAX_VALUE);
// 记录是否入队
boolean[] vis = new boolean[graph.length];
// 统计当前节点的遍历次数,用于判断负环
int[] nums = new int[graph.length];
// 初始条件
distTo[k] = 0;
vis[k] = true;
// 只有一个点,不含边
nums[k] = 0;
// 是否为负环
boolean flag = false;
// SPFA开始,k为起点
Queue<Integer> queue = new LinkedList<>();
queue.offer(k);
while (!queue.isEmpty()) {
int curId = queue.poll();
// 出队
vis[curId] = false;
// 遍历与该节点相邻的节点
for (int[] next : graph[curId]) {
int nextId = next[0];
int weight = next[1];
// 如果当前的更新距离更小才能更新
if (distTo[nextId] > distTo[curId] + weight) {
// 更新距离
distTo[nextId] = distTo[curId] + weight;
// 当前节点的最短路径包含的边数 + 1
nums[nextId] = nums[curId] + 1;
if (nums[nextId] == n) {
// 是负环
flag = true;
break;
}
// 如果队列中没有,就入队
if (vis[nextId] == false) {
vis[nextId] = true;
queue.offer(nextId);
}
}
}
// 是负环
if (flag) {
break;
}
}
// 是负环了
if (flag) {
return -1;
}
int res = 0;
for (int i = 1; i < graph.length; i++) {
if (distTo[i] == Integer.MAX_VALUE) {
return -1;
}
res = Math.max(res, distTo[i]);
}
return res;
}
}
上面的代码中,为了方便,没有使用State类,队列就是直接存储节点ID即可。
一直在说负环,下面就给一道负环的题目,就是模板题
需要注意w >= 0时,要建立双向边。
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int t = scanner.nextInt();
while (t > 0) {
int n = scanner.nextInt();
int m = scanner.nextInt();
// 先建图
List<int[]>[] graph = new LinkedList[n + 1];
for (int i = 0; i < n + 1; i++) {
graph[i] = new LinkedList<>();
}
for (int i = 0; i < m; i++) {
int u = scanner.nextInt();
int v = scanner.nextInt();
int w = scanner.nextInt();
if (w < 0) {
graph[u].add(new int[] {v, w});
} else {
graph[u].add(new int[] {v, w});
graph[v].add(new int[] {u, w});
}
}
int[] distTo = new int[n + 1];
Arrays.fill(distTo, Integer.MAX_VALUE);
// 从顶点1出发
distTo[1] = 0;
boolean[] vis = new boolean[n + 1];
int[] nums = new int[n + 1];
Queue<Integer> queue = new LinkedList<>();
queue.add(1);
vis[1] = true;
nums[1] = 0;
boolean flag = false;
while (!queue.isEmpty()) {
int curId = queue.poll();
vis[curId] = false;
for (int[] node : graph[curId]) {
int to = node[0];
int weight = node[1];
if (distTo[to] > distTo[curId] + weight) {
distTo[to] = distTo[curId] + weight;
nums[to] = nums[curId] + 1;
if (nums[to] == n) {
flag = true;
break;
}
if (vis[to] == false) {
vis[to] = true;
queue.offer(to);
}
}
}
if (flag) {
break;
}
}
if (flag == false) {
System.out.println("NO");
} else {
System.out.println("YES");
}
t--;
}
}
}
博客又爆内存了,下面内容看图论算法四吧!