(仿佛回到了当年打比赛的时候呢
传送门:http://poj.org/problem?id=3013
题目大意:由一堆顶点和边构造出一棵圣诞树,1号顶点固定为树根,顶点和边各自有权重值(均为正数)。构造圣诞树的边的开销是边权乘以子树中所有顶点的权重之和,总开销则是所有边的开销之和。求圣诞树的最小开销。
稍微变换一下,容易得知构造圣诞树的最小开销是:
Σ ( 某顶点到1号顶点的最小距离 * 该顶点的权重值 )
故这道题是个纯粹的单源最短路问题。
说起单源最短路,我们可以很快想起图论课程一定会教的两个经典算法,即Bellman-Ford算法和Dijkstra算法。本文只讨论后者,前者(以及它的优化版本SPFA)之后有时间再说,毕竟今天是Christmas Eve,不想写太多咯。
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算法做优化,下面来看。
重新看一下上面的代码,可以发现遍历顶点并找出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值的结构体,并重载<运算符使其变成最小堆,如下。
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算法之外,斐波那契堆、配对堆也都可以,并且效率会更高。但最小堆一般都够用了,并且笔者之前没有介绍过斐波那契堆和配对堆,它们俩还是有点难理解的,因此就不强行在这里讲了,择日介绍吧。
Happy Christmas Eve~
民那晚安晚安。