题目13.2链接Minimum Spanning Tree
二者均采用邻接矩阵
第一个采用STL简化了代码(思路直接来源于Prim):
#include
#include
using namespace std;
const int Max = 105;
const int INF = (1<<29);
int n, G[Max][Max], Min;
set<int> T;
set<int> V;
//T为MST的顶点,V为剩余的顶点
void Pime0(int u)
{
T.insert(u);
V.erase(u);
set<int>::iterator i, j;
while(T.size()<n)
{
int minv = INF;
int index = -1;
for(i=T.begin(); i!=T.end(); i++)//寻找T到V的最短边
{
for(j=V.begin(); j!=V.end(); j++)
{
if(G[*i][*j]!=INF && minv>G[*i][*j])
{
minv = G[*i][*j];
index = *j;
}
}
}
T.insert(index);
V.erase(index);
Min += minv;
}
}
int main()
{
cin >> n;
Min = 0;
for(int i=0; i<n; i++)
{
V.insert(i);
}
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
{
cin >> G[i][j];
if(G[i][j]==-1)
G[i][j] = INF;
}
}
Pime0(0);
cout << Min << endl;
return 0;
}
第二种方法:变量如下设置
以下表示中T表示已经成为MST的结点集合,V表示所有结点集合。
变量 | 含义 |
---|---|
color[n] | color[v]用于记录v的访问状态,0表示结点在MST中,-1表示不在 |
G[n][n] | 邻接矩阵,G[u][v]中记录u到v的权值 |
d[n] | d[v]用于记录连接T内顶点与V-T内顶点(v)的边中,权值最小的边的权值 |
p[n] | p[v]用于记录MST中顶点v的父节点,方便重建MST |
先找T到V-T的最短边(通过的d[n])及其顶点(v),将v加入T后,然后更新V-T中的最短边数组d[n]
算法复杂度O(N^2)
#include
#include
using namespace std;
const int Max = 105;
const int INF = (1<<29);
int n, G[Max][Max], Min, color[Max], d[Max], p[Max];
void Prim()
{
memset(color, -1, sizeof(color));
for(int i=0; i<n; i++)
{
d[i] = INF;
}
d[0] = 0;
p[0] = -1;
while(1)
{
int minv = INF, u;
for(int i=0; i<n; i++)//找到T到V-T中的最短边的顶点
{
if(color[i]!=0 && d[i]<minv)
{
minv = d[i];
u = i;
}
}
if(minv==INF) break;
color[u] = 0;//MST中加入u
Min += minv;
for(int j=0; j<n; j++)//当MST加入u后,更新d[v]
{
if(color[j]!=0 && G[u][j]!=INF && G[u][j]<d[j])
{
d[j] = G[u][j];
p[j] = u;//j的父节点为u,有利于重建MST
}
}
}
}
int main()
{
cin >> n;
Min = 0;
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
{
cin >> G[i][j];
if(G[i][j]==-1)
G[i][j] = INF;
}
}
Prim();
cout << Min << endl;
return 0;
}
题目15.5链接Minimum Spanning Tree
采用Kruskal算法,从排好序(升序)的边集合中选择不会产生回路的边,直到边的个数等于顶点个数减一。
这个方法主要采用了互质集合(Union-Find)的高等数据结构,具体讲解见acm之旅–高等数据结构。算法复杂度为O(|E|log|E|)。
代码如下:
#include
#include
#include
#include
using namespace std;
struct Node//边的数据结构体
{
int source, target, value;
};
bool cmp(Node &a, Node &b)
{
return a.value<b.value;
}
const int MAX = 100005;
int V, E, MIN = 0;
Node e[MAX];
vector<Node> Mst;//最小生成树
class DisjointSet
{
public:
vector<int> rank, p;//rank记录集合树高,rank[x],则x为集合根,p指向父节点
DisjointSet(){}
DisjointSet(int size)
{
rank.resize(size, 0);
p.resize(size, 0);
for(int i=0; i<size; i++)
{
makeSet(i);
}
}
void makeSet(int x)//创建仅包含元素x的新集合
{
p[x] = x;
rank[x] = 0;
}
bool same(int x, int y)//判断x和y是否在同一集合
{
return findSet(x) == findSet(y);
}
void unite(int x, int y)//合并指定的元素及其集合
{
link(findSet(x), findSet(y));
}
void link(int x, int y)//合并函数
{
if(rank[x]>rank[y])//根据树的高度来确定合并策略
{
p[y] = x;
}
else
{
p[x] = y;
if(rank[x]==rank[y])
{
rank[y]++;
}
}
}
int findSet(int x)//寻找x的集合的根
{
if(x!=p[x])
{
p[x] = findSet(p[x]);
}
return p[x];
}
};
void kruskal()
{
DisjointSet ds = DisjointSet(V);//建立互质的集合
sort(e, e+E, cmp);//为边排序
for(int i=0; i<E; i++)
{
if(!ds.same(e[i].source, e[i].target))//如果两点不连通
{
ds.unite(e[i].source, e[i].target);
MIN += e[i].value;
Mst.push_back(e[i]);//记录最小生成树的边
if(Mst.size()==V-1) break;//数的边个数等于顶点数减一
}
}
cout << MIN << endl;
}
int main()
{
scanf("%d %d", &V, &E);
for(int i=0; i<E; i++)
{
cin >> e[i].source >> e[i].target >> e[i].value;
}
kruskal();
return 0;
}
参考博文:算法(五):图解贝尔曼-福特算法
算法复杂度为O(VE),E和V分别为边数和点数。
可以解决的问题
思想:使用一个松弛条件不断更新相应的最短距离,直到没有结点需要更新为止。
代码:
typedef long long LL;
const int MAX = 100000;
const int INF = 1<<29;
struct Edge
{
int from, to, cost;
};
//N是结点个数,E是边的个数
int N, E;
Edge e[MAX];
LL d[MAX];//存储最短路径长度
//找到各个结点的最短路径,并判断是否存在负环
bool Bellman_Ford()
{
fill(d, d+MAX, INF);
d[1] = 0;
int k = 0;
while(1)
{
bool updated = false;
for(int i=0; i<E; i++)//遍历所有的边
{
Edge w = e[i];
if(d[w.from]!=INF && d[w.to]>d[w.from]+w.cost)
{
d[w.to] = d[w.from]+w.cost;
updated = true;
}
}
if(!updated) break;//没有更新则立即停止
k++;
if(k>=N) return false;//判断是否有负环
}
return true;
}
//单纯判断是否存在负环
bool find_negative_loop()
{
//memset(d, 0, sizeof(d));
fill(d, d+MAX, INF);
d[1] = 0;
for(int i=1; i<=N; i++)
{
for(int j=0; j<E; j++)
{
Edge w = e[j];
if(d[w.to]>d[w.from]+w.cost)
{
d[w.to]=d[w.from]+w.cost;
if(i==N) return true;//第N次仍然更新
}
}
}
return false;
}
参考博文:SPFA 算法详解(最短路径)
实际上就是FIFO队列+Bellman-Ford算法,使得最差情况复杂度为O(VE)E和V分别为边数和点数。
核心代码:
bool SPFA(int s,int n)
{
queue <int> q;
memset(vis,inf,sizeof(vis));
memset(ven,0,sizeof(ven));//标记一个结点是否在队列中
memset(nums,0,sizeof(nums));//记录一个结点进入队列的次数
vis[s]=0;//初始化距离
ven[s]=1,nums[s]++;//标记s节点在队列,队列次数+1
q.push(s);
while(!q.empty())
{
int x=q.front();
q.pop();//出队
ven[x]=0;//标记不在队列
for(int i=pre[x]; ~i; i=a[i].next)//遍历与x节点连通的点
{
int y=a[i].y;
if(vis[y]>vis[x]+a[i].time)//更新
{
vis[y]=vis[x]+a[i].time;
if(!ven[y])
//由于更新了节点,所以后续以这个为基础的最短路,也要更新下
//所以如果在队列就不用加入,不在的话加入更新后续节点
{
q.push(y);
ven[y]=1;//标记这个节点在队列中
nums[y]++;//记录加入次数
if(nums[y]>n)//如果这个点加入超过n次,说明存在负圈,直接返回
return false;
}
}
}
}
return true;
}
第一种与prim算法类似,相当于改变了d数组的含义,其他的都不变。该方法使用邻接矩阵,复杂度为O(N^2)。
Dijkstra算法为什么不能用于负权图
变量如下设置
变量 | 含义 |
---|---|
color[n] | color[v]用于记录v的访问状态,0表示该结点的最短路已经确定,-1表示还没有确定 |
G[n][n] | 邻接矩阵,G[u][v]中记录u到v的权值 |
d[n] | d[v]用于记录起点s到v的最短路径成本 |
p[n] | p[v]用于记录最短路径中顶点v的父节点,方便重建最短路径 |
#include
#include
using namespace std;
const int Max = 105;
const int INF = (1<<29);
int n, G[Max][Max], Min, color[Max], d[Max], p[Max];
int u, k, v, c;
void dijstra()
{
memset(color, -1, sizeof(color));
for(int i=0; i<n; i++)
{
d[i] = INF;
}
d[0] = 0;
p[0] = -1;
while(1)
{
int minv = INF, u;
for(int i=0; i<n; i++)//找到s到V-T中的最短路径的顶点
{
if(color[i]!=0 && d[i]<minv)
{
minv = d[i];
u = i;
}
}
if(minv==INF) break;
color[u] = 0;
for(int j=0; j<n; j++)
{
if(color[j]!=0 && G[u][j]!=INF && d[u]+G[u][j]<d[j])//使用新获得的结点更新最短路径
{
d[j] = G[u][j]+d[u];
p[j] = u;//j的父节点为u,有利于重建最短路径
}
}
}
//打印输出
for(int i=0; i<n; i++)
{
cout << i << " " << d[i] << endl;
}
}
int main()
{
cin >> n;
Min = 0;
for(int i=0; i<n; i++)
{
for(int j=0; j<n; j++)
{
G[i][j] = INF;
}
}
for(int i=0; i<n; i++)
{
cin >> u >> k;
for(int i=0; i<k; i++)
{
cin >> v >> c;
G[u][v] = c;//化为邻接矩阵
}
}
dijstra();
return 0;
}
第二种方法:该方法采用优先级队列(堆)+邻接表来实现,使得复杂度降为O((|V|+|E|)log(|V|)),即选最短路径使用优先级队列(O(|V|log|V|),更新采用邻接表(O(|E|log|V|)。
思路与第一种一样,就是使用STL和邻接表优化了一下。
#include
#include
#include
#include
#include
using namespace std;
const int Max = 10005;
const int INF = (1<<29);
struct Node
{
int v, c;//v表示结点编号,c表示结点距离
bool operator < (const Node& a) const{
return c > a.c;
}
};
int n, color[Max], d[Max], p[Max];
int u, k, v, c;
vector<Node> G[Max];
priority_queue<Node> pq;
Node pn;
void dijstra()
{
memset(color, -1, sizeof(color));
for(int i=0; i<n; i++)
{
d[i] = INF;
}
d[0] = 0;
p[0] = -1;
pn = {0, d[0]};
pq.push(pn);
while(!pq.empty())
{
//找到最短路径的点
pn = pq.top(); pq.pop();
int u = pn.v;
color[u] = 0;
if(d[u]<pn.c) continue;//如果得到的最小值比目前的最短路径还大,则忽略//不是必须的
for(int j=0; j<G[u].size(); j++)//更新与u相邻的结点的最短路径
{
int v = G[u][j].v;
if(color[v]!=0 && d[u]+G[u][j].c<d[v])
{
d[v] = G[u][j].c+d[u];
pn = {v, d[v]};
pq.push(pn);
p[j] = u;//j的父节点为u,有利于重建MST
}
}
}
//打印输出
for(int i=0; i<n; i++)
{
if(d[i]==INF)
cout << i << " " << -1 << endl;
else
cout << i << " " << d[i] << endl;
}
}
int main()
{
cin >> n;
for(int i=0; i<n; i++)
{
cin >> u >> k;
for(int i=0; i<k; i++)
{
cin >> v >> c;
Node pn = {v, c};
G[u].push_back(pn);
}
}
dijstra();
return 0;
}
链接:acm之旅–高等图算法