文章目录
本文参考:
链式前向星——最完美图解
【宫水三叶】涵盖所有的「存图方式」与「最短路算法(详尽注释)」
堆优化版Dijkstra算法
深入理解Bellman-Ford(SPFA)算法
图论最短路:Bellman-Ford与其优化SPFA算法的一点理解
本文写于我的另一篇文章算法学习-拓扑排序期间,在进行拓扑算法学习的过程中,涉及到了一系列存图的过程,在看三叶姐的题解中,时常被这些存图的数组搞得晕头转向,由此可见,对于数组这种简单数据结构的熟练运用也是算法高超的体现啊。因此,本文对相关的存图方式以及数据结构中最普遍的最短路问题的算法,进行专题学习。
基础知识
存图方式
一、邻接矩阵存图
用最朴素的二维数组方式来存图,对于w[i][j],i、j需要根据题目中节点的编号灵活标记。
// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] w = new int[N][N];
int INF = 0x3f3f3f3f;
// 初始化邻接矩阵,不能互通的两点间距离为INF,对角线的两点距离为0
// i,j为节点数值,不一定从0开始
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] = w[j][i] = i == j ? 0 : INF;
}
}
// 加边操作
void add(int a, int b, int c) {
w[a][b] = c;
}
二、邻接表存图
一般输入都会以roads[i] = [ai, bi, distancei]的数组形式给出两个节点间的连接关系,这一步存图就是要对这些输入进行处理,汇总一个节点的所有相邻节点。
1. Collection容器存储相邻节点
图中一般「没有存储图的边代价」(如果想存入边的代价只需要用HashSet<int[]>存储就好)
着重于「邻点信息和入度信息的统计」,常用于「拓扑排序」,同时也可以用于一些与边代价无关的DFS、BFS搜索。
- HashSet数组实现如下:
//建立邻接表和入度数组
HashSet<Integer>[] adj=new HashSet[numCourses];
for(int i=0;i<numCourses;i++){
adj[i]=new HashSet<>();
}
int[] in=new int[numCourses];
//存储其他节点并统计入度信息
for(int[]p:prerequisites){
adj[p[1]].add(p[0]);
in[p[0]]+=1;
}
- ArrayList数组实现如下:
//建立邻接表和入度数组
ArrayList<Integer>[] adj=new ArrayList[numCourses];
for(int i=0;i<numCourses;i++){
adj[i]=new ArrayList<>();
}
int[] in=new int[numCourses];
//存储其他节点并统计入度信息
for(int[]p:prerequisites){
adj[p[1]].add(p[0]);
in[p[0]]+=1;
}
有些题目很坏,如6163.给定条件下构造矩阵,只能用ArrayList实现,因为给定条件中会给重边,如果在统计入度信息的时候重复计算,那么在统计节点出队列信息的时候,也应该重复减去入度。
- HashMap实现如下,但要注意在接下来取一个节点的相邻节点的时候,可能会出现对应节点的邻接节点set为空的情况,要先进行判断:
//建立邻接表和入度数组
HashMap<Integer,HashSet<Integer>> adj=new HashMap<>();
int[] in=new int[numCourses];
for(int[]p:prerequisites){
HashSet<Integer> set=adj.getOrDefault(p[1],new HashSet<>()); //对应节点的set为空,之前没初始化
set.add(p[0]); // 建立的是有向图
adj.put(p[1],set);
in[p[0]]+=1;
}
-
以上建图方式都是通过题目已经给定的数组进行邻接表的建立,在有些题目中(比如树),我们则需要通过**「递归」的方式**进行无向图的建立。
在先序遍历的过程中,对根节点,左右叶子节点分别建立邻接表,无向图存图实现如下:
HashMap<Integer,HashSet<Integer>> map;
map=new HashMap<>();
buildGraph(root);
public void buildGraph(TreeNode root){
if(root==null) return;
if(root.left!=null){
HashSet<Integer> set1=map.getOrDefault(root.val,new HashSet<Integer>());
HashSet<Integer> set2=map.getOrDefault(root.left.val,new HashSet<Integer>());
set1.add(root.left.val);
set2.add(root.val);
map.put(root.val,set1);
map.put(root.left.val,set2);
}
if(root.right!=null){
HashSet<Integer> set1=map.getOrDefault(root.val,new HashSet<Integer>());
HashSet<Integer> set2=map.getOrDefault(root.right.val,new HashSet<Integer>());
set1.add(root.right.val);
set2.add(root.val);
map.put(root.val,set1);
map.put(root.right.val,set2);
}
buildGraph(root.left);
buildGraph(root.right);
}
2. 结构体实现链式前向星
struct node中包括两种数据结构:
边集数组:edge[ ],edge[i]表示第i条边,在构造时通过cnt不断自增进行编号,以此对每条边进行区分。
头结点数组:int head[ ],head[i]存以节点数字 i 为起点的第一条边的下标(在edge[]中的下标)。该数组首先都被初始化为-1,在头插法建图的过程中不断改变。
struct node{
int to,next,w;
}edge[maxe];//边集数组,边数一般要设置比maxn*maxn大的数,如果题目有要求除外
int head[maxn];//头结点数组
举一个简单的例子如下图所示:

