所谓dijkstra算法就是解决从原点到所有点的最短距离(边有权值)问题的算法之一,注意dijkstra算法只适用于边的权值非负的情况。
模板:
1,暴力版本
最暴力的dijkstra算法的复杂度是o(n^2)的,就是维护一个distance数组代表到每个节点的最短距离,一开始数组初始化为正无穷,到原点的距离是0,从原点开始,原点指向的所有点修改distance,此后每来到一个点遍历所有他指向的边,如果distance[当前点]+weight小于distance[终点],就修改终点的distance值,在更新的同时记录走过的边,如果这个边走过就不再研究,直到所有的边都走过结束循环,这是最暴力的解法,无需深究。
2,普通堆版本
该版本是最适用的版本,主要掌握该种解法,首先需要准备distance数组以及visited数组代表点的目前最短距离和是否弹出过该点,然后需要一个小根堆,堆中元素为[点,最短距离]以最短距离为比较标准,从原点开始把原点指向的所有点和边的权值(即当前该点最短距离)加入堆中,此后进入循环,弹出堆中节点,如果该节点已经弹出过就忽略,如果没弹出过就标记为弹出过,同时研究该点所有指向的节点,如果distance[当前点]+weight小于distance[终点]就把该终点distance更新并加入堆中,继续循环,直到堆为空,程序结束。复杂度o(n*logn)(n是边的数量)
简单的理解这个算法的原理就是堆中一直弹出最小的距离,如果有多个该节点的距离也肯定弹出最小的一个就是答案,在这之后研究的所有点和距离就算可以来到此节点,肯定是后弹出的距离加上边的权值,肯定要大于我们一开始确定的答案,所以这种方法得出的就是最短距离,具体证明略。
3,反向索引堆版本
复杂度可以做到o(n*logm)(n是边,m是节点数)因为节点数绝大多数情况下小于边的数量所以这种解法可以做到最极致的速度,先挖坑攒着,有机会来补上该版本解法。
板子:743.网络延迟时间
class Solution {
public:
int networkDelayTime(vector<vector<int>>& times, int n, int k) {
vector<int> dis(n+1,INT_MAX);
dis[k]=0;
vector<bool> vis(n+1);
vector<vector<pair<int,int>>> graph(n+1);
int m=times.size();
for(int i=0;i<m;i++){
graph[times[i][0]].push_back({times[i][1],times[i][2]});
}
priority_queue<pair<int,int>,vector<pair<int,int>>,greater<>> pq;
pq.emplace(0,k);
while(!pq.empty()){
auto tmp=pq.top();
pq.pop();
if(!vis[tmp.second]){
vis[tmp.second]=true;
for(auto it=graph[tmp.second].begin();it!=graph[tmp.second].end();it++){
if((*it).second+dis[tmp.second]<dis[(*it).first]&&!vis[(*it).first]){
dis[(*it).first]=(*it).second+dis[tmp.second];
pq.emplace((*it).second+dis[tmp.second],(*it).first);
}
}
}
}
int ans=-1;
for(int i=1;i<=n;i++){
ans=max(ans,dis[i]);
}
return ans==INT_MAX?-1:ans;
}
};
1631.最小体力消耗
你准备参加一场远足活动。给你一个二维 rows x columns
的地图 heights
,其中 heights[row][col]
表示格子 (row, col)
的高度。一开始你在最左上角的格子 (0, 0)
,且你希望去最右下角的格子 (rows-1, columns-1)
(注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。
一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。
请你返回从左上角走到右下角的最小 体力消耗值 。
这道题就是一个很简单可以抽象成最短路的问题,原点就是起点,求出distance表之后返回到右下角的距离即可,注意边的权值是一整条路径上高度差绝对值的最大值,所以只有当前位置绝对值大于前面所有边权值最大值的时候才会更新,此外,因为我们知道djk算法其实弹出一个点之后就已经求出了到该点的最小距离,所以不必要全部算完,求出到右下角距离后结束循环即可。
class Solution {
public:
int abs(int a){
return a>0?a:0-a;
}
int minimumEffortPath(vector<vector<int>>& heights) {
int n=heights.size(),m=heights[0].size();
cout<<n<<" "<<m<<endl;
vector<vector<int>> dis(n+1,vector<int>(m+1));
for(int i=0;i<n;i++){
for(int j=0;j<m;j++) dis[i][j]=INT_MAX;
}
dis[0][0]=0;
vector<vector<bool>> vis(n+1,vector<bool>(m+1));
int mov[5]={1,0,-1,0,1};
priority_queue<pair<int,pair<int,int>>,vector<pair<int,pair<int,int>>>,greater<>> pq;
pair<int,int> a=pair<int,int>(0,0);
pq.emplace(0,a);//first是距离,second是点序号
while(!pq.empty()){
auto tmp=pq.top();
pq.pop();
int u=tmp.first,cx=tmp.second.first,cy=tmp.second.second;
if(!vis[cx][cy]){
if(cx==n-1&&cy==m-1){
return u;
}
vis[cx][cy]=true;
for(int i=0;i<4;i++){
int x=mov[i];
int y=mov[i+1];
if(cx+x>=0&&cx+x<n&&cy+y>=0&&cy+y<m){
if(!vis[cx+x][cy+y]&&max(u,abs(heights[cx][cy]-heights[cx+x][cy+y]))<dis[cx+x][cy+y]){
dis[cx+x][cy+y]=max(u,abs(heights[cx][cy]-heights[cx+x][cy+y]));
pair<int,int> b=pair<int,int>(cx+x,cy+y);
pq.emplace(dis[cx+x][cy+y],b);
}
}
}
}
}
return dis[n-1][m-1];
}
};
778.水位上升的泳池中游泳
在一个 n x n
的整数矩阵 grid
中,每一个方格的值 grid[i][j]
表示位置 (i, j)
的平台高度。
当开始下雨时,在时间为 t
时,水池中的水位为 t
。你可以从一个平台游向四周相邻的任意一个平台,但是前提是此时水位必须同时淹没这两个平台。假定你可以瞬间移动无限距离,也就是默认在方格内部游动是不耗时的。当然,在你游泳的时候你必须待在坐标方格里面。
你从坐标方格的左上平台 (0,0)
出发。返回 你到达坐标方格的右下平台 (n-1, n-1)
所需的最少时间 。
这道题和上一道题异曲同工就是更新一条路径的代价不同,本题中一条路径的代价是途径的点中值最大的一个。
class Solution {
public:
int swimInWater(vector<vector<int>>& grid) {
int move[5]={1,0,-1,0,1};
int n=grid.size();
vector<vector<int>> dis(n,vector<int>(n));
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
dis[i][j]=INT_MAX;
}
}
dis[0][0]=grid[0][0];
vector<vector<bool>> vis(n,vector<bool>(n));
priority_queue<pair<int,pair<int,int>>,vector<pair<int,pair<int,int>>>,greater<>> pq;
pair<int,int> a=pair<int,int>(0,0);
pq.emplace(dis[0][0],a);
while(!pq.empty()){
auto tmp=pq.top();
pq.pop();
int d=tmp.first,x=tmp.second.first,y=tmp.second.second;
if(!vis[x][y]){
if(x==n-1&&y==n-1){
return d;
}
vis[x][y]=true;
for(int i=0;i<4;i++){
int cx=x+move[i],cy=y+move[i+1];
if(cx>=0&&cx<n&&cy<n&&cy>=0&&dis[cx][cy]>max(d,grid[cx][cy])){
dis[cx][cy]=max(d,grid[cx][cy]);
pair<int,int> b=pair<int,int>(cx,cy);
pq.emplace(dis[cx][cy],b);
}
}
}
}
return dis[n-1][n-1];
}
};
分层图最短路:(扩点最短路)
是图论算法里的一个优化算法,当最短路的路径选择需要受到一些其他限制的时候,可以进行扩点,所谓扩点就是把该点的状态扩展为该点本身加上受到的限制条件,这样在进行判断,弹出等操作时可以结合扩点的限制条件进行判断,在具体题目中扩点的含义以及如何分层是难点,结合题目分析总结 。
864.获取所有钥匙的最短途径
给定一个二维网格 grid
,其中:
- '.' 代表一个空房间
- '#' 代表一堵墙
- '@' 是起点
- 小写字母代表钥匙
- 大写字母代表锁
我们从起点开始出发,一次移动是指向四个基本方向之一行走一个单位空间。我们不能在网格外面行走,也无法穿过一堵墙。如果途经一个钥匙,我们就把它捡起来。除非我们手里有对应的钥匙,否则无法通过锁。
假设 k 为 钥匙/锁 的个数,且满足 1 <= k <= 6
,字母表中的前 k
个字母在网格中都有自己对应的一个小写和一个大写字母。换言之,每个锁有唯一对应的钥匙,每个钥匙也有唯一对应的锁。另外,代表钥匙和锁的字母互为大小写并按字母顺序排列。
返回获取所有钥匙所需要的移动的最少次数。如果无法获取所有钥匙,返回 -1
。
这道题是一个经典的迷宫型的问题,但是问题在于出现了一个钥匙的限制,也就是你来到一个位置,这个位置能不能走不完全取决于格子本身,而是取决于是否此前你走过的路,所以我们需要记录收集的钥匙状态,并把状态和点作为整体当作bfs的对象,直到状态变为收集了全部钥匙。
class Solution {
public:
int shortestPathAllKeys(vector<string>& grid) {
int mov[5]={1,0,-1,0,1};
int n=grid.size(),m=grid[0].size();
int cnt=0;
int x,y;
vector<vector<vector<bool>>> vis(n,vector<vector<bool>>(m,vector<bool> (300)));
for(int i=0;i<n;i++){
for(int j=0;j<m;j++){
if(grid[i][j]=='@'){
x=i,y=j;
}
if(grid[i][j]>='a'&&grid[i][j]<='z'){
cnt++;
}
}
}
int l=0,r=0,level=1;
vis[x][y][0]=true;
int ok=(1<<cnt)-1;
vector<vector<int>> queue;
queue.push_back({x,y,0});
r++;
while(l<r){
for(int size=r-l,k=0;k<size;k++){
x=queue[l][0];
y=queue[l][1];
int key=queue[l++][2];
for(int i=0;i<4;i++){
int cx=x+mov[i],cy=y+mov[i+1];
int gd=key;
if(cx<0||cx>=n||cy<0||cy>=m) continue;
if(vis[cx][cy][gd]) continue;
if(grid[cx][cy]=='#'||(grid[cx][cy]>='A'&&grid[cx][cy]<='Z'&&(gd&(1<<(grid[cx][cy]-'A')))==0)) continue;
if(grid[cx][cy]>='a'&&grid[cx][cy]<='z'){
gd|=1<<(grid[cx][cy]-'a');
}
if(gd==ok){
return level;
}
vis[cx][cy][gd]=true;
queue.push_back({cx,cy,gd});
r++;
}
}
level++;
}
return -1;
}
};
LCP 35.电动车游城市
小明的电动车电量充满时可行驶距离为 cnt
,每行驶 1 单位距离消耗 1 单位电量,且花费 1 单位时间。小明想选择电动车作为代步工具。地图上共有 N 个景点,景点编号为 0 ~ N-1。他将地图信息以 [城市 A 编号,城市 B 编号,两城市间距离]
格式整理在在二维数组 paths
,表示城市 A、B 间存在双向通路。初始状态,电动车电量为 0。每个城市都设有充电桩,charge[i]
表示第 i 个城市每充 1 单位电量需要花费的单位时间。请返回小明最少需要花费多少单位时间从起点城市 start
抵达终点城市 end
。
这道题就是经典的分层图加上djk的题目,首先每次我们来到一个城市能做的事情受限于当前的电量,所以很明显扩点就是把节点及到达节点时的电量当作元素加入堆中,那么如何处理充电的事呢,因为充电也要消耗代价,所以我们可以把充电看作一条特殊的路,也就是消耗了充电的时间,但是位置没有变化,且电量变多,但是这个地方有一个需要注意的地方,那就是我们并不是要把充到各种电量的情况全部枚举,虽然理论上我们确实应该列举充不同数量电量的情况,但是我们可以只充一格,至于要不要再充,我们可以在堆中弹出的时候再去研究要不要再冲,因为充一格电在消耗时间的方面是优于其他所有情况的(充电的情况),所以我们把充下一格电的任务交给再弹出的当前节点,这样直到到达终点就结束。
class Solution {
public:
int electricCarPlan(vector<vector<int>>& paths, int cnt, int start, int end, vector<int>& charge) {
int n=charge.size();
vector<vector<vector<int>>> gra(n);
vector<vector<int>> dis(n,vector<int>(cnt+1));
for(int i=0;i<n;i++){
for(int j=0;j<=cnt;j++){
dis[i][j]=INT_MAX;
}
}
vector<vector<bool>> vis(n,vector<bool>(cnt+1));
int a=paths.size();
for(int i=0;i<a;i++){
gra[paths[i][0]].push_back({paths[i][1],paths[i][2]});
gra[paths[i][1]].push_back({paths[i][0],paths[i][2]});
}
priority_queue<pair<int,pair<int,int>>,vector<pair<int,pair<int,int>>>,greater<>> pq;
pq.emplace(0,pair<int,int>(start,0));
dis[start][0]=0;
while(!pq.empty()){
auto t=pq.top();
pq.pop();
int d=t.first,power=t.second.second,h=t.second.first;
if(h==end){
return d;
}
vis[h][power]=true;
for(auto it=gra[h].begin();it!=gra[h].end();it++){
if(power>=(*it)[1]){
if(d+(*it)[1]<dis[(*it)[0]][power-(*it)[1]]){
if(!vis[(*it)[0]][power-(*it)[1]]){
dis[(*it)[0]][power-(*it)[1]]=d+(*it)[1];
pq.emplace(dis[(*it)[0]][power-(*it)[1]],pair<int,int>((*it)[0],power-(*it)[1]));
}
}
}
}
if(power<cnt){
if(!vis[h][power+1]&&d+charge[h]<dis[h][power+1]){
dis[h][power+1]=d+charge[h];
pq.emplace(d+charge[h],pair<int,int>(h,power+1));
}
}
}
return -1;
}
};
洛谷.飞行路线
Alice 和 Bob 现在要乘飞机旅行,他们选择了一家相对便宜的航空公司。该航空公司一共在 �n 个城市设有业务,设这些城市分别标记为 00 到 �−1n−1,一共有 �m 种航线,每种航线连接两个城市,并且航线有一定的价格。
Alice 和 Bob 现在要从一个城市沿着航线到达另一个城市,途中可以进行转机。航空公司对他们这次旅行也推出优惠,他们可以免费在最多 �k 种航线上搭乘飞机。那么 Alice 和 Bob 这次出行最少花费多少?
输入格式
第一行三个整数 �,�,�n,m,k,分别表示城市数,航线数和免费乘坐次数。
接下来一行两个整数 �,�s,t,分别表示他们出行的起点城市编号和终点城市编号。
接下来 �m 行,每行三个整数 �,�,�a,b,c,表示存在一种航线,能从城市 �a 到达城市 �b,或从城市 �b 到达城市 �a,价格为 �c。
输出格式
输出一行一个整数,为最少花费。
这道题跟上一道题有异曲同工之妙,一开始我理解错了题目,以为是免费乘坐前k个航线,但是实际上题目的意思是任意选择免单的航线,求出最便宜的方案,那么下一条路的花费当前取决于当前已经免单了几条航线,所以我们要把免单次数的状态扩点进入堆中,对于每一次选择都把免和不免的情况加入堆中,直到到达终点,当然不要忘了免单的时候更新免单次数。
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
void solve(){
int n,m,k;
cin>>n>>m>>k;
int s,end;
cin>>s>>end;
vector<vector<vector<int>>> gra(n);
vector<vector<int>> dis(n,vector<int>(k+1));
for(int i=0;i<n;i++){
for(int j=0;j<=k;j++){
dis[i][j]=INT_MAX;
}
}
vector<vector<bool>> vis(n,vector<bool>(k+1));
for(int i=0;i<m;i++){
int a,b,c;
cin>>a>>b>>c;
gra[a].push_back({b,c});
gra[b].push_back({a,c});
}
priority_queue<pair<int,pair<int,int>>,vector<pair<int,pair<int,int>>>,greater<>> pq;
pq.emplace(0,pair<int,int>(s,0));
dis[s][0]=0;
while(!pq.empty()){
auto t=pq.top();
pq.pop();
int d=t.first,cur=t.second.first,kk=t.second.second;
if(cur==end){
cout<<dis[end][kk];
return;
}
vis[cur][kk]=true;
for(auto it=gra[cur].begin();it!=gra[cur].end();it++){
int to=(*it)[0],w=(*it)[1];
if(!vis[to][kk]&&d+w<dis[to][kk]){
dis[to][kk]=d+w;
pq.emplace(d+w,pair<int,int>(to,kk));
}
if(kk<k&&!vis[to][kk+1]&&d<dis[to][kk+1]){
dis[to][kk+1]=d;
pq.emplace(d,pair<int,int>(to,kk+1));
}
}
}
}
int main()
{
solve();
return 0;
}
本篇简单总结了djk算法以及分层图最短路的一些题目,本文根据网络资料总结。
至此。