链式前向星是图的一种静态链表存储,用边集数组和邻接表相结合,
可以快速访问一个顶点的所有邻接点。
链式前向星和邻接表一样,采用头插法进行链接,所以,边输入的顺序不同,
创建的链式前向星也不同;对于无向图,可通过与1异或得到反向边。
链式前向星包含两种结构:
(1)边集数组: edge[], edge[i]表示第i条边;
(2)头结点组数: head[], head[i]存以i为起点的第一条边的下标(在edge[]中的下标)
struct node {
int to;
int next;
int w;
} edge[MAXN*MAXN];
int head[MAXN];
添加一条边(u, v, w)
void add(int u, int v, int w)
{
edge[cnt].to = v;
edge[cnt].w = w;
edge[cnt].next = head[u]; // 采用头插法
head[u] = cnt++;
}
如果是有向图,每输入一条边,执行一次add(u, v, w)即可;
如果是无向图,则需要执行两次{add(u, v, w); add(v, u, w)}
对一个有向无环图G进行拓扑排序,是将G中所有顶点排成一个线性序列,
使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。
拓扑排序可以用来判断图中是否有环。
拓扑排序基本思想
(1)从有向图中选一个入度为0的顶点输出
(2)将此顶点和以它为起点的弧删除
(3)重复(1)和(2),直到不存在无前驱的顶点
算法:
(1)求所有顶点的入度
(2)把所有入度为0的顶点入队列
(3)当队列不空时
a. 取队列顶点为u,输出顶点u
b. 顶点u的所有邻接点入度-1,如果有入度为0的顶点,则入队列

#include <queue>
#include <vector>
#include <iostream>
const unsigned int MAXVER = 10;
std::vector<int> adj[MAXVER];
unsigned int indegree[MAXVER];
unsigned int MAXID = 0;
bool topsort()
{
std::queue<int> qu;
for (unsigned int i = 1; i <= MAXID; ++i)
{
if (indegree[i] == 0)
qu.push(i);
}
std::vector<int> ans;
while (!qu.empty())
{
int tmp = qu.front();
qu.pop();
//std::cout << tmp << " ";
ans.push_back(tmp);
int size = adj[tmp].size();
for (int t = 0; t < size; ++t)
if (--indegree[adj[tmp][t]] == 0)
qu.push(adj[tmp][t]);
}
if (ans.size() == MAXID)
{
for (int j = 0; j < MAXID; ++j)
std::cout << ans[j] << " ";
std::cout << std::endl;
return true;
}
else
return false;
}
int main()
{
int arr[][2] = { {1, 2}, {1, 3}, {1, 4}, {2, 4}, {2, 5}, {3, 6}, {4, 3}, {5, 4}, {5, 7}, {7, 6} };
int num = sizeof(arr) / sizeof(int) / 2;
for (int i = 0; i < num; ++i)
{
std::cout << arr[i][0] << "->" << arr[i][1] << std::endl;
adj[arr[i][0]].push_back(arr[i][1]);
++indegree[arr[i][1]];
if (arr[i][0] > MAXID)
MAXID = arr[i][0];
if (arr[i][1] > MAXID)
MAXID = arr[i][1];
}
topsort();
return 0;
}
1->2
1->3
1->4
2->4
2->5
3->6
4->3
5->4
5->7
7->6
1 2 5 4 7 3 6
Dijkstra 算法核心思想:
将结点分成两个集合:
已确定最短路长度的点集(记为S集合)的和
未确定最短路长度的点集(记为 V-S 集合)。
一开始所有的点都属于 V-S 集合。
初始化distTo[s] = 0.0;,其他点的最短路径均为无穷大。
然后重复如下操作:
(1)从 V-S 集合中,选取一个最短路长度最小的结点,移到S集合中。
(2)对那些刚刚被加入 S 集合的结点的所有出边执行松弛操作。
直到 V-S 集合为空为止。
使用优先队列维护(1)操作中最短路长度最小的结点,
如果同一个点的最短路被更新多次,
因为先前更新时插入的元素不能被删除,也不能被修改,只能留在优先队列中,
故优先队列内的元素个数是O(E) 的,时间复杂度为O(E*log(E)) 。
Djikstra 算法,从bfs演化而来,优先队列的bfs,
我们每次寻找的点就是当下最好的,贪心地扩大图中的点,
每次找到的tmp就是在vis集合中能到达的且离源点s最近的点,它的最短路就可以确认。
一个点可以进队多次,但是取出来只有剩下的在队伍中作废(因为vis置为1了)。
最短路径算法通常依赖于一个性质,一条两结点间的最短路径包含路径上的其他的最短路径。Dijkstra算法不允许图中存在负权边
1.单源最短路径:从某点s到其他所有结点的最短路径
2.松弛技术:松弛边(u,v),检测当前从s到v的最优路径是否有必要经过s到u,如有必要,则取边(u,v)
G = (V,E) where
V is a set of vertices and
E is a set of edges.
Dijkstra's algorithm keeps two sets of vertices:
S the set of vertices whose shortest paths from the source have already been determined and
V-S the remaining vertices.
(1) Set S to empty,
(2)While there are still vertices in V-S,
a. Sort the vertices in V-S according to the current best estimate of their distance from the source,
b. Add u, the closest vertex in V-S, to S,
c. Relax all the vertices still in V-S connected to u
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <vector>
#include <list>
#include <queue>
#include <stack>
#include <string>
using std::string;
using std::priority_queue;
using std::vector;
using std::stack;
using std::list;
using std::pair;
using std::make_pair;
//using std::greater;
using std::cout;
using std::endl;
using namespace std;
const double MAX = 10000.0;
class DirectedEdge
{
public:
DirectedEdge(int u, int v, double w)
{
if (u >= 0 && v >= 0)
{
this->u = u;
this->v = v;
weight = w;
}
}
int from()
{
return u;
}
int to()
{
return v;
}
double getWeight()
{
return weight;
}
string toString()
{
char result[64] = "";
sprintf_s(result, "%d -- %d: %8.2f\n", u, v, weight);
string str = result;
return str;
}
private:
int u;
int v;
double weight;
};
class EdgeWeightedDigraph
{
public:
EdgeWeightedDigraph(int nVertex, int nEdge, int arr[][2], double weight[]);
~EdgeWeightedDigraph();
list<DirectedEdge *> getAdj(int v);
list<DirectedEdge *> edges();
int getV() { return V; }
private:
int V;
int E;
list<DirectedEdge *> *adj;
};
EdgeWeightedDigraph::EdgeWeightedDigraph(int nVertex, int nEdge, int arr[][2], double weight[])
{
V = nVertex;
E = nEdge;
adj = new list<DirectedEdge *>[nVertex];
for (int i = 0; i < nEdge; ++i)
{
DirectedEdge *e = new DirectedEdge(arr[i][0], arr[i][1], weight[i]);
adj[arr[i][0]].push_back(e);
}
}
EdgeWeightedDigraph::~EdgeWeightedDigraph()
{
list<DirectedEdge *> ls = edges();
for (list<DirectedEdge *>::iterator it = ls.begin(); it != ls.end(); ++it)
{
delete *it;
*it = NULL;
}
delete[]adj;
adj = NULL;
}
list<DirectedEdge *> EdgeWeightedDigraph::getAdj(int v)
{
if (v >= 0 && v < V)
return adj[v];
}
list<DirectedEdge *> EdgeWeightedDigraph::edges()
{
list<DirectedEdge *> ls;
for (int i = 0; i < V; ++i)
{
list<DirectedEdge *> tmpList = getAdj(i);
for (list<DirectedEdge *>::iterator it = tmpList.begin(); it != tmpList.end(); ++it)
ls.push_back(*it);
}
return ls;
}
//greater<pair<int, double> >
//使用greater<>后,小根堆(升序),top()返回的是最小值而不是最大值
using Ty = std::pair<int, double>;
struct myGreater {
bool operator() (Ty a, Ty b) {
return a.second > b.second;
}
};
class DijkstraSP
{
public:
DijkstraSP(EdgeWeightedDigraph *g, int s);
~DijkstraSP();
double getDistTo(int v) { return distTo[v]; }
bool hasPathTo(int v) { return distTo[v] < MAX; }
stack<DirectedEdge *> pathTo(int v);
private:
void relax(DirectedEdge *e);
private:
bool *vis;
double *distTo; // distTo[v]=distance of shortest s --> v path
DirectedEdge **edgeTo;// edgeTo[v]=last edge on shortest s --> v path
//priority_queue<pair<int, double>, vector<pair<int, double> >, greater<pair<int, double> > > pq;
priority_queue<pair<int, double>, vector<pair<int, double> >, myGreater > pq;
};
DijkstraSP::DijkstraSP(EdgeWeightedDigraph *g, int s)
{
int v = g->getV();
distTo = new double[v];
edgeTo = new DirectedEdge *[v];
vis = new bool[v];
for (int i = 0; i < v; ++i)
{
distTo[i] = MAX;
edgeTo[i] = NULL;
vis[i] = false;
}
distTo[s] = 0.0;
// relax vertices in order of distance from s, 这里的relax还有很多改进的地方
pq.push(make_pair(s, distTo[s]));
while (!pq.empty())
{
int t = pq.top().first;
pq.pop();
if (vis[t])
continue;
vis[t] = true;
list<DirectedEdge *> ls = g->getAdj(t);
for (list<DirectedEdge *>::iterator it = ls.begin(); it != ls.end(); ++it)
{
relax(*it);
}
}
}
DijkstraSP::~DijkstraSP()
{
if (NULL != distTo)
{
delete[]distTo;
distTo = NULL;
}
if (NULL != edgeTo)
{
delete[]edgeTo;
edgeTo = NULL;
}
if (nullptr != vis)
{
delete[] vis;
vis = nullptr;
}
}
//To relax an edge u->v means to test whether the best known way from s to v is to go from s to u,
//then take the edge from u to v, and, if so, update our data structures
void DijkstraSP::relax(DirectedEdge *e)
{
int u = e->from();
int v = e->to();
double w = e->getWeight();
if (distTo[v] > distTo[u] + w)
{
distTo[v] = distTo[u] + w;
edgeTo[v] = e;
pq.push(make_pair(v, distTo[v]));
}
}
stack<DirectedEdge *> DijkstraSP::pathTo(int v)
{
stack<DirectedEdge *> path;
if (hasPathTo(v))
{
for (DirectedEdge *e = edgeTo[v]; e != NULL; e = edgeTo[e->from()])
{
path.push(e);
}
}
return path;
}
int main()
{
int arr[15][2] = { { 4, 5 },{ 5, 4 },{ 4, 7 },{ 5, 7 },{ 7, 5 },
{ 5, 1 },{ 0, 4 },{ 0, 2 },{ 7, 3 },{ 1, 3 },
{ 2, 7 },{ 6, 2 },{ 3, 6 },{ 6, 0 },{ 6, 4 } };
double weight[15] = { 0.35, 0.35, 0.37, 0.28, 0.28, 0.32, 0.38, 0.26, 0.39,
0.29, 0.34, 0.40, 0.52, 0.58, 0.93 };
EdgeWeightedDigraph g(8, 15, arr, weight);
int s = 0;
DijkstraSP sp(&g, s);
for (int t = 0; t < 8; ++t)
if (sp.hasPathTo(t))
{
cout << s << " to " << t << " (" << sp.getDistTo(t) << ")" << endl;
stack<DirectedEdge *> path = sp.pathTo(t);
while (!path.empty())
{
DirectedEdge *e = path.top();
cout << e->toString();
path.pop();
}
}
else
cout << s << " to " << t << " has no path" << endl;
return 0;
}
output:
0 to 0 (0)
0 to 1 (1.05)
0 -- 4: 0.38
4 -- 5: 0.35
5 -- 1: 0.32
0 to 2 (0.26)
0 -- 2: 0.26
0 to 3 (0.99)
0 -- 2: 0.26
2 -- 7: 0.34
7 -- 3: 0.39
0 to 4 (0.38)
0 -- 4: 0.38
0 to 5 (0.73)
0 -- 4: 0.38
4 -- 5: 0.35
0 to 6 (1.51)
0 -- 2: 0.26
2 -- 7: 0.34
7 -- 3: 0.39
3 -- 6: 0.52
0 to 7 (0.6)
0 -- 2: 0.26
2 -- 7: 0.34
Floyd算法又称插点法,可以求所有顶点对之间的最短路径的长度,边权可正可负。
其算法核心是在结点i与结点j之间插入结点k,
看看是否可以缩短结点i与结点j之间的距离(松弛操作)。
算法核心思想:
(1)采用邻接矩阵G[][]存储图,dist[i][j]记录从结点i到结点j的最短路径长度,
pre[i][j]记录结点i到结点j的最短路径上的结点j的直接前驱。
(2)初始化 dist[i][j]=G[i][j], 如果结点i到结点j有边相连,pre[i][j]=i,
否则pre[i][j]=-1。
(3)插点:在结点i,j之间插入结点k,看是否可以缩短结点i,j之间的距离(松弛操作)
if (dist[i][j] > dist[i][k] + dist[k][j])
{
dist[i][j] = dist[i][k] + dist[k][j];
pre[i][j] = pre[k][j];
}
#include <iostream>
const int MAXD = 0x5FFF;
const int MAXN = 100+2;
int dist[MAXN][MAXN] = { };
int pre[MAXN][MAXN] = { };
int G[MAXN][MAXN] = { };
void Floyd(int n)
{
int i = 0, j = 0, k = 0;
for (i = 1; i <= n; ++i)
for (j = 1; j <= n; ++j)
{
if (i == j)
dist[i][j] = 0;
else
dist[i][j] = G[i][j];
if (dist[i][j] < MAXD && i != j)
pre[i][j] = i;
else
pre[i][j] = -1;
}
for (i = 1; i <= n; ++i)
for (j = 1; j <= n; ++j)
for (k= 1; k <= n; ++k)
if (dist[i][j] > dist[i][k] + dist[k][j])
{
dist[i][j] = dist[i][k] + dist[k][j];
pre[i][j] = pre[k][j];
}
}
void printpath(int u, int v)
{
if (pre[u][v] != -1)
{
printpath(u, pre[u][v]);
std::cout << pre[u][v] << " ";
}
}
int main()
{
int arr[][3] = { { 1, 2, 2 },
{ 1, 3, 5 },
{ 2, 3, 2 },
{ 2, 4, 6 },
{ 3, 4, 7 },
{ 4, 3, 2 },
{ 3, 5, 1 },
{ 4, 5, 4 } };
for (int i = 1; i <= 5; ++i)
for (int j = 1; j <= 5; ++j)
{
G[i][j] = MAXD;
dist[i][j] = MAXD;
pre[i][j] = -1;
}
int m = sizeof(arr)/sizeof(arr[0]);
for (int i = 0; i < m; ++i) // 处理重复的边,保留较小的权值
G[arr[i][0]][arr[i][1]] = G[arr[i][0]][arr[i][1]] < arr[i][2] ? G[arr[i][0]][arr[i][1]]: arr[i][2];
Floyd(5);
for (int i = 1; i <= 5; ++i)
{
for (int j = 1; j <= 5; ++j)
std::cout << dist[i][j] << " ";
std::cout << std::endl;
}
return 0;
}
0 2 4 8 5
24575 0 2 6 3
24575 24575 0 7 1
24575 24575 2 0 3
24575 24575 24575 24575 0
Dijkstra算法贪心选取未被处理的具有最小最短路径的结点,然后对其邻接点进行松弛操作。
Bellman-Ford算法也可求单源最短路径,对所有边进行松弛操作,共n-1次,
由于负环可以无限制的减少最短路径长度,所以,如果第n次操作扔可松弛,则一定存在负环。
算法核心思想:
(1)采用边集数组存储图, (u, v, w)
(2)松弛操作,
dist[v]表示从源点到结点v的最短路径长度
对所有的边i(u, v, w), 如果dist[e[i].v] > dist[e[i].u] + e[i].w,
则 dist[e[i].v] = dist[e[i].u] + e[i].w
(3)重复松弛n-1次
(4)再执行一次,如果仍然可以松弛,则说明有负环。
#include <iostream>
const int MAXN = 100 + 2;
const int MAXW = 0x5ffff;
int dist[MAXN];
struct node {
int u;
int v;
int w;
node(int u1, int v1, int w1)
: u(u1), v(v1), w(w1) {}
node() : u(0), v(0), w(0) {}
};
node edge[MAXN*MAXN];
int n; // 结点个数
int m; // 边数量
bool Bellman_Ford(int s)
{
dist[s] = 0;
for (int index = 1; index < n; ++index)
{
bool flag = false; // 松弛优化
for (int i = 0; i < m; ++i)
if (dist[edge[i].u] != MAXW && (dist[edge[i].v] > dist[edge[i].u] + edge[i].w))
{
dist[edge[i].v] = dist[edge[i].u] + edge[i].w;
flag = true;
}
if (!flag)
return false;
}
for (int i = 0; i <m; ++i)
if (dist[edge[i].u] != MAXW && (dist[edge[i].v] > dist[edge[i].u] + edge[i].w))
return true;
return false;
}
int main()
{
int arr[][3] = { { 2, 3, 2 },
{ 1, 2, -3 },
{ 1, 5, 5 },
{ 4, 5, 2 },
{ 3, 4, 3} };
m = sizeof(arr) / sizeof(arr[0]);
n = 5;
for (int i = 1; i <= n; ++i)
dist[i] = MAXW;
for (int i = 0; i < m; ++i)
{
edge[i] = node(arr[i][0], arr[i][1], arr[i][2]);
std::cout << edge[i].u << " " << edge[i].v << " " << edge[i].w << " " << std::endl;
}
bool hasCircle = Bellman_Ford(1);
std::cout << "hasCircle: " << hasCircle << std::endl;
for (int i = 1; i <= n; ++i)
std::cout << dist[i] << std::endl;
return 0;
}
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
hasCircle: 0
0
-3
-1
2
4
队列优化的Bellman - Ford算法,又称为SPFA算法,松弛操作只会发生在最短路径松弛过的前驱结点上,
用一个队列记录松弛过的结点,可以避免冗余的计算。
通常用于求解包含负边的单源最短路径,以及判负环。
在最坏的情况下(稠密图),SPFA算法的时间复杂度和Bellman - Ford算法相同,为O(nm);
但在稀疏图上运行效率较高,为O(km), 其中k是一个较小的常数。
算法核心思想
(1)采用链式前向星存储图,dist[i]记录从源点到结点i的最短路径长度,vis[i]标记结点i是否在队列中,
sum[i]记录结点i入队次数
(2)创建一个队列,源点s入队,标记s在队列中,sum[s]的入队次数加1
(3)松弛操作,取出对头t, 标记t不在队列中。考察t的所有出边i(u, v, w),
如果 dist[v] > dist[t] + edge[i].w,
则 dist[v] = dist[t] + edge[i].w。
如果结点v不在队列中,如果v的入队次数加1后大于等于n, 则说明存在负环,退出;
否则v入队,标记v在队列中。
(4)重复松弛操作,直到队列为空。
#include <iostream>
#include <deque>
const int MAXN = 10 + 2;
struct node {
int to;
int next;
int w;
} edge[MAXN*MAXN];
int head[MAXN];
bool vis[MAXN];
int sum[MAXN];
int dist[MAXN];
int n = 0, m = 0, cnt = 0;
void add(int u, int v, int w)
{
edge[cnt].to = v;
edge[cnt].w = w;
edge[cnt].next = head[u]; // 采用头插法
head[u] = cnt++;
}
bool Spfa(int s)
{
std::deque<int> qu;
vis[s] = true;
dist[s] = 0;
++sum[s];
qu.push_back(s);
while (!qu.empty())
{
int t = qu.front();
qu.pop_front();
vis[t] = false;
for (int i = head[t]; ~i; i = edge[i].next)
{
int v = edge[i].to;
if (dist[v] > dist[t] + edge[i].w)
{
dist[v] = dist[t] + edge[i].w;
if (!vis[v])
{
if (++sum[v] >= n) // has minus circle
return true;
if (!qu.empty() && dist[qu.front()] > dist[v]) // 优化:待插结点的最短路径dist > 队首结点的最短路径dist, 尾插;否则,头插
qu.push_front(v);
else
qu.push_back(v);
vis[v] = true;
}
}
}
}
return false;
}
int main()
{
int arr[][3] = { { 1, 2, 2 },
{ 2, 3, 2 },
{ 2, 4, 1 },
{ 1, 3, 5 },
{ 3, 4, 3 },
{ 1, 4, 4 } };
n = 4;
m = sizeof(arr) / sizeof(arr[0]);
for (int j = 1; j <= n; ++j)
{
vis[j] = false;
sum[j] = 0;
dist[j] = 0x5ffff;
head[j] = -1;
}
for (int i = 0; i < m; ++i)
add(arr[i][0], arr[i][1], arr[i][2]);
Spfa(1);
for (int i = 1; i <= n; ++i)
std::cout << dist[i] << " ";
std::cout << std::endl;
return 0;
}
0 2 4 3

本文深入讲解了几种重要的图算法,包括链式前向星、拓扑排序、Dijkstra算法、Floyd算法及Bellman-Ford算法等。通过实例演示了算法的具体实现过程,并探讨了它们在解决实际问题中的应用。
4503

被折叠的 条评论
为什么被折叠?