-
输入1 2 5;头插法中可以将head[i]视作头,
edge[0].next=head[1](-1); head[1]=cnt(0)++;

-
输入2 4 12;
edge[1].next=head[2](-1); head[2]=cnt(1)++;

-
输入1 4 3;
edge[2].next=head[1](0); head[1]=cnt(2)++;

添加一条边u v w的代码如下:
如果是有向图,执行一次add(u,v,w)就行,如果是无向图,需要执行两次add(u,v,w);add(v,u,w)。
void add(int u,int v,int w){//添加一条边
edge[cnt].to=v;
edge[cnt].w=w;
edge[cnt].next=head[u];
head[u]=cnt++;
}
访问一个节点的所有邻接点代码如下:
for(int i=head[u];i!=-1;i=edge[i].next){
int v=edge[i].to; //u的邻接点
int w=edge[i].w; //u—v的权值
…
}
3. 数组实现链式前向星
同2中的需要边、头节点两种重要的数据结构,每条边通过idx进行标记,head仍然是存储每个节点的对应的第一条边。
相关的邻接表数据结构定义如下:
// 邻接表
// 存储与 结点v 直接相连的边编号idx,head下标索引为节点的序号v
private int[] head = new int[N];
// 表示idx边所指向的节点,edgeNode下标索引为边编号idx
private int[] edgeNode = new int[M];
// 存储边编号为idx的边连接的下一条边idx,nextEdge下标索引为边编号idx
private int[] nextEdge = new int[M];
// 记录某条边idx的权重,weight下标索引为边编号idx
private int[] weight = new int[M];
邻接表加边操作:
/**
* 邻接表 加边操作
* @param srcV: 源点
* @param desV: 目标点
* @param weight: 边权重
*/
private void add(int srcV, int desV, int weight) {
// 表示边编号为idx, 指向节点的序号为desV
this.edge[idx] = desV;
// 头插法在头数组上加边
this.nextEdge[idx] = this.head[srcV];
this.head[srcV] = idx;
// 将编号为idx的边的权值 赋为 weight
this.weight[idx] = weight;
idx++;
}
最短路算法
一、Floyd(邻接矩阵)
「多源汇最短路」Floyd 算法进行求解时,使用「邻接矩阵」来进行存图。跑一遍 Floyd,可以得到「从任意起点出发,到达任意起点的最短距离」。
适用于存在重边和自环,边权可能为负数的加权图。
算法要点为:以每个点为「中转站」,刷新所有边的距离。
如果要求解某一出发点出发的最短路径,固定出发点k,从所有 w[k][x] 中取 max 即是「从 k点出发,到其他点 x 的最短距离的最大值」。
Floyd 也是基于动态规划,其原始的三维状态定义为 f[i][j][k] 代表从点 i 到点 j,且经过的所有点编号不会超过 k(即可使用点编号范围为[1,k])的最短路径。这样的状态定义引导我们能够使用 Floyd 求最小环或者求“重心点”(即删除该点后,最短路值会变大)。
// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
// 通常根据数据范围N,开辟得较大
int[][] w = new int[N][N];
int INF = 0x3f3f3f3f;
// 初始化邻接矩阵,根据已有的节点数n初始化
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] = w[j][i] = i == j ? 0 : INF;
}
}
//按题意给其他边赋值
for(int[]t:times){
int u=t[0],v=t[1],w=t[2];
w[u][v]=w;
}
void floyd() {
// floyd 基本流程为三层循环:
// 枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作,遍历已有的节点数为n的网络
for (int p = 1; p <= n; p++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] = Math.min(w[i][j], w[i][p] + w[p][j]);
}
}
}
}
时间复杂度:O(N^3)
空间复杂度:O(N^2)
二、朴素 Dijkstra(邻接矩阵)
「单源最短路」算法朴素 Dijkstra 算法进行求解时,使用「邻接矩阵」来进行存图。跑一遍 Dijkstra 我们可以得到从源点 k 到其他点 x 的最短距离,再从所有最短路中取 max 即是「从 k 点出发,到其他点 x 的最短距离的最大值」。
可以用于加权图,没有负权边,可以有环。
算法要点为:每次从 「未求出最短路径的点」中 取出 距离距离起点 最小路径的点,以这个点为桥梁, 刷新「未求出最短路径的点」的距离。
代码实现思路:
-
初始化距离
用一维数组int dist[]初始化确定的起点到其他点的距离,这个数据结构在前面存图的w[][]以后建立。只有起点确定dist[1] = 0, dist[i] = +∞。 -
迭代更新
整个过程可以看为两个不相交的集合,
集合S:当前已经确定从起点出发到其他点的最短距离,即已经选出的点,通过vis数组标记已取出;
集合T:S的补集,即还未选出的点。
for(int i = 1; i <= n; i ++)for循环n次,每次选出一个最短距离点t,将此点标记为已访问,并用此点更新S和T中的所有点的最短距离。对于t,若dist[i]>dist[t]+w[t][i],则可以用新选出来的点进行距离更新。

// 邻接矩阵数组:w[a][b] = c 代表从 a 到 b 有权重为 c 的边
int[][] w = new int[N][N];
int INF = 0x3f3f3f3f;
// 初始化邻接矩阵
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
w[i][j] = w[j][i] = i == j ? 0 : INF;
}
}
//按题意给其他边赋值
for(int[]t:times){
int u=t[0],v=t[1],w=t[2];
w[u][v]=w;
}
void dijkstra() {
// 起始先将所有的点标记为「未更新」和「距离为正无穷」
Arrays.fill(vis, false);
Arrays.fill(dist, INF);
// 只有起点最短距离为 0
dist[k] = 0;
// 迭代 n 次
for (int p = 1; p <= n; p++) {
// 每次找到「最短距离最小」且「未被更新」的点 t
int t = -1;
for (int i = 1; i <= n; i++) {
if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
}
// 标记点 t 为已更新
vis[t] = true;
// 用点 t 的「最小距离」更新其他点,这个过程如果w[i][j]有边连接,就能更新
// dist[k]刚开始为0,必然可以更新与其相近的其他点
for (int i = 1; i <= n; i++) {
dist[i] = Math.min(dist[i], dist[t] + w[t][i]);
}
}
}
时间复杂度:O(N^2)
空间复杂度:O(N^2)
三、堆优化 Dijkstra(邻接表)
堆优化 Dijkstra 算法与朴素 Dijkstra 都是「单源最短路」算法。朴素Dijkstra 的方法是遍历所有的点通过比较找出最近的点,在这个地方可以使用 优先队列 来进行优化,通过 优先队列 优化后 朴素Dijkstra算法 就叫做 堆优化版的Dijkstra算法。
首先将优先队列定义成小根堆,将与出发点的距离越小的节点放在队头。首先将出发点初始化为(点编号, 到起点的距离)new int[]{k, 0}加入到优先队列中que.add(new int[]{k, 0}),然后从这个点开始扩展。先将队头元素出队,标记为已挑选为最短边节点,同时注意「重边」问题(下面第二段有解释),然后遍历这个点的所有出边所到达的点 j(e[i]所存的指向点),更新所有点距离源点更近的距离dist[j]。
如果源点直接到 j 点的距离比源点先到最短点id 点再从 id 点到 j 点的距离大,那么就更新 dist[j],使 dist[j] 到源点的距离最短,并将该点的编号以及该点到源点的距离作为一个 new int[]{j, dist[j]} 加入到优先队列中,然后将其标记,表示该点已经确定最短距离。因为是小根堆,所以会根据距离进行排序,距离最短的点总是位于队头。一直扩展下去,直到队列为空。
因为有 「重边」 的缘故,所以从该点拓展出来的边可能会有冗余数据,即如果在扩展idx=1的时候,第一次遍历到的点是 2 号点,距离 源点 的距离为 10,此时 dist[2] = 0x3f3f3f3f > dist[1] + distance[1 -> 2] = 0 + 10 = 10 所以 dist[2] 会被更新为 10,此时会将 {2, 10} 入队。但是很不巧从 源点 到 2 号点有一个距离为 6 的重边,当遍历到这个重边时,由于 dist[2] = 10 > dist[1] + distance[1 -> 2] = 0 + 6 = 6,所以 {2, 6} 也入队了,入队之后由于是小根堆按照距离源点的距离由小到大排序,所以 {2, 6} 会排在 {2, 10} 前面,所以 {2, 6}距离短的会先出队,出队之后2节点会被标记。所以当下一次再遇到已经被标记的 2 号点时,直接 continue 忽略掉继续扩展下一个点即可。
class Solution {
int INF=0x3f3f3f3f;
int N=110;
int M=6010;
int[]head=new int[N];
int[]edge=new int[M];
int[]nextEdge=new int[M];
int[]w=new int[M];
int[]dis=new int[N];
boolean[]vis=new boolean[N];
int idx=0;
int n,k;
public void add(int a,int b,int c){
edge[idx]=b;
nextEdge[idx]=head[a];
head[a]=idx;
w[idx]=c;
idx++;
}
public int networkDelayTime(int[][] times, int _n, int _k) {
n=_n;
k=_k;
Arrays.fill(head,-1);
for(int[]t:times){
int u=t[0],v=t[1],c=t[2];
add(u,v,c);
}
pqDijkstra();
int ans=0;
for(int i=1;i<=n;i++){
ans=Math.max(ans,dis[i]);
}
return ans>INF/2?-1:ans;
}
public void pqDijkstra(){
Arrays.fill(vis,false);
Arrays.fill(dis,INF);
dis[k]=0;
PriorityQueue<int[]> que=new PriorityQueue<>((a,b)->a[1]-b[1]);
que.offer(new int[]{k,0});
while(!que.isEmpty()){
int[]top=que.poll();
int id=top[0];
//进行访问标记与忽略
if(vis[id]) continue;
vis[id]=true;
for(int i=head[id];i!=-1;i=nextEdge[i]){
//和最短边相连的所有边,判断距离是否进行更新
int j=edge[i];
if(dis[j]>dis[id]+w[i]){
dis[j]=dis[id]+w[i];
//将原先为INF,现在已经被更新的点加入到que中
que.offer(new int[]{j,dis[j]});
}
}
}
}
}
时间复杂度:总共需要遍历 m 条边,插入数据修改小根堆的时间复杂度为O(logN),所以时间复杂度为O(mlogn)。因为对于 「稀疏图」来说边数与点数很接近,所以可以看做为O(nlogn)。但是对于「稠密图」来说边数接近点数的平方个,如果「稠密图」使用堆优化版的Dijkstra算法,那么时间复杂度将会是O(n^2logn),显然不如直接使用朴素Dijkstra算法。所以堆优化版的Dijkstra算法更适用于「稀疏图」 ,而朴素Dijkstra算法更适用于「稠密图」。
四、Bellman Ford(邻接表&邻接矩阵)
Bellman Ford可以用于在「负权重图中求最短路」,该算法也是「单源最短路」算法。
Bellman Ford基于动态规划,其原始的状态定义为 f[i][k] 代表从起点到 i 点,且经过最多 k 条边的最短路径。这样的状态定义引导我们能够使用 Bellman Ford 来解决有边数限制的最短路问题。
其中涉及「迭代操作」,其定义为,每次都遍历图中的所有边,对每条边(的两个端点)都进行松弛操作。
涉及到「松弛操作」,d[to] = min(d[to], d[from] + w[from][to])或者w[i][j] = Math.min(w[i][j], w[i][p] + w[p][j]),我们可以理解其目的是为了发现距离尽可能小的路径。「每一次成功的松弛操作,都意味着我们发现了一条新的最短路」。
别人给出的总结值得在题目中慢慢体会:
- 只有上一次迭代中松弛过的点才有可能参与下一次迭代的松弛操作,这里的参与指的是让临近节点的
dist[i]改变。这个定理很容易理解,上一次迭代改变了某些点的dist[i]才会让它周围的点在下一轮更新距离中收到影响。 - 迭代的实际意义:每次迭代
k中,我们找到了经历了k条边的最短路。这个可以从源点处开始的前几轮迭代中理解,参考深入理解Bellman-Ford(SPFA)算法中的作图。这点需要重点理解,是我们解题循环体设置的前提。 - “没有点能够被松弛”时,迭代结束。这个松弛次数也可以通过题目的约束条件设置,如787.K站中转内最便宜的航班中根据中转次数定下迭代边数。
数组实现邻接表存图:
class Solution {
int INF=0x3f3f3f3f;
int N=110;
int M=6010;
int[]head=new int[N];
int[]edge=new int[M];
int[]nextEdge=new int[M];
int[]w=new int[M];
int[]dis=new int[N];
int idx=0;
int n,k;
public void add(int a,int b,int c){
edge[idx]=b;
nextEdge[idx]=head[a];
head[a]=idx;
w[idx]=c;
idx++;
}
public int networkDelayTime(int[][] times, int _n, int _k) {
n=_n;
k=_k;
Arrays.fill(head,-1);
for(int[]t:times){
int u=t[0],v=t[1],c=t[2];
add(u,v,c);
}
bf();
int ans=0;
for(int i=1;i<=n;i++){
ans=Math.max(ans,dis[i]);
}
return ans>INF/2?-1:ans;
}
public void bf(){
Arrays.fill(dis,INF);
dis[k]=0;
//迭代n次,每次都使用上一次的结果进行松弛操作
for(int p=1;p<=n;p++){
int[]prev=dis.clone();
//遍历所有节点的所有边,边索引为j
for(int i=1;i<=n;i++){
for(int j=head[i];j!=-1;j=nextEdge[j]){
int b=edge[j];
dis[b]=Math.min(dis[b],prev[i]+w[j]);
}
}
}
}
}
时间复杂度:O(n*m)相当于是迭代n次,每次遍历所有的边进行松弛操作,更新dis[]。
邻接矩阵存图:
class Solution {
int N=110;
int[][]g=new int[N][N];
int[]dis=new int[N];
int INF=0x3f3f3f3f;
int n,k,src,dst;
public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
n=_n;
k=_k;
src=_src;
dst=_dst;
//题目中的序号从0开始
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
g[i][j]=g[j][j]=i==j?0:INF;
for(int[]f:flights){
int u=f[0],v=f[1],c=f[2];
g[u][v]=c;
}
int ans=df();
return ans>INF/2?-1:ans;
}
public int df(){
Arrays.fill(dis,INF);
dis[src]=0;
//松弛k+1次,对应k+1条边
for(int p=1;p<=k+1;p++){
int[]prev=dis.clone();
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
dis[j]=Math.min(dis[j],prev[i]+g[i][j]);
}
}
}
return dis[dst];
}
}
时间复杂度:O(k*n^2)相当于是迭代k次,每次遍历所有的点进行松弛操作,更新dis[]。
五、BFS求最短路及其在内向基环树中的应用
BFS适用于「无权」图(每条边权值为1),可以有环,是单源最短路算法,是很经典的论题了。图中有环会出现一个节点被多次访问的情况,为了求最短路径避免路径长度被反复更新,需要有一个访问数组来标记访问状态,这里在进队前就标记访问。
参考数据结构笔记——最短路径BFS算法,队列模板如:
标记重复访问两种方式:
- 访问数组
- 是否是初始化的-1操作
距离更新有两种方式:
- 前面节点的距离+1
- 用BFS当前圈数作为距离
采用访问数组标记,距离按前一个点+1计算:
//求顶点u到其他顶点的最短路径
void BFS_Distance(Graph G,int u){
for(i = 0;i < G.vexnum;++i){
d[i] = w;
path[i] = -1;
}
d[u] = 0;
visited[u] = TRUE;
EnQueue(Q,u);
while(!isEmpty(Q)){
DeQueue(Q,u);
for(w = FirstNeighbor(G,u);w >= 0;w = NextNeighbor(G,u,w))
if(!visited[w]){
d[w] = d[u] + 1;
path[w] = u;
//访问数组访问标记
visited[w] = TRUE;
EnQueue(Q,w);
}//if
}//while
}
采用距离数组初始化-1判断访问,距离按前一个点+1计算:
public void bfs(int node,int[]edges,int[]dis){
ArrayDeque<Integer> que=new ArrayDeque<>();
que.offer(node);
dis[node]=0;
while(!que.isEmpty()){
int size=que.size();
for(int i=0;i<size;i++){
int top=que.poll();
//下一个点没有被访问过
if(edges[top]!=-1&&dis[edges[top]]==-1){
dis[edges[top]]=dis[top]+1;
que.offer(edges[top]);
}
}
}
}
采用距离数组初始化-1判断访问,距离按圈数增加计算:
public void bfs(int node,int[]edges,int[]dis){
ArrayDeque<Integer> que=new ArrayDeque<>();
que.offer(node);
dis[node]=0;
int cur=0;
while(!que.isEmpty()){
int size=que.size();
cur++;
for(int i=0;i<size;i++){
int top=que.poll();
if(edges[top]!=-1&&dis[edges[top]]==-1){
dis[edges[top]]=cur;
que.offer(edges[top]);
}
}
}
}
基环树是一种特殊的图,可以理解为「树加一边」使之成环,每个节点只有「一条出边」,即只有一个子代节点,这可以和一个节点对应多个节点的多叉树以及无向图做区分。参考基环树,基环内向树,基环外向树、内向基环树。由于每个节点至多有一个子代节点,因此可根据题意直接迭代节点的子节点即可。
public void bfs(int node,int[]edges,int[]dis){
//当前点没有被访问过,在不是最后一个点以及有环的情况下一直算dis
for(int d=0;node!=-1&&dis[node]==-1;node=edges[node]){
dis[node]=d++;
}
}
时间复杂度:O(N)。
相关题目
743.网络延迟时间
套用最短路模板解题即可。
Floyd解法:
class Solution {
int N=110;
int M=6010;
int[][]w=new int[N][N];
int INF=0x3f3f3f3f;
int n,k;
public int networkDelayTime(int[][] times, int _n, int _k) {
n=_n;
k=_k;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
w[i][j]=w[j][i]=i==j?0:INF;
}
}
for(int[]t:times){
int u=t[0],v=t[1],c=t[2];
w[u][v]=c;
}
floyd();
int ans=0;
for(int i=1;i<=n;i++){
ans=Math.max(ans,w[k][i]);
}
return ans>INF/2?-1:ans;
}
public void floyd(){
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
w[i][j]=Math.min(w[i][j],w[i][k]+w[k][j]);
}
}
}
}
}
朴素Dijkstra:
class Solution {
int N=110;
int M=6010;
int[][]w=new int[N][N];
int n,k;
int INF=0x3f3f3f3f;
boolean[]vis=new boolean[N];
//dist[x] = y 代表从「源点/起点」到 x 的最短距离为 y
int[]dis=new int[N];
public int networkDelayTime(int[][] times, int _n, int _k) {
n=_n;
k=_k;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
w[i][j]=w[j][i]=i==j?0:INF;
}
}
for(int[]t:times){
int u=t[0],v=t[1],c=t[2];
w[u][v]=c;
}
dijkstra();
int ans=0;
for(int i=1;i<=n;i++){
ans=Math.max(ans,dis[i]);
}
return ans>INF/2?-1:ans;
}
public void dijkstra(){
Arrays.fill(vis,false);
Arrays.fill(dis,INF);
// 只有起点最短距离为 0
dis[k]=0;
//每次选出一个点
for(int p=1;p<=n;p++){
// 每次找到「最短距离最小」且「未被更新」的点 t
int t=-1;
//找出未选出的最短边点
for(int i=1;i<=n;i++){
if(!vis[i]&&(t==-1||dis[t]>dis[i])) t=i;
}
// 标记点 t 为已更新
vis[t]=true;
for(int i=1;i<=n;i++){
dis[i]=Math.min(dis[i],dis[t]+w[t][i]);
}
}
}
}
堆优化Dijkstra:
class Solution {
int INF=0x3f3f3f3f;
int N=110;
int M=6010;
int[]head=new int[N];
int[]edge=new int[M];
int[]nextEdge=new int[M];
int[]w=new int[M];
int[]dis=new int[N];
boolean[]vis=new boolean[N];
int idx=0;
int n,k;
public void add(int a,int b,int c){
edge[idx]=b;
nextEdge[idx]=head[a];
head[a]=idx;
w[idx]=c;
idx++;
}
public int networkDelayTime(int[][] times, int _n, int _k) {
n=_n;
k=_k;
Arrays.fill(head,-1);
for(int[]t:times){
int u=t[0],v=t[1],c=t[2];
add(u,v,c);
}
pqDijkstra();
int ans=0;
for(int i=1;i<=n;i++){
ans=Math.max(ans,dis[i]);
}
return ans>INF/2?-1:ans;
}
public void pqDijkstra(){
Arrays.fill(vis,false);
Arrays.fill(dis,INF);
dis[k]=0;
PriorityQueue<int[]> que=new PriorityQueue<>((a,b)->a[1]-b[1]);
que.offer(new int[]{k,0});
while(!que.isEmpty()){
int[]top=que.poll();
int id=top[0];
//进行访问标记与忽略
if(vis[id]) continue;
vis[id]=true;
for(int i=head[id];i!=-1;i=nextEdge[i]){
//和最短边相连的所有边,判断距离是否进行更新
int j=edge[i];
if(dis[j]>dis[id]+w[i]){
dis[j]=dis[id]+w[i];
//将原先为INF,现在已经被更新的点加入到que中
que.offer(new int[]{j,dis[j]});
}
}
}
}
}
Bellman Ford解法:
class Solution {
int INF=0x3f3f3f3f;
int N=110;
int M=6010;
int[]head=new int[N];
int[]edge=new int[M];
int[]nextEdge=new int[M];
int[]w=new int[M];
int[]dis=new int[N];
int idx=0;
int n,k;
public void add(int a,int b,int c){
edge[idx]=b;
nextEdge[idx]=head[a];
head[a]=idx;
w[idx]=c;
idx++;
}
public int networkDelayTime(int[][] times, int _n, int _k) {
n=_n;
k=_k;
Arrays.fill(head,-1);
for(int[]t:times){
int u=t[0],v=t[1],c=t[2];
add(u,v,c);
}
bf();
int ans=0;
for(int i=1;i<=n;i++){
ans=Math.max(ans,dis[i]);
}
return ans>INF/2?-1:ans;
}
public void bf(){
Arrays.fill(dis,INF);
dis[k]=0;
//迭代n次,每次都使用上一次的结果进行松弛操作
for(int p=1;p<=n;p++){
int[]prev=dis.clone();
//遍历所有节点的所有边,边索引为j
for(int i=1;i<=n;i++){
//通过节点编号找到与其相连的所有边
for(int j=head[i];j!=-1;j=nextEdge[j]){
int b=edge[j];
dis[b]=Math.min(dis[b],prev[i]+w[j]);
}
}
}
}
}
787.K站中转内最便宜的航班
相比于经典的单源最短路和多源最短路问题,多了个中转的限制。采用Bellman Ford,「限制最多经过不超过 k 个点」等价于「限制最多不超过 k+1 条边」。
用邻接矩阵存图,在k+1次松弛中,每次都遍历全图,对dis[]进行更新。
class Solution {
int N=110;
int[][]g=new int[N][N];
int[]dis=new int[N];
int INF=0x3f3f3f3f;
int n,k,src,dst;
public int findCheapestPrice(int _n, int[][] flights, int _src, int _dst, int _k) {
n=_n;
k=_k;
src=_src;
dst=_dst;
//题目中的序号从0开始
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
g[i][j]=g[j][j]=i==j?0:INF;
for(int[]f:flights){
int u=f[0],v=f[1],c=f[2];
g[u][v]=c;
}
int ans=df();
return ans>INF/2?-1:ans;
}
public int df(){
Arrays.fill(dis,INF);
dis[src]=0;
//松弛k+1次,对应k+1条边
for(int p=1;p<=k+1;p++){
int[]prev=dis.clone();
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
dis[j]=Math.min(dis[j],prev[i]+g[i][j]);
}
}
}
return dis[dst];
}
}
更进一步,由于 Bellman Ford 核心操作是每次迭代中需要遍历所有的边,因此也可以直接使用flights数组进行图遍历,而无须额外存图。
class Solution {
int N=110;
int[]dis=new int[N];
int INF=0x3f3f3f3f;
public int findCheapestPrice(int n, int[][] flights, int src, int dst, int k) {
Arrays.fill(dis,INF);
dis[src]=0;
for(int p=0;p<k+1;p++){
int[]prev=dis.clone();
for(int[]f:flights){
int x=f[0],y=f[1],c=f[2];
//在flights中存的起点和终点之间更新
dis[y]=Math.min(dis[y],prev[x]+c);
}
}
int ans=dis[dst];
return ans>INF/2?-1:ans;
}
}
1976.到达目的地的方案数
参考题解【宫水三叶】图论综合题 : 朴素 Dijkstra + 拓扑排序 + DP
先用邻接矩阵存储稠密图,然后采用单源最短路算法Dijsktra进行最短路求解。根据最短距离数组int[] dist重建最短路的关键路径图,在此图上跑拓扑排序,结合动态规划统计方案数,排序节点上为到该节点的最短路径方案数。
class Solution {
int N=210;
int MOD=(int)1e9+7;
long[][]g=new long[N][N];
//这里需要保证所有路径加起来的和不超过INF,INF大于等于200*1E9
long INF=(long)1e12;
boolean[]vis=new boolean[N];
long[]dis=new long[N];
int[]in=new int[N];
int n;
public int countPaths(int _n, int[][] roads) {
n=_n;
//dijkstra求出最短路径长度数组dis
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
g[i][j]=g[j][i]=i==j?0:INF;
}
}
for(int[]r:roads){
int u=r[0],v=r[1],c=r[2];
g[u][v]=g[v][u]=c;
}
dijkstra();
//根据最短长度数组重建图,方便拓扑排序
HashSet<Integer>[] adj=new HashSet[N];
for(int i=0;i<N;i++){
adj[i]=new HashSet<>();
}
for(int[]r:roads){
int u=r[0],v=r[1],c=r[2];
g[u][v]=g[v][u]=c;
if(dis[u]+c==dis[v]){
adj[u].add(v);
in[v]++;
}
if(dis[v]+c==dis[u]){
adj[v].add(u);
in[u]++;
}
}
//f[i]表示在最短路径图中,到路口编号为i的方案数为f[i]
long[]f=new long[N];
f[0]=1;
//入度为0的点先入队列,在该题中只有源点符合条件
Deque<Integer> que=new ArrayDeque<>();
que.offer(0);
while(!que.isEmpty()){
int top=que.poll();
for(int i:adj[top]){
f[i]+=f[top];
f[i]%=MOD;
if(--in[i]==0) que.offer(i);
}
}
return (int)f[n-1];
}
public void dijkstra(){
Arrays.fill(vis,false);
Arrays.fill(dis,INF);
dis[0]=0;
//迭代n次,选出n个点
for(int p=0;p<n;p++){
int t=-1;
//第一次通常选出前面赋值的源点dis[0]=0;
for(int i=0;i<n;i++){
if(!vis[i]&&(t==-1||dis[i]<dis[t])){
t=i;
}
}
vis[t]=true;
for(int i=0;i<n;i++){
dis[i]=Math.min(dis[i],dis[t]+g[t][i]);
}
}
}
}
2359.找到离给定两个节点最近的节点
无权图有环,可以尝试BFS求最短路算法,而不用小题大做用其他有权图算法。采用距离数组-1做访问判断,距离按圈数增加+1。
class Solution {
public int closestMeetingNode(int[] edges, int node1, int node2) {
int len=edges.length;
int[] dis1=new int[len];
int[] dis2=new int[len];
Arrays.fill(dis1,-1);
Arrays.fill(dis2,-1);
bfs(node1,edges,dis1);
bfs(node2,edges,dis2);
int minDis=len;
int index=-1;
for(int i=0;i<len;i++){
if(dis1[i]==-1||dis2[i]==-1) continue;
int maxD=Math.max(dis1[i],dis2[i]);
if(maxD<minDis){
minDis=maxD;
index=i;
}
}
return index;
}
public void bfs(int node,int[]edges,int[]dis){
ArrayDeque<Integer> que=new ArrayDeque<>();
que.offer(node);
dis[node]=0;
int cur=0;
while(!que.isEmpty()){
int size=que.size();
cur++;
for(int i=0;i<size;i++){
int top=que.poll();
if(edges[top]!=-1&&dis[edges[top]]==-1){
dis[edges[top]]=cur;
que.offer(edges[top]);
}
}
}
}
}
这道题目给的是内向基环树,可以采用一个循环求最短路径的方法,
class Solution {
public int closestMeetingNode(int[] edges, int node1, int node2) {
int len=edges.length;
int[] dis1=new int[len];
int[] dis2=new int[len];
Arrays.fill(dis1,-1);
Arrays.fill(dis2,-1);
bfs(node1,edges,dis1);
bfs(node2,edges,dis2);
int minDis=len;
//不存在返回-1
int index=-1;
for(int i=0;i<len;i++){
if(dis1[i]==-1||dis2[i]==-1) continue;
int maxD=Math.max(dis1[i],dis2[i]);
if(maxD<minDis){
minDis=maxD;
index=i;
}
}
return index;
}
public void bfs(int node,int[]edges,int[]dis){
//在不是最后一个点以及有环的情况下一直算dis
for(int d=0;node!=-1&&dis[node]==-1;node=edges[node]){
dis[node]=d++;
}
}
}
304

被折叠的 条评论
为什么被折叠?



