文章目录
ccf认证 201812-4 数据中心(spfa、dijkstra、kruskal、prim多种算法版本)
这道题有两类解法,分别是利用最小生成树和单源最短路径。其中最小生成树又可以用prim和kruskal两种算法,单源最短路径则可以使用dijkstra和spfa两种算法。索性借此机会将这几个图论中的基本、常用的算法总结一下,以后要用这几个算法的模板的时候就到这里来取。
最小生成树
这道题的最终解就是图的最小生成树的n-1条边中权重最大的那条,网上有一些证明方法,但在我看了的那些中,证明并不严谨,只知道应该和kruskal算法的内容有关。就我阅读《算法导论》的经验来看,图论的这些基本算法虽然不难写出来,但真的要严格证明算法正确性,是很难的,因此我索性不去深究其正确性了,这里只是想贴出代码、作为参考。
kruskal算法
我以前潜意识里以为kruskal算法实现起来很复杂,因为要以并查集为基础,而并查集的实现有要花时间。在积累了一些经验后发现,平常一般用到并查集,并不需要达到最高效率那种,只需要实现了路径压缩即可,而这很容易写出来。因此发现,原来kruskal算法竟如此简单,毕竟基于实现好的并查集,它不论是策略上还是实现上都很简易。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
//kruskal算法求最小生成树
//在main函数中,可以使用一些附加操作获取更多信息,比如记录下所选取的边
const int MAXV = 50010;
const int MAXE = 100010;
int n, m, root;
int fa[MAXV];
struct Edge{
int u, v, w;
Edge() { }
Edge(int _u, int _v, int _w): u(_u), v(_v), w(_w) { }
friend bool operator<(Edge a, Edge b){
return a.w < b.w;
}
}edges[MAXE];
int find(int s){ //并查集的查询操作,加入了路径压缩
if (s == fa[s])
return s;
return fa[s] = find(fa[s]);
}
int main(){
cin >> n >> m >> root;
int u, v, w;
for (int i = 1; i <= m; i++){
cin >> edges[i].u >> edges[i].v >> edges[i].w;
}
sort(edges + 1, edges + 1 + m);
for (int i = 1; i <= n; i++)
fa[i] = i;
int cnt, ans, a, b;
Edge it;
cnt = ans = 0;
for (int i = 1; i <= m; i++){
it = edges[i];
a = find(it.u);
b = find(it.v);
if (a == b) //如果组成环,则继续下一轮循环
continue;
fa[a] = b; //将左边树根接到右边树根上
cnt++; //找到一条有效边,计数+1
ans = it.w; //由于边的权重是递增的,所以这里直接赋值为it.w
if (cnt == n - 1) //当选取了达到n-1条边时,就组成了最小生成树,跳出循环
break;
}
cout << ans;
return 0;
}
prim算法
相较之下,我觉得kruskal算法实现起来更简单明了,总感觉prim算法有的地方有些隐晦、有点不放心的感觉。
不过要比较这两个算法的话还是从算法特点来比较的合理。kruskal算法更适合于稀疏图,而prim算法更适合稠密图。从题目的数据规模展示来看,感觉稀疏图多一些。
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>
using namespace std;
//注意点:
//1. PQ.empty()和cnt二者控制prim算法的结束,可根据不同结束方式判断图的连通性
//2. 由于prim算法中不会用到边Edge的起边u的信息,故不保存u
//3. 用以Edge为元素的二维向量G表示图,除此之外只用到了in和PQ这两个数据结构
//4. 虽然是用有向图的方法储存的无向图的信息,但是由于算法的性质,当其中一条边被使用了之后,
// 另一条自动失效了
struct Edge{ //最小生成树和单源最短路径算法中定义的边类是不同的,在于这里要包括起始边的信息
int v, w;
Edge() { }
Edge(int _v, int _w): v(_v), w(_w) { }
friend bool operator<(Edge a, Edge b){
return a.w > b.w; //注意,这里为了适应优先级队列方向是变了的
}
};
int n, m, root, ans;
const int MAXV = 50010;
const int MAXE = 100010;
vector<vector<Edge> > G(MAXV);
bool in[MAXV];
void prim(){
ans = 0;
fill(in, in + MAXV, false);
priority_queue<Edge> PQ;
in[root] = true;
for (auto it : G[root])
if (!in[it.v])
PQ.push(it);
int cnt = 0; //cnt记录的是加入最小生成树的边的数量
while (!PQ.empty()){ //可以在主函数中加入对cnt的判断,如果cnt!=n,意味着PQ空了还没生成树,意味着不连通
Edge e = PQ.top();
PQ.pop();
if (!in[e.v]){ //若in[e.v]==false,则边e是一条合格边(被加入最小生成树中)
//由于PQ里面的边的起点一定是在点集里面的,故只需判断终点即可
in[e.v] = true;
cnt++;
ans = max(ans, e.w);
for (auto it : G[e.v])
if (!in[it.v])
PQ.push(it);
}
if (cnt == n - 1) //当cnt==n-1时退出循环
break;
}
}
int main(){
cin >> n >> m >> root;
int u, v, w;
for (int i = 1; i <= m; i++){
cin >> u >> v >> w;
G[u].push_back(Edge(v, w));
G[v].push_back(Edge(u, w)); //无向边当有向边处理
}
prim();
cout << ans;
return 0;
}
单源最短路径
做这道题目我一开始使用的是dijkstra算法,其中需要在原始的dijkstra算法的基础上稍作变形,因为这里以root为源,其到达其他结点的路径长度不是一般的路径上的权重相加,而是将路径上权重最大的边的权重作为路径的长度。对于其正确性,这里不做详细证明,只提供一个帮助理解的方法:假设有一条路径r->u->v
,将u->v
的权重看作路径r->u->v
上最大权重减去路径r->u
上的最大权重,由于前者包含后者,因此这个差值一定为非负数,这就意味着将路径上的最大权重作为路径长度的问题本质上也是一般的单源最短路径问题,且边权非负。
dijkstra算法
要解决这个问题只需要在基本的dijkstra算法基础上稍作修改,即在进行路径松弛时修改。
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
//使用了优先级队列优化的dijkstra算法
struct edge{
int v;
int weight;
edge() { }
edge(int _v, int _weight): v(_v), weight(_weight) { }
};
struct node{
int id;
int dist;
node() { }
node(int _id, int _dist): id(_id), dist(_dist) { }
friend bool operator<(node a, node b){ //使用friend关键字
return a.dist > b.dist; //与常规相反,可以实现优先级队列中值小的优先的目的
}
};
int n, m, root;
const int MAXV = 500010;
const int INF = 0x3fffffff;
vector<vector<edge> > Adj(MAXV);
int d[MAXV];
bool vis[MAXV] = {false};
void dijkstra(){
fill(d, d + MAXV, INF);
d[root] = 0;
priority_queue<node> PQ;
for (int i = 1; i <= n; i++)
PQ.push(node(i, d[i]));
int u;
for (int i = 1; i < n; i++){
while (vis[u = PQ.top().id])
PQ.pop();
PQ.pop();
vis[u] = true;
if (d[u] == INF)
return;
edge it;
for (int j = 0; j < Adj[u].size(); j++)
if (!vis[(it = Adj[u][j]).v] && max(d[u], it.weight) < d[it.v]){
//和基础的dijkstra算法不同的地方
d[it.v] = max(d[u], it.weight);
PQ.push(node(it.v, d[it.v]));
}
}
}
int main(){
cin >> n >> m >> root;
int u, v, w;
for (int i = 1; i <= m; i++){
cin >> u >> v >> w;
Adj[u].push_back(edge(v, w));
Adj[v].push_back(edge(u, w)); //dijkstra算法处理有向边,因此这里要将无向边当作有向边处理
}
dijkstra();
int m = 0;
for (int i = 1; i <= n; i++)
m = max(m, d[i]);
cout << m;
return 0;
}
spfa算法
下面这个程序和上面的dijkstra算法的程序大体上很相近,不同的是dijkstra函数换成了spfa函数,且后者多了个inq数组、少了个vis数组,且后者由于不需要使用优先级队列,故不需要定义结构体node。如此看来,spfa甚至比dijkstra更容易实现,效率也相差无几。
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
//spfa算法
struct edge{
int v, w;
edge() { }
edge(int _v, int _w): v(_v), w(_w) { }
};
int n, m, root;
const int MAXV = 50010;
const int INF = 0x3fffffff;
vector<vector<edge> > Adj(MAXV);
int d[MAXV];
bool inq[MAXV];
void spfa(){
fill(d, d + MAXV, INF);
fill(inq, inq + MAXV, false);
d[root] = 0;
queue<int> Q;
Q.push(root);
inq[root] = true;
int u;
while (!Q.empty()){
u = Q.front(); //注意是front不是top
Q.pop();
inq[u] = false;
for (auto it : Adj[u]){
if (max(d[u], it.w) < d[it.v]){
d[it.v] = max(d[u], it.w);
if (!inq[it.v]){
inq[it.v] = true;
Q.push(it.v);
}
}
}
}
}
int main(){
cin >> n >> m >> root;
int u, v, w;
for (int i = 1; i <= m; i++){
cin >> u >> v >> w;
Adj[u].push_back(edge(v, w));
Adj[v].push_back(edge(u, w)); //单源最短路径算法处理有向边,因此这里要将无向边当作有向边处理
}
spfa();
int m = 0;
for (int i = 1; i <= n; i++)
m = max(m, d[i]);
cout << m;
return 0;
}