dij算法堆优化_Dijkstra算法与其最小堆优化

(仿佛回到了当年打比赛的时候呢

POJ 3013(Big Christmas Tree)

题目大意:由一堆顶点和边构造出一棵圣诞树,1号顶点固定为树根,顶点和边各自有权重值(均为正数)。构造圣诞树的边的开销是边权乘以子树中所有顶点的权重之和,总开销则是所有边的开销之和。求圣诞树的最小开销。

稍微变换一下,容易得知构造圣诞树的最小开销是:

Σ ( 某顶点到1号顶点的最小距离 * 该顶点的权重值 )

故这道题是个纯粹的单源最短路问题。

说起单源最短路,我们可以很快想起图论课程一定会教的两个经典算法,即Bellman-Ford算法和Dijkstra算法。本文只讨论后者,前者(以及它的优化版本SPFA)之后有时间再说,毕竟今天是Christmas Eve,不想写太多咯。

朴素的Dijkstra算法

Dijkstra算法本质上是贪心+BFS的策略,即以源点为起始,不断探测相邻的顶点,每次都选择当前路径最短的那个顶点,并对该顶点的所有出边做松弛操作,各个顶点的最短路径就会一直扩散下去,直到所有顶点都处理完毕。

下面来描述一下它。设图G=(V,E)是一张带权有向图。声明一个数组dist,dist[v]表示当前从源点s开始到顶点v的最短路径长度(初始化dist[s]为0,其他为无穷大)。Dijkstra算法可以用如下伪码表示。

function dijkstra(G, s):

// 初始化

create vertex set V

create current min-distance array dist[]

foreach vertex v of G:

add v to V

dist[v] = INFINITY

dist[s] = 0

while V is not empty:

// 选择未处理的顶点集合V中dist最小的那个顶点

u = vertex in V with MINIMUM dist[u]

remove u from V

// 遍历所有出边顶点

foreach out-edge vertex v of u:

// 松弛,其实就是判断A经由B到C的路径以及A直接到C的路径哪个短

alt_dist = dist[u] + length(u, v)

if alt_dist < dist[v]:

dist[v] = alt_dist

return dist

英文维基上给了一个很好的GIF来描述Dijkstra算法的执行过程。

需要注意,Dijkstra算法无法处理带负权边的图,即边“长度”小于0的图。由于该算法的贪心性质,它“看不到”远处的负权边,所以会破坏松弛操作的正确性(加上负权边之后本来已经确定的最短路径就不再是最短的了),得出的结果就是错的。特别地,如果图里存在负权环,那么Dijkstra算法就会陷入死循环出不来。所以,只要题目里存在负权边,我们就必须换用Bellman-Ford/SPFA算法。(没有负权边就别用SPFA了,SPFA已经被各种竞赛卡得不要不要的了)

算法介绍完了,来学以致用,做一下上面那道圣诞树的题目吧。代码如下。

#include

#include

#include

using namespace std;

typedef long long ll;

const int MAXN = 50010;

const ll INF = 1e18;

int t, nv, ne, a, b, c;

int edgeNum;

int weight[MAXN];

bool visited[MAXN];

ll dist[MAXN];

struct Edge {

int next, to, weight;

};

Edge edges[MAXN * 2];

int head[MAXN];

inline void addEdge(int from, int to, int weight) {

edges[++edgeNum].weight = weight;

edges[edgeNum].to = to;

edges[edgeNum].next = head[from];

head[from] = edgeNum;

edges[++edgeNum].weight = weight;

edges[edgeNum].to = from;

edges[edgeNum].next = head[to];

head[to] = edgeNum;

}

void dijkstra() {

for (int i = 0; i <= nv; i++) {

dist[i] = INF;

}

dist[1] = 0;

memset(visited, 0, sizeof(visited));

for (int i = 1; i <= nv; i++) {

int u = -1;

ll minDist = INF;

for (int j = 1; j <= nv; j++) {

if (!visited[j] && dist[j] < minDist) {

minDist = dist[j];

u = j;

}

}

if (u == -1) {

break;

}

visited[u] = true;

for (int e = head[u]; e != 0; e = edges[e].next) {

int v = edges[e].to;

if (!visited[v] && dist[u] + edges[e].weight < dist[v]) {

dist[v] = dist[u] + edges[e].weight;

}

}

}

}

int main() {

scanf("%d", &t);

while (t--) {

memset(head, 0, sizeof(head));

edgeNum = 0;

scanf("%d%d", &nv, &ne);

for (int i = 1; i <= nv; i++) {

scanf("%d", &weight[i]);

}

for (int i = 1; i <= ne; i++) {

scanf("%d%d%d", &a, &b, &c);

addEdge(a, b, c);

}

dijkstra();

ll result = 0;

for (int i = 1; i <= nv; i++) {

if (dist[i] == INF) {

result = -1;

break;

}

result += weight[i] * dist[i];

}

if (result == -1) {

printf("No Answer\n");

} else {

printf("%lld\n", result);

}

}

return 0;

}

由于边的花费和节点的权重值都能达到216,所以dist数组和结果值要乖乖用long long。另外,这里采用链式前向星来存储图,它是一种介于邻接表和邻接矩阵之间的结构,在竞赛中很常用。关于链式前向星的细节请参考原作者Malash的这篇文章(orz)。

好了,提交一下。恭喜~我们得到了华丽丽热气腾腾的TLE。

从朴素Dijkstra算法的实现可知,它的时间复杂度是O(|E|+|V|2)=O(|V|2)。而题目给出的|V|最大可达50000,时间限制为3000ms,显然是非常紧张的。所以不管是在竞赛中还是在实际应用中,都会对朴素Dijkstra算法做优化,下面来看。

最小堆优化的Dijkstra算法

重新看一下上面的代码,可以发现遍历顶点并找出dist最小的点的过程效率很低,就是下面这一小段。

for (int i = 1; i <= nv; i++) {

int u = -1;

ll minDist = INF;

for (int j = 1; j <= nv; j++) {

if (!visited[j] && dist[j] < minDist) {

minDist = dist[j];

u = j;

}

}

// ....

那么能不能维护住这些dist最小的点,不必每次都去O(n2)地扫呢?答案自然是可以的,用最小堆就完事了,C++ STL自带有优先队列priority_queue。看官可以阅读笔者之前写的《二叉堆、优先队列与Top-N问题》这篇文章获得一点基础知识。

具体实现起来,还有两点要注意:

要维护好顶点下标到dist值的映射;

C++的priority_queue与Java的PriorityQueue相反,默认是最大堆。

我们可以定义顶点下标与dist值的结构体,并重载

struct QNode {

int vno, dist;

bool operator < (const QNode &x) const {

return x.dist < dist;

}

};

接下来改写dijkstra()方法,轻松愉快了。将原版那个耗时的for循环换成从优先队列中pop出dist值最小的顶点,其他一切照常。

#include

void dijkstra() {

priority_queue q;

for (int i = 0; i <= nv; i++) {

dist[i] = INF;

}

dist[1] = 0;

memset(visited, 0, sizeof(visited));

QNode tn, qn;

tn.vno = 1;

tn.dist = 0;

q.push(tn);

while (!q.empty()) {

tn = q.top();

q.pop();

int u = tn.vno;

if (visited[u]) {

continue;

}

visited[u] = true;

for (int e = head[u]; e != 0; e = edges[e].next) {

int v = edges[e].to;

if (!visited[v] && dist[u] + edges[e].weight < dist[v]) {

dist[v] = dist[u] + edges[e].weight;

qn.vno = v;

qn.dist = dist[v];

q.push(qn);

}

}

}

}

提交,成功AC。

最小堆优化的Dijkstra算法时间复杂度可以改进到O[(|E|+|V|)log|V|],对于稀疏图(即|E| << |V|2的图)尤为有效。

事实上,除了用最小堆优化Dijkstra算法之外,斐波那契堆、配对堆也都可以,并且效率会更高。但最小堆一般都够用了,并且笔者之前没有介绍过斐波那契堆和配对堆,它们俩还是有点难理解的,因此就不强行在这里讲了,择日介绍吧。

The End

Happy Christmas Eve~

民那晚安晚安。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值