图的最短路径算法
dijkstral算法
dijkstral算法适用于无负权边的图。
邻接矩阵形式
图结构和辅助数组
const int INF = 0x3fffffff; // inf定义为了int的上限
// Cost为边权矩阵; weight为点权,若有则可以使用
int G[MAXN][MAXN], Cost[MAXN][MAXN], weight[MAXN];
// weight_cal若题目中要求点权的累计最大则可使用 cost_cal若题目中要求边权累计最大则可以使用
int dist[MAXN], weight_cal[MAXN], cost_cal[MAXN];
int path_num[MAXN]; // 路径条数
bool visited[MANX];
vector<int> path[MAXN]; // 求具体路径可以使用
算法模板(基于贪心思想)
void dijkstral(int start_node){
// fill函数需要包含algroithm头文件 若自己实现可以使用循环
fill(G[0], G[0] + MAXN * MAXN, INF);
fill(dist, dist + MAXN, INF);
fill(visited, visited + MAXN, 0);
fill(path_num, path_num + MAXN, 0);
fill(weight_cal, weight_cal + MAXN, 0);
fill(cost_cal, cost_cal + MAXN, 0);
dist[start_node] = 0;
weight_cal[start_node] = weight[start_node];
for(int i = 0; i < n; i++){
// n 为定点数 顶点编号默认从0~n
int u = -1, MAX_D = INF;
for(int j = 0; j < n; j++){
if(visited[j] == false && dist[j] < MAX_D){
u = j;
MAX_D = dist[j];
}
}
if(u == -1) break;
visited[u] = true;
// 更新距离
for(int v = 0; v < n; v++){
if(visited[v] == false && G[u][v] != INF){
if(dist[v] > dist[u] + G[u][v]){
dist[v] = dist[u] + G[u][v]; // 更新距离
path_num[v] = path_num[u]; // 更新路径数量
weight_cal[v] = weight_cal[u]; // 点券累计
cost_cal[v] = cost_cal[u];// 边权累计
// 具体路径
path[v].clear();
path[v].push_back(u);
} else if( dist[v] == dist[u] + G[u][v]){
path_num[v] += path_num[u];
weight_cal[v] = max(weight_cal[u] + weight[v], weight[v]);
cost_cal[v] = max(cost_cal[u] + Cost[u][v], cost_cal[v]);
path[v].push_back(u)
} // if..else if..
} // if
} // for...
}
}
若是嫌麻烦, 先用dijkstarl模板求出所有具体最短路径,再用dfs来求解题目所需要的那条路径
vector<int> temp, best_path;
void dfs(int u, int start_node){
temp.push_back(u);
if(u == start_node){
// 需要选择的一些条件
if(expression) best_path = temp;
} else {
for(auto v : path[u]){
dfs(v, start_node);
}
}
temp.pop_back(); // 退出该节点
}
邻接表形式
邻接表形式适用于MAXN大的情况,一般大于1000可以考虑使用邻接表。
借助STL中的vector容器可以定义图结构
struct node {
int v, dis; // v是顶点编号, dis为距离
}
vector<node> G[MAXN];
dijkstarl算法(改动很小)
void dijkstral(int start_node){
// fill函数需要包含algroithm头文件 若自己实现可以使用循环
fill(dist, dist + MAXN, INF);
fill(visited, visited + MAXN, 0);
fill(path_num, path_num + MAXN, 0);
fill(weight_cal, weight_cal + MAXN, 0);
fill(cost_cal, cost_cal + MAXN, 0);
dist[start_node] = 0;
weight_cal[start_node] = weight[start_node];
for(int i = 0; i < n; i++){
// n 为定点数 顶点编号默认从0~n
int u = -1, MAX_D = INF;
for(int j = 0; j < n; j++){
if(visited[j] == false && dist[j] < MAX_D){
u = j;
MAX_D = dist[j];
}
}
if(u == -1) break;
visited[u] = true;
// 更新距离
for(auto it : G[u]){
int v = it.v, dis = it.dis;
if(!visited[v]) {
if(dist[v] > dist[u] + G[u][v]){
dist[v] = dist[u] + G[u][v]; // 更新距离
path_num[v] = path_num[u]; // 更新路径数量
weight_cal[v] = weight_cal[u]; // 点券累计
cost_cal[v] = cost_cal[u];// 边权累计
// 具体路径
path[v].clear();
path[v].push_back(u);
} else if( dist[v] == dist[u] + G[u][v]){
path_num[v] += path_num[u];
weight_cal[v] = max(weight_cal[u] + weight[v], weight[v]);
cost_cal[v] = max(cost_cal[u] + Cost[u][v], cost_cal[v]);
path[v].push_back(u)
} // if .. else if ..
} // if ..
} // for ..
}// for ...
}
使用堆优化的Dijkstral算法
当邻接表形式的dikstral算法无法满足时间复杂度时,可以考虑使用堆优化来优化该算法。堆优化的本质是用小根堆的性质来代替下面这段代码
int u = -1, MAX_D = INF;
for(int j = 0; j < n; j++){
if(visited[j] == false && dist[j] < MAX_D){
u = j;
MAX_D = dist[j];
}
}
若手动建堆并每次删除维护,可以查看我的堆模板,使用downAdjust和insert来维护堆。
在STL中提供了好用的优先队列priority_queue, 可以用它来代替堆并让STL自动帮我们维护
定义节点结构,并且重载小于号<
struct node {
int dis, vector;
node(int a, int b) : dis(a), vector(b) {}; // 构造函数 可以不写
friend bool operator < (node a, node b) {
return a.dis > b.dis;
} // 重载了 小于号 <
/*
bool operator < (const node& a) const {
return dis > a.dis;
}
*/ // 这种写法也可以重载小于号, 认准一种就行
};
堆优化的算法
void dijkstral(int start) {
memset(visited, false, sizeof(visited));
fill(dist, dist + MAXN, INF);
priority_queue<node> q;
dist[start] = 0;
q.push(node(0, start)); // 初始入堆
while (!q.empty()) {
// 以下步骤代替了 for j : 0 to n 找u最小的过程
// 堆顶元素 默认就是最小的距离的顶点了
node now = q.top();
q.pop();
int u = now.v
if (visited[u]) continue; // 堆顶的元素可能访问过了
visited[u] = true;
for (int i= 0; i < G[u].size(); i++) { // 邻接表形式的图
int v = G[u][i].v;
if (visited[v] == false && dist[v] > dist[u] + G[u][v].dis) {
dist[v] = dist[u] + G[u][v].dis;
q.push(node(dist[v], v)); // 把这一对入堆
}
}
/* 临界矩阵版本
for (int v = 0; v < n; v++) {
if (visited[v] == false && g[u][v] != INF) {
if (dist[v] > dist[u] + g[u][v]) {
dist[v] = dist[u] + g[u][v];
q.push(node(dist[v], v));
}
}
}
*/
}
}
带负权边的最短路径算法——SPFA
在PAT中基本使用dijkstral算法即可满足题目要求,但如果题目出现负权边,我们可以使用Bellman Ford算法和SPFA算法,但Bellman Ford的时间复杂度相对于SPFA通常会来的更高些,必将SPFA优化Bellman Ford算法了,因此推荐使用SPFA
图定义和一些相关的辅助数组
struct node {
int v, dis;
};
vector<node> G[MAXN];
int num[MAXN], dist[MAXN];
bool inq[MAXN];
SPFA算法返回bool值,返回true代表没用负权变,false代表有负权变
对于有源点可达负权环的图,dijkstral可能会失效,如下所示
dist数组和visited数组的情况如下(S为源点)
顶点 | S | A | B | C |
---|---|---|---|---|
dist | 0 | 5 | INF | INF |
visited | true | true | false | false |
通过贪心策略更新dist数组
顶点 | S | A | B | C |
---|---|---|---|---|
dist | 0 | 5 | 4 | 6 |
visited | true | true | false | false |
然后会选择B顶点作为下一个顶点并访问它
顶点 | S | A | B | C |
---|---|---|---|---|
dist | 0 | 5 | 4 | 6 |
visited | true | true | true | false |
然后贪心更新dist数组(B不可到达C)
顶点 | S | A | B | C |
---|---|---|---|---|
dist | 0 | 5 | 4 | 6 |
visited | true | true | true | false |
最后访问C
顶点 | S | A | B | C |
---|---|---|---|---|
dist | 0 | 5 | 4 | 6 |
visited | true | true | true | true |
因此S到B的最短路径是4,但实际可以发现最短路径应该是A->C->B,距离为5+1-5 = 1
SPFA的思想其实很简单;
从某个源点出发到各个点的最短路径若存在一定会产生一棵最短路径树。
有两种最极端的情况,一种是这棵树的高度只有2层,即源点直接到所有点有路径且最短;另一种则是树的高度为n,类似于了一根链表。
现在考虑第一种情况,当最短路径存在时,访问次数最多的一定是源点,并且访问次数应该小于等于n-1次(通过该点去更新相邻点)。
因此我们并不设置visited数组,让顶点的访问可以是无限次。然后每一次访问顶点后更新dist数组,并且将该顶点的访问次数加1
由于负权环的存在,每次选择顶点会反复选择负边环上的顶点,因此负边上的顶点会反复出现。当出现次数大于等于n时,我们就可以认为存在负边环。
// SPAF最短路径算法
bool SPFA(int start){
memset(inq, 0, sizeof(inq));
memset(num, 0, sizeof(num));
fill(dist, dist + MAXN, INF);
queue<int> q; //使用队列来进行访问
q.push(start);
inq[start] = true; // inq数组来保证队列中的顶点唯一
num[start]++;
while(!q.empty()){
int u = q.front();
q.pop();
// 出队更新inq数组,方便后续再入队
// 如负边AB, A出队后inq[A] = false, 然后选择B入队。
// 当B被选择时,inq[B] = false, 然后A又会被选择入队。如此反复
inq[u] = false;
for(int i = 0; i < G[u].size(); i++){
int v = G[u][i].v, dis = G[u][i].dis;
if(dist[v] > dist[u] + dis){
dist[v] = dist[u] + dis;
// 把不在队内的顶点v入队
if(inq[v] == false){
q.push(v);
inq[v] = true;
num[v]++;
if(num[v] >= n) return false;
}
}
}
}
return true;
}