数据结构与算法:Dijkstra算法和分层图最短路
前言
这次的这些题感觉就会比前几篇要简单一点了,大多都是是背模板。
一、Dijkstra算法
Dijkstra算法是用在权值无负数的图上,用来找最短距离的算法。
1.模板——网络延迟时间
class Solution {
public:
//邻接表建图
vector<vector<vector<int>>>graph;
static bool cmp(vector<int>&a,vector<int>&b)
{
return a[1]>b[1];
}
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
int m=times.size();
build(n);
//建图
for(int i=0;i<m;i++)
{
graph[times[i][0]].push_back({times[i][1],times[i][2]});
}
vector<int>distance(n+1,INT_MAX);
distance[k]=0;
vector<bool>visited(n+1,false);
priority_queue<vector<int>,vector<vector<int>>,decltype(&cmp)>heap(cmp);
heap.push({k,0});
while(!heap.empty())
{
int u=heap.top()[0];
heap.pop();
if(!visited[u])
{
visited[u]=true;
for(int i=0;i<graph[u].size();i++)
{
int v=graph[u][i][0];
int w=graph[u][i][1];
if(!visited[v]&&distance[u]+w<distance[v])
{
distance[v]=distance[u]+w;
heap.push({v,distance[v]});
}
}
}
}
int ans=INT_MIN;
for(int i=1;i<=n;i++)
{
ans=max(ans,distance[i]);
}
return ans==INT_MAX?-1:ans;
}
void build(int n)
{
graph.resize(n+1);
}
};
Dijkstra算法的过程就是,首先设置distance数组存起点到每个点的最短距离,那么为了每次求最短,所以初始时每个点都设置成无穷大。之后除了还要设置一个visited数组记录来没来过,还要借助一个以distance为排序的小根堆。
之后只要堆不为空,每次取堆顶元素,没来过就去当前节点的所有边看,如果到当前节点的距离加上边权比下一个点的距离更小,即通过这条路能把去下一个节点的距离变得更小,就更新并入堆。
2.模板——【模板】单源最短路径(标准版)
#include<bits/stdc++.h>
using namespace std;
//邻接表建图
vector<vector<vector<int>>>graph;
static bool cmp(vector<int>&a,vector<int>&b)
{
return a[1]>b[1];
}
void build(int n)
{
graph.resize(n+1);
}
void solve(int n,int m,int s,vector<vector<int>>&edges)
{
build(n);
//建图
for(int i=0;i<m;i++)
{
graph[edges[i][0]].push_back({edges[i][1],edges[i][2]});
}
vector<int>distance(n+1,INT_MAX);
distance[s]=0;
vector<bool>visited(n+1,false);
//小根堆
priority_queue<vector<int>,vector<vector<int>>,decltype(&cmp)>heap(cmp);
heap.push({s,0});
while(!heap.empty())
{
int u=heap.top()[0];
heap.pop();
if(!visited[u])
{
visited[u]=true;
for(int i=0;i<graph[u].size();i++)
{
int v=graph[u][i][0];
int w=graph[u][i][1];
if(!visited[v]&&distance[u]+w<distance[v])
{
distance[v]=distance[u]+w;
heap.push({v,distance[v]});
}
}
}
}
//输出
for(int i=1;i<=n;i++)
{
cout<<distance[i]<<" ";
}
}
void read()
{
int n,m,s;
cin>>n>>m>>s;
vector<vector<int>>edges(m,vector<int>(3));
for(int i=0;i<m;i++)
{
cin>>edges[i][0]>>edges[i][1]>>edges[i][2];
}
solve(n,m,s,edges);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
read();
return 0;
}
这个题跟上个题一样。
其实观察一下就能发现,在数据结构与算法:宽度优先遍历中提到的01bfs其实就是Dijkstra的特殊情况,而当边权只有0和1时,就可以用双端队列代替小根堆。
3.最小体力消耗路径
class Solution {
public:
vector<int>move={-1,0,1,0,-1};
static bool cmp(vector<int>&a,vector<int>&b)
{
return a[2]>b[2];
}
int minimumEffortPath(vector<vector<int>>& heights) {
int n=heights.size();
int m=heights[0].size();
vector<vector<int>>distacne(n,vector<int>(m,INT_MAX));
distacne[0][0]=0;
vector<vector<bool>>visited(n,vector<bool>(m,false));
priority_queue<vector<int>,vector<vector<int>>,decltype(&cmp)>heap(cmp);
heap.push({0,0,0});
while(!heap.empty())
{
int x=heap.top()[0];
int y=heap.top()[1];
int c=heap.top()[2];
heap.pop();
if(!visited[x][y])
{
//剪枝
if(x==n-1&y==m-1)
{
return c;
}
visited[x][y]=true;
for(int i=0;i<4;i++)
{
int nx=x+move[i];
int ny=y+move[i+1];
if(nx>=0&&nx<n&&ny>=0&&ny<m&&!visited[nx][ny])
{
int nc=max(c,abs(heights[x][y]-heights[nx][ny]));
if(nc<distacne[nx][ny])
{
distacne[nx][ny]=nc;
heap.push({nx,ny,nc});
}
}
}
}
}
return distacne[n-1][m-1];
}
};
这个题其实还是Dijkstra的模板题,只是计算距离的方法不同。
所以只需要统计最大的绝对差,即目前为止的绝对差和当前点和下一个点的绝对差的最大值,然后以此为去往下一个点的代价更新即可。
4.水位上升的泳池中游泳
class Solution {
public:
vector<int>move={-1,0,1,0,-1};
static bool cmp(vector<int>&a,vector<int>&b)
{
return a[2]>b[2];
}
int swimInWater(vector<vector<int>>& grid) {
int n=grid.size();
vector<vector<int>>distance(n,vector<int>(n,INT_MAX));
distance[0][0]=grid[0][0];
vector<vector<bool>>visited(n,vector<bool>(n,false));
priority_queue<vector<int>,vector<vector<int>>,decltype(&cmp)>heap(cmp);
heap.push({0,0,distance[0][0]});
while(!heap.empty())
{
int x=heap.top()[0];
int y=heap.top()[1];
int t=heap.top()[2];
heap.pop();
if(!visited[x][y])
{
//剪枝
if(x==n-1&&y==n-1)
{
return t;
}
visited[x][y]=true;
for(int i=0;i<4;i++)
{
int nx=x+move[i];
int ny=y+move[i+1];
if(nx>=0&&nx<n&&ny>=0&&ny<n&&!visited[nx][ny])
{
int nt=max(0,grid[nx][ny]-t);
if(distance[x][y]+nt<distance[nx][ny])
{
distance[nx][ny]=distance[x][y]+nt;
heap.push({nx,ny,distance[nx][ny]});
}
}
}
}
}
return distance[n-1][n-1];
}
};
这个题其实也是模板,不同的还是统计边权,即去往下一个点的时间为0和下一个点水升上来的时间减去来到当前点的时间取最大值。
二、分层图最短路
当最短距离需要满足其他条件时,可以将来到相同点时不同的已满足条件的情况看作不同的节点。
1.获取所有钥匙的最短路径
class Solution {
public:
vector<int>move={-1,0,1,0,-1};
int shortestPathAllKeys(vector<string>& grid) {
int n=grid.size();
int m=grid[0].length();
queue<vector<int>>node;
int key=0;//用位信息压缩钥匙状态
//初始化
for(int i=0;i<n;i++)
{
for(int j=0;j<m;j++)
{
if(grid[i][j]=='@')
{
node.push({i,j,0});
}
//是钥匙
if(grid[i][j]>='a'&&grid[i][j]<='f')
{
key|=1<<(grid[i][j]-'a');
}
}
}
//初始化visited -> 考虑钥匙状态
vector<vector<vector<bool>>>visited
(n,vector<vector<bool>>(m,vector<bool>(key+1,false)));
int level=0;
while(!node.empty())
{
level++;
int size=node.size();
for(int i=0;i<size;i++)
{
int x=node.front()[0];
int y=node.front()[1];
int k=node.front()[2];
node.pop();
for(int j=0;j<4;j++)
{
int nx=x+move[j];
int ny=y+move[j+1];
int nk=k;
if(nx>=0&&nx<n&&ny>=0&&ny<m&&grid[nx][ny]!='#')
{
//是锁且没钥匙
if(grid[nx][ny]>='A'&&grid[nx][ny]<='F'&&
(nk&(1<<(grid[nx][ny]-'A')))==0)
{
continue;
}
//是钥匙
if(grid[nx][ny]>='a'&&grid[nx][ny]<='f')
{
nk|=1<<(grid[nx][ny]-'a');
}
//剪枝
if(nk==key)
{
return level;
}
if(!visited[nx][ny][nk])
{
visited[nx][ny][nk]=true;
node.push({nx,ny,nk});
}
}
}
}
}
return -1;
}
};
这个题除了距离还需要钥匙这个条件,再观察数据范围可以发现钥匙就六个,所以考虑用位信息来压缩已有钥匙的状态。
首先需要遍历格子找出总共的钥匙数和起点位置。由于已有钥匙的状态会看作不同节点,所以visited数组要再升一维。之后去跑宽度优先遍历,注意当有锁但没钥匙时要直接跳过不看,是钥匙的话就更新状态即可。
2.电动车游城市
class Solution {
public:
//邻接表建图
vector<vector<vector<int>>>graph;
static bool cmp(vector<int>&a,vector<int>&b)
{
return a[2]>b[2];
}
int electricCarPlan(vector<vector<int>>& paths, int cnt, int start, int end, vector<int>& charge) {
int n=charge.size();
int m=paths.size();
//建图 -> 无向图!
graph.resize(n);
for(int i=0;i<m;i++)
{
graph[paths[i][0]].push_back({paths[i][1],paths[i][2]});
graph[paths[i][1]].push_back({paths[i][0],paths[i][2]});
}
vector<vector<int>>distance(n,vector<int>(cnt+1,INT_MAX));
distance[start][0]=0;
vector<vector<bool>>visited(n,vector<bool>(cnt+1,false));
priority_queue<vector<int>,vector<vector<int>>,decltype(&cmp)>heap(cmp);
heap.push({start,0,0});//当前点 来到当前点的电量 花费的时间
while(!heap.empty())
{
int cur=heap.top()[0];
int power=heap.top()[1];
int time=heap.top()[2];
heap.pop();
if(!visited[cur][power])
{
//剪枝
if(cur==end)
{
return time;
}
visited[cur][power]=true;
//能充电 -> 充一格
if(power<cnt)
{
if(!visited[cur][power+1]&&time+charge[cur]<distance[cur][power+1])
{
distance[cur][power+1]=time+charge[cur];
heap.push({cur,power+1,distance[cur][power+1]});
}
}
//不充电
for(int i=0;i<graph[cur].size();i++)
{
int next=graph[cur][i][0];
int restPower=power-graph[cur][i][1];
int nextTime=time+graph[cur][i][1];
if(restPower>=0&&!visited[next][restPower]&&
nextTime<distance[next][restPower])
//能到且时间更短
{
distance[next][restPower]=nextTime;
heap.push({next,restPower,nextTime});
}
}
}
}
return -1;
}
};
这个题需要考虑的就是剩余电量,所以每来到一个点都要分充电和不充电两种情况考虑。因为不同的剩余电量属于不同状态,所以每次充电时不用讨论充几格,而是同一只充一格,剩下的留给之后去充。之后根据充电和不充电对应的时间代价去跑Dijkstra即可。
3.飞行路线
#include<bits/stdc++.h>
using namespace std;
static bool cmp(vector<int>&a,vector<int>&b)
{
return a[2]>b[2];
}
//邻接表建图
vector<vector<vector<int>>>graph;
int solve(int n,int m,int k,int s,int t,vector<vector<int>>&edges)
{
//建图 -> 无向图!
graph.resize(n);
for(int i=0;i<m;i++)
{
graph[edges[i][0]].push_back({edges[i][1],edges[i][2]});
graph[edges[i][1]].push_back({edges[i][0],edges[i][2]});
}
vector<vector<int>>distance(n,vector<int>(k+1,INT_MAX));
distance[s][0]=0;
vector<vector<bool>>visited(n,vector<bool>(k+1,false));
priority_queue<vector<int>,vector<vector<int>>,decltype(&cmp)>heap(cmp);
heap.push({s,0,0});
while(!heap.empty())
{
int cur=heap.top()[0];
int use=heap.top()[1];
int cost=heap.top()[2];
heap.pop();
if(!visited[cur][use])
{
if(cur==t)
{
return cost;
}
visited[cur][use]=true;
for(int i=0;i<graph[cur].size();i++)
{
int next=graph[cur][i][0];
//能用 -> 用一次
int nextUse=use+1;
int nextCost=0;
if(use<k&&!visited[next][nextUse]&&nextCost+distance[cur][use]<distance[next][nextUse])
{
distance[next][nextUse]=nextCost+distance[cur][use];
heap.push({next,nextUse,distance[next][nextUse]});
}
//不用
nextUse=use;
nextCost=graph[cur][i][1];
if(!visited[next][nextUse]&&nextCost+distance[cur][use]<distance[next][nextUse])
{
distance[next][nextUse]=nextCost+distance[cur][use];
heap.push({next,nextUse,distance[next][nextUse]});
}
}
}
}
return -1;
}
void read()
{
int n,m,k;
cin>>n>>m>>k;
int s,t;
cin>>s>>t;
vector<vector<int>>edges(m,vector<int>(3));
for(int i=0;i<m;i++)
{
cin>>edges[i][0]>>edges[i][1]>>edges[i][2];
}
cout<<solve(n,m,k,s,t,edges);
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
read();
return 0;
}
这个题就是讨论用和不用两种情况即可。
总结
其实这几道题归根结底还是模板,重点是要通过分析题目想到可以用Dijkstra和分层图最短路解决。