赞
踩
目录
图的存储结构主要分为两种
一般邻接矩阵的行和列的下标都是代表顶点
邻接矩阵能够O(1)判断两个顶点的连接关系,并且取得权值
邻接矩阵存储方式非常适合存储稠密图
相对而言它并不适合查找与该点相连的所有顶点,时间复杂度O(N),(它要遍历该顶点所在的行)
-
- namespace matrix
- {
- template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
- class Graph
- {
-
- public:
- Graph(const V *vertexs, size_t n)
- {
- _vertexs.reserve(n);
- for (size_t i = 0; i < n; i++)
- {
- _vertexs.emplace_back(vertexs[i]);
- _v2index[vertexs[i]] = i;
- }
-
- _matrix.resize(n);
- for (size_t i = 0; i < n; i++)
- {
- _matrix[i].resize(n, MAX_W);
- }
- }
-
- void AddEdge(const V &src, const V &dst, const W &w)
- {
- size_t srci = GetVertexIndex(src);
- size_t dsti = GetVertexIndex(dst);
-
- _AddEdge(srci, dsti, w);
- }
-
- void Print()
- {
- // 顶点
- for (size_t i = 0; i < _vertexs.size(); ++i)
- {
- std::cout << "[" << i << "]"
- << "->" << _vertexs[i] << std::endl;
- }
- std::cout << std::endl;
-
- // 矩阵
- // 横下标
- std::cout << " ";
- for (size_t i = 0; i < _vertexs.size(); ++i)
- {
- printf("%4d", i);
- }
- std::cout << std::endl;
-
- for (size_t i = 0; i < _matrix.size(); ++i)
- {
- std::cout << i << " "; // 竖下标
- for (size_t j = 0; j < _matrix[i].size(); ++j)
- {
- if (_matrix[i][j] == MAX_W)
- {
- printf("%4c", '*');
- }
- else
- {
- printf("%4d", _matrix[i][j]);
- }
- }
- std::cout << std::endl;
- }
- std::cout << std::endl;
- }
-
- private:
- size_t GetVertexIndex(const V &v)
- {
- auto pos = _v2index.find(v);
- if (pos != _v2index.end())
- {
- return pos->second;
- }
- else
- {
- throw "顶点不存在,无法转换为下标";
- return -1;
- }
- }
-
- void _AddEdge(size_t srci, size_t dsti, const W &w)
- {
- _matrix[srci][dsti] = w;
- if (distance == false)
- {
- _matrix[dsti][srci] = w;
- }
- }
-
-
- private:
- std::vector<V> _vertexs; //点集
- std::unordered_map<V, int> _v2index; //点与下标映射关系
- std::vector<std::vector<W>> _matrix; //邻接矩阵
- };
这里图的模板参数
template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
分别指:V是顶点的类型,W是权值的类型,MAX_W是初始化的权值,设定为最大,代表两点之间没有相连,Direction是否为有向图
我们观察无向图的邻接矩阵会发现,无向图邻接矩阵是关于主对角线对称的,所以在添加边时就需要将它的对称点也加上边
- void test_Graph()
- {
- Graph<char, int, INT_MAX, true> g("0123", 4);
- g.AddEdge('0', '1', 1);
- g.AddEdge('0', '3', 4);
- g.AddEdge('1', '3', 2);
- g.AddEdge('1', '2', 9);
- g.AddEdge('2', '3', 8);
- g.AddEdge('2', '1', 5);
- g.AddEdge('2', '0', 3);
- g.AddEdge('3', '2', 6);
-
- g.Print();
- }
邻接表类似于哈希桶,邻接表它有入度表和出度表,分别用来存储指向该点的顶点和从该顶点出发的顶点
它比较适合存储稀疏图,查找一个顶点连接出去的边
不适合确定两个顶点是否相连以及权值
邻接表的各个接口类似于邻接矩阵,实现方法也类似
- namespace link_table
- {
- template <class W>
- struct Edge
- {
- size_t dsti; //出点
- W _w; //权值
-
- Edge *_next;
-
- Edge(size_t dsti, const W &w)
- : _dsti(dsti), _w(w)
- {
- }
- };
-
- template <class V, class W, W MAX_W = INT_MAX, bool Direction = false>
- class Graph
- {
- typedef Edge<W> Edge;
-
- public:
- Graph(const V *vertexs, size_t n)
- {
- _vertexs.reserve(n);
- for (size_t i = 0; i < n; i++)
- {
- _vertexs.emplace_back(vertexs[i]);
- _v2index[vertexs[i]] = i;
- }
-
- _tables.resize(n, nullptr);
- }
-
- size_t GetVertexIndex(const V &v)
- {
- auto pos = _v2index.find(v);
- if (pos != _v2index.end())
- {
- return pos->second;
- }
- else
- {
- throw "顶点不存在,无法转换为下标";
- return -1;
- }
- }
-
- void AddEdge(const V &src, const V &dst, const W &w)
- {
- size_t srci = GetVertexIndex(src);
- size_t dsti = GetVertexIndex(dst);
-
- Edge *edgeS = new Edge(w);
- edgeS->_next = _tables[srci];
- _tables[srci] = edgeS;
-
- if (Direction == false)
- {
- Edge *edgeD = new Edge(w);
- edgeD->_next = _tables[dsti];
- _tables[dsti] = edgeD;
- }
- }
-
- void Print()
- {
- // 顶点
- for (size_t i = 0; i < _vertexs.size(); ++i)
- {
- cout << "[" << i << "]"
- << "->" << _vertexs[i] << endl;
- }
- cout << endl;
-
- for (size_t i = 0; i < _tables.size(); ++i)
- {
- cout << _vertexs[i] << "[" << i << "]->";
- Edge *cur = _tables[i];
- while (cur)
- {
- cout << "[" << _vertexs[cur->_dsti] << ":" << cur->_dsti << ":" << cur->_w << "]->";
- cur = cur->_next;
- }
- cout << "nullptr" << endl;
- }
- }
-
- private:
- std::vector<V> _vertexs; //顶点集合
- std::unordered_map<V, int> _v2index; //顶点与下标的关系
- std::vector<Edge *> _tables;
- };
- }
图的遍历主要分为两种,深度优先遍历(DFS),广度优先遍历(BFS)
深度优先遍历,就好像一个人做事情,不撞到南墙不回头,它是不断的向深处遍历,直到不能够继续遍历下去就会返回,二叉树的前序遍历就是典型的DFS
再比如现在要找东西,假设有三个抽屉,东西在哪个抽屉不清楚,现在要将他们找到,深度优先遍历的做法是:将一个抽屉一次性全部遍历完,再去递归遍历其它盒子
对应到图来说就是寻找与某个点相连的路径
- void DFS(const V &src)
- {
- size_t srci = GetVertexIndex(src);
-
- std::vector<bool> visted(_vertexs.size(), false);
- _DFS(srci, visted);
- }
-
- void _DFS(size_t srci, std::vector<bool> &visted)
- {
- std::cout << srci << " : " << _vertexs[srci] << std::endl;
- visted[srci] = true;
-
- for (size_t i = 0; i < _vertexs.size(); i++)
- {
- if (_matrix[srci][i] == MAX_W || visted[i] == true)
- {
- continue;
- }
- _DFS(i, visted);
- }
- }
-
- void test_DFS()
- {
- Graph<char, int, INT_MAX, true> g("0123", 4);
- g.AddEdge('0', '1', 1);
- g.AddEdge('0', '3', 4);
- g.AddEdge('1', '3', 2);
- g.AddEdge('1', '2', 9);
- g.AddEdge('2', '3', 8);
- g.AddEdge('2', '1', 5);
- g.AddEdge('2', '0', 3);
- g.AddEdge('3', '2', 6);
-
- g.Print();
-
- g.DFS('1');
- }
还是以找东西为例
假设有三个抽屉,东西在哪个抽屉不清楚,现在要将他们找到,广度优先遍历的做法是:
1、先将三个抽屉打开,在最外层找一遍
2、将每一个抽屉中红色的盒子打开,再找一遍
3、将红色盒子中的绿色盒子打开,再找一遍
直到找完所有的盒子,注意:每一个盒子只能找一次,不能重复找
广度优先遍历一般是借助于队列来实现
它是以某一点为起点,向它的周围搜索
标记容器会标记重复搜索过的顶点
在入队列时就标记一下,这样就可以防止像A出去的时候入的BCD
像B出的时候入的ACE,那么入队列时AC就不会重复进入队列了
但是这种思路是有一点问题的
如果给的图不是强连通图呢? 它有若干连通分支怎么办?
答案很简单,在一个连通分支中,BFS一定会遍历该连通分支所有的顶点,标记容器中剩下的没有被标记的就是其余连通分支的顶点,再次重复上述过程,不停遍历,直到标记容器中所有元素都被标记
- void BFS(const V &src)
- {
- size_t srci = GetVertexIndex(src);
-
- // 队列和标记数组
- std::queue<int> q;
- std::vector<bool> visited(_vertexs.size(), false);
-
- q.emplace(srci);
- visited[srci] = true;
-
- while (!q.empty())
- {
- int cur = q.front();
- q.pop();
- std::cout << cur << " : " << _vertexs[cur] << std::endl;
- for (size_t i = 0; i < _matrix.size(); i++)
- {
- if (_matrix[cur][i] == MAX_W || visited[i] == true)
- {
- continue;
- }
- q.emplace(i);
- visited[i] = true;
- }
- }
- std::cout << std::endl;
- }
- void test_BFS()
- {
- Graph<char, int, INT_MAX, true> g("0123", 4);
- g.AddEdge('0', '1', 1);
- g.AddEdge('0', '3', 4);
- g.AddEdge('1', '3', 2);
- g.AddEdge('1', '2', 9);
- g.AddEdge('2', '3', 8);
- g.AddEdge('2', '1', 5);
- g.AddEdge('2', '0', 3);
- g.AddEdge('3', '2', 6);
-
- g.Print();
-
- g.BFS('0');
- }
-
它总共分为两大步骤
1、每次选一条边,共选n-1条边
2、在不构成回路的情况下,尽量选权值最小的边
最小生成树不是唯一的,但是他们的权值是相同的
我们先将原图拷贝一份,不拷贝邻接矩阵,因为最小生成树也是一个图,然后选择权值小的边,添加到我们的最小生成树中
选择的边尽量选择权值小的,这里选边可以借助于优先级队列,建一个小堆
每次去堆里面取数据,同时要避免成环,这里可以采用并查集来解决,一条边是有两个顶点的
如果两个顶点已经在一个集合中了,添加就会造成环,所以添加不在同一个集合的顶点,并且将添加边的两个顶点合并到同一集合中,直到堆中数据为空,然后比较添加的边的数量是否等于n-1
如果等于,那么他就是最小生成树,不等于就没有最小生成树。
- W Kruskal(self &minTree)
- {
- size_t n = _vertexs.size();
-
- minTree._vertexs = _vertexs;
- minTree._v2index = _v2index;
- minTree._matrix.resize(n);
- for (size_t i = 0; i < n; i++)
- {
- minTree._matrix[i].resize(n, MAX_W);
- }
- // 根据边的权值排序
- std::priority_queue<Edge, std::vector<Edge>, std::greater<Edge>> minque;
- for (size_t i = 0; i < n; i++)
- {
- for (size_t j = 0; j < n; j++)
- {
- if (i < j && _matrix[i][j] != MAX_W)
- {
- minque.emplace(i, j, _matrix[i][j]);
- }
- }
- }
-
- // 选出n-1条边
- size_t size = 0;
- W totalW = W(); // 权值总和
- UnionFindSet ufs(n);
- while (!minque.empty())
- {
- Edge min = minque.top();
- minque.pop();
-
- if (!ufs.InSet(min._srci, min._dsti))
- {
- std::cout << _vertexs[min._srci] << "->" << _vertexs[min._dsti] << " : " << min._w << std::endl;
- minTree._AddEdge(min._srci, min._dsti, min._w);
- ufs.Union(min._srci, min._dsti);
- size++;
- totalW += min._w;
- }
- else
- {
- std::cout << "构成环: ";
- std::cout << _vertexs[min._srci] << " -> " << _vertexs[min._dsti] << " : " << min._w << std::endl;
- }
- }
-
- if (size == n - 1)
- {
- return totalW;
- }
- else
- {
- return W();
- }
- }
普里姆算法(Prim算法),图论中的一种算法,可在加权连通图里搜索最小生成树。意即由此算法搜索到的边子集所构成的树中,不但包括了连通图里的所有顶点(英语:Vertex (graph theory)),且其所有边的权值之和亦为最小。该算法于1930年由捷克数学家沃伊捷赫·亚尔尼克(英语:Vojtěch Jarník)发现;并在1957年由美国计算机科学家罗伯特·普里姆(英语:Robert C. Prim)独立发现;1959年,艾兹格·迪科斯彻再次发现了该算法。因此,在某些场合,普里姆算法又被称为DJP算法、亚尔尼克算法或普里姆-亚尔尼克算法。
Prim算法也是利用了贪心算法,不过它与克鲁斯卡尔算法的贪心策略不同,克鲁斯卡尔算法是采用全局最优解的方式,从全局选择权值最小的边,而普利姆算法是采用局部最优解,逐渐推到全局最优解,所以普利姆算法需要给出一个起始点
这里我们让a点作为起始点,然后a连通的有两个顶点b,h
然后比较ab,ah的权值,ab边权值小于bh权值,所以选择ab边
b又连接c,h ,bc权值比bh小,所以选择bc,重复上述过程,直到选择了n - 1条边
Prim算法与Kruskal算法相比,它天然的避开了圈,所以它并不需要进行判断是否会形成圈
我们将其分为两个集合,一个集合为已选点集合,另一个是未选点集合
我们在两个集合之间选权值最小的边,直到未选集合为空
这里要注意,虽然Prim算法天然避免形成圈,但是我们在选择权值最小边的时候很容易形成圈
这里可以不用借助于其它数据结构来解决,最小边的目标点在已选集合中,就会构成圈
我们还是使用优先级队列来对边进行排序,每次选择权值最小的边
将目标点放到已选集合中,在未选集合中删掉该点
然后将目标点所连的点全部进入优先级队列中,继续寻找 ,直到优先级队列为空,或者已经找到了n - 1条边
- W Prim(self &minTree, const W &src)
- {
- size_t srci = GetVertexIndex(src);
- size_t n = _vertexs.size();
-
- minTree._vertexs = _vertexs;
- minTree._v2index = _v2index;
- minTree._matrix.resize(n);
- for (size_t i = 0; i < n; i++)
- {
- minTree._matrix[i].resize(n, MAX_W);
- }
-
- std::vector<bool> X(n, false);
- std::vector<bool> Y(n, true);
- X[srci] = true;
- Y[srci] = false;
-
- std::priority_queue<Edge, std::vector<Edge>, std::greater<Edge>> minq;
- for (size_t i = 0; i < n; i++)
- {
- if (_matrix[srci][i] != MAX_W)
- {
- minq.emplace(srci, i, _matrix[srci][i]);
- }
- }
-
- std::cout << "Prim开始选边" << std::endl;
- size_t size = 0;
- W totalW = W();
-
- while (!minq.empty())
- {
- Edge min = minq.top();
- minq.pop();
-
- if (X[min._dsti])
- {
- std::cout << "构成环: ";
- std::cout << _vertexs[min._srci] << " -> " << _vertexs[min._dsti] << " : " << _matrix[min._srci][min._dsti] << std::endl;
- }
- else
- {
- minTree._AddEdge(min._srci, min._dsti, min._w);
- X[min._dsti] = true;
- Y[min._dsti] = false;
- size++;
- totalW += min._w;
-
- if (size == n - 1)
- {
- break;
- }
-
- for (size_t i = 0; i < n; i++)
- {
- if (_matrix[min._dsti][i] != MAX_W && Y[i])
- {
- minq.emplace(min._dsti, i, _matrix[min._dsti][i]);
- }
- }
- }
- }
-
- if (size == n - 1)
- {
- return totalW;
- }
- else
- {
- return W();
- }
- }
- void TestGraphMinTree()
- {
- const char str[] = "abcdefghi";
- Graph<char, int> g(str, strlen(str));
- g.AddEdge('a', 'b', 4);
- g.AddEdge('a', 'h', 8);
- // g.AddEdge('a', 'h', 9);
- g.AddEdge('b', 'c', 8);
- g.AddEdge('b', 'h', 11);
- g.AddEdge('c', 'i', 2);
- g.AddEdge('c', 'f', 4);
- g.AddEdge('c', 'd', 7);
- g.AddEdge('d', 'f', 14);
- g.AddEdge('d', 'e', 9);
- g.AddEdge('e', 'f', 10);
- g.AddEdge('f', 'g', 2);
- g.AddEdge('g', 'h', 1);
- g.AddEdge('g', 'i', 6);
- g.AddEdge('h', 'i', 7);
-
- Graph<char, int> kminTree;
- std::cout << "Kruskal:" << g.Kruskal(kminTree) << std::endl;
- kminTree.Print();
- std::cout << std::endl
- << std::endl;
-
- Graph<char, int> pminTree;
- std::cout << "Prim:" << g.Prim(pminTree, 'a') << std::endl;
- pminTree.Print();
- std::cout << std::endl;
-
- for (size_t i = 0; i < strlen(str); ++i)
- {
- std::cout << "Prim:" << g.Prim(pminTree, str[i]) << std::endl;
- }
- }
迪杰斯特拉算法选择的是顶点,更新该点与它连接点的路径长度,选择路径长度最短点,重复操作
总之迪杰斯特拉算法只需要对每一个顶点进行计算,去找与它相连的路径最短点,然后让路径最短点重复上述过程,因为每次选择的都是最短距离的点,所以在整体上达到了最短
因为是对每一个顶点操作,会不停的更新节点距离
这里迪杰斯特拉算法我们化简,使用两个一维数组,dist代表每一个点,距离给出的起始点的最短距离
另一个一维数组是pPath,用来记录当前节点的上一个节点的下标
s y z t x是5个不同的点
0,1,2,3,4是这5个点的映射下标
然后选择边
因为s->s,s->t,s->y,的距离是确定的,更新dist和pPath。
接下来因为y是与s连接的距离最短的点,所以在从y点,计算与它连接的最短的距离
y->z的距离是2,那么s->z的距离就是s->y + y->z的距离,让原本的s->z的距离与s->y +y -> z的距离相比,选择距离最短的,同时记录z的上一个节点s的下标
与y相连的其它点也进行同样操作,直到图中每一个顶点都被计算过
-
- void Dijkstra(const V &src, std::vector<W> &dist, std::vector<int> &pPath)
- {
- size_t srci = GetVertexIndex(src);
- size_t n = _vertexs.size();
- dist.resize(n, MAX_W);
- pPath.resize(n, -1);
-
- dist[srci] = 0;
- pPath[srci] = srci;
-
- std::vector<bool> S(n, false);
-
- for (size_t j = 0; j < n; j++)
- {
- int u = 0;
- W min = MAX_W;
- for (size_t i = 0; i < n; i++)
- {
- if (S[i] == false && dist[i] < min)
- {
- u = i;
- min = dist[i];
- }
- }
-
- S[u] = true;
- for (size_t v = 0; v < n; v++)
- {
- if (S[v] == false && _matrix[u][v] != MAX_W && _matrix[u][v] + dist[u] < dist[v])
- {
- dist[v] = dist[u] + _matrix[u][v];
- pPath[v] = u;
- }
- }
- }
- }
这里不太好观察最短路径,实现一个打印最短路径的函数
dist中存储的是每一个节点的上一个节点,所以我们需要正着遍历一遍之后reverse
对于每一个节点来说都是这样
- void PrintShortPath(const V &src, const std::vector<W> &dist, std::vector<int> &pPath)
- {
- size_t srci = GetVertexIndex(src);
- size_t n = _vertexs.size();
-
- for (size_t i = 0; i < n; i++)
- {
- if (i != srci)//起始节点最初不用入栈
- {
- std::vector<int> path;
- size_t parenti = i;
- while (parenti != srci)
- {
- path.emplace_back(parenti);
- parenti = pPath[parenti];
- }
- path.emplace_back(srci);
- std::reverse(path.begin(), path.end());
-
- for (const auto &index : path)
- {
- std::cout << _vertexs[index] << " -> ";
- }
- std::cout << std::endl;
- }
- }
- }
- void TestGraphDijkstra()
- {
- const char *str = "syztx";
- Graph<char, int, INT_MAX, true> g(str, strlen(str));
- g.AddEdge('s', 't', 10);
- g.AddEdge('s', 'y', 5);
- g.AddEdge('y', 't', 3);
- g.AddEdge('y', 'x', 9);
- g.AddEdge('y', 'z', 2);
- g.AddEdge('z', 's', 7);
- g.AddEdge('z', 'x', 6);
- g.AddEdge('t', 'y', 2);
- g.AddEdge('t', 'x', 1);
- g.AddEdge('x', 'z', 4);
-
- std::vector<int> dist;
- std::vector<int> parentPath;
- g.Dijkstra('s', dist, parentPath);
- g.PrintShortPath('s', dist, parentPath);
-
- // 图中带有负权路径时,贪心策略则失效了。
- // 测试结果可以看到s->t->y之间的最短路径没更新出来
- // const char *str = "sytx";
- // Graph<char, int, INT_MAX, true> g(str, strlen(str));
- // g.AddEdge('s', 't', 10);
- // g.AddEdge('s', 'y', 5);
- // g.AddEdge('t', 'y', -7);
- // g.AddEdge('y', 'x', 3);
- // std::vector<int> dist;
- // std::vector<int> parentPath;
- // g.Dijkstra('s', dist, parentPath);
- // g.PrintShortPath('s', dist, parentPath);
-
- // const char *str = "syztx";
- // Graph<char, int, INT_MAX, true> g(str, strlen(str));
- // g.AddEdge('s', 't', 6);
- // g.AddEdge('s', 'y', 7);
- // g.AddEdge('y', 'z', 9);
- // g.AddEdge('y', 'x', -3);
- // g.AddEdge('z', 's', 2);
- // g.AddEdge('z', 'x', 7);
- // g.AddEdge('t', 'x', 5);
- // g.AddEdge('t', 'y', 8);
- // g.AddEdge('t', 'z', -4);
- // g.AddEdge('x', 't', -2);
- // std::vector<int> dist;
- // std::vector<int> parentPath;
- // g.Dijkstra('s', dist, parentPath);
- // g.PrintShortPath('s', dist, parentPath);
- }
贝尔曼-福特算法(英语:Bellman–Ford algorithm),求解单源最短路径问题的一种算法,由理查德·贝尔曼(Richard Bellman) 和莱斯特·福特创立的。有时候这种算法也被称为 Moore-Bellman-Ford 算法,因为Edward F. Moore也为这个算法的发展做出了贡献。它的原理是对图进行|V| - 1 次松弛操作,得到所有可能的最短路径。其优于迪科斯彻算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,高达O(|V| * |E|)。但算法可以进行若干种优化,提高了效率。贝尔曼-福特算法与迪科斯彻算法类似,都以松弛操作为基础,即估计的最短路径值渐渐地被更加准确的值替代,直至得到最优解。在两个算法中,计算时每个边之间的估计距离值都比真实值大,并且被新找到路径的最小长度替代。 然而,迪科斯彻算法以贪心法选取未被处理的具有最小权值的节点,然后对其的出边进行松弛操作;而贝尔曼-福特算法简单地对所有边进行松弛操作,共|V| - 1次,其中|V|是图的点的数量。在重复地计算中,已计算得到正确的距离的边的数量不断增加,直到所有边都计算得到了正确的路径。这样的策略使得贝尔曼-福特算法比迪科斯彻算法适用于更多种类的输入。
这里也可以看出Bellman-Ford算法是一种暴力求解算法
Bellman-Ford算法的执行过程,源节点为s,节点中的数值为该节点的距离,加了阴影的边表示前驱节点值,本图的例子中,每一次的松弛操作对边的处理次序都是(t, x), (t, y), (t, z), (y, x), (y, z), (z, x), (z, s), (s, t), (s, y) 如果没有负权回路该算法则会返回true
- bool BellmanFord(const V &src, std::vector<W> &dist, std::vector<int> &pPath)
- {
- size_t srci = GetVertexIndex(src);
- size_t n = _vertexs.size();
- dist.resize(n, MAX_W);
- pPath.resize(n, -1);
-
- dist[srci] = W();
-
- for (size_t k = 0; k < n; k++)
- {
- bool update = false;
- std::cout << "更新到: " << k + 1 << "轮" << std::endl;
- for (size_t i = 0; i < n; i++)
- {
- for (size_t j = 0; j < n; j++)
- {
- if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j])
- {
- update = true;
- std::cout << _vertexs[i] << " -> " << _vertexs[j] << " : " << _matrix[i][j] << std::endl;
- dist[j] = dist[i] + _matrix[i][j];
- pPath[j] = i;
- }
- }
- }
-
- if (update == false)
- {
- break;
- }
- }
- //检验是否具有负权回路,如果有直接跳出,避免死循环
- for (size_t i = 0; i < n; i++)
- {
- for (size_t j = 0; j < n; j++)
- {
- if (dist[i] + _matrix[i][j] < dist[j])
- {
- return false;
- }
- }
- }
- return true;
- }
这里总共套了三层for循环
第一层k,代表的是更新的轮数,每一个节点最多更新n轮,同时设定一个标记位,如果上一轮没有更新节点就直接退出
第二第三层i, j 代表节点 i 到节点 j 的距离,每次都选它的最短距离
- void testBellmanFord()
- {
- const char *str = "syztx";
- Graph<char, int, INT_MAX, true> g(str, strlen(str));
- g.AddEdge('s', 't', 6);
- g.AddEdge('s', 'y', 7);
- g.AddEdge('y', 'z', 9);
- g.AddEdge('y', 'x', -3);
- // g.AddEdge('y', 's', 1); // 新增
- g.AddEdge('z', 's', 2);
- g.AddEdge('z', 'x', 7);
- g.AddEdge('t', 'x', 5);
- // g.AddEdge('t', 'y', -8); //更改
- g.AddEdge('t', 'y', 8);
-
- g.AddEdge('t', 'z', -4);
- g.AddEdge('x', 't', -2);
- std::vector<int> dist;
- std::vector<int> parentPath;
- if (g.BellmanFord('s', dist, parentPath))
- g.PrintShortPath('s', dist, parentPath);
- else
- std::cout << "带负权回路" << std::endl;
- }
这里采用了带负权回路的测试用例,目前没有算法能够解决带有负权回路的情况
Floyd-Warshall算法的原理是动态规划
设Di,j,k为从i到j的只以(1……k)集合中的节点为中间节点的最短路径的长度
1、若最短路径经过点k,则Di,j,k = Di,k,k-1 + Dk, j, k-1
2、若最短路径不经过点k,则Di,j,k = Di,j,k-1
在实际算法中,为了节约空间,可以直接在原来的空间上进行迭代,这样空间可降至二维
因为本算法是动态规划,所以必然符合动态规划三部曲
1、确定下标含义
2、确定状态转移方程
3、确定遍历方向
dp[i][j][k] 代表从i点到j点经过k个点的最短路径
状态转移方程,前面已经说明
确定遍历方向,因为状态转移方程很明显是从前面往后面推,所以应该是正向遍历
同时我们压缩空间,在BellmanFord算法的dist和pPath数组扩展到二维
这个算法整体上十分简单
- // 多源最短路径,不需要给出源点
- void FloydWarshall(std::vector<std::vector<W>> &dist, std::vector<std::vector<int>> &pPath)
- {
- size_t n = _vertexs.size();
- dist.resize(n, std::vector<W>(n, MAX_W));
- pPath.resize(n, std::vector<int>(n, -1));
-
- // 动规三部曲,1、初始化
- // 将直接相连的顶点初始化
- for (size_t i = 0; i < n; i++)
- {
- for (size_t j = 0; j < n; j++)
- {
- if (_matrix[i][j] != MAX_W)
- {
- dist[i][j] = _matrix[i][j];
- pPath[i][j] = i;
- }
-
- if (i == j)
- {
- dist[i][j] = W();
- }
- }
- }
-
- // 2、确定遍历顺序
- // 3、状态转移方程
- for (size_t k = 0; k < n; k++)
- {
- for (size_t i = 0; i < n; i++)
- {
- for (size_t j = 0; j < n; j++)
- {
- if (dist[i][k] != MAX_W && dist[k][j] != MAX_W && dist[i][k] + dist[k][j] < dist[i][j])
- {
- dist[i][j] = dist[i][k] + dist[k][j];
- pPath[i][j] = pPath[k][j];
- }
- }
- }
- }
-
- // 打印结果Debug
-
- // 打印权值
- for (size_t i = 0; i < n; i++)
- {
- for (size_t j = 0; j < n; j++)
- {
- if (dist[i][j] == MAX_W)
- {
- printf("%3c", '*');
- }
- else
- {
- printf("%3d", dist[i][j]);
- }
- }
- std::cout << "\n";
- }
- std::cout << "\n";
-
- // 打印路径,下标
- for (size_t i = 0; i < n; i++)
- {
- for (size_t j = 0; j < n; j++)
- {
- printf("%3d", pPath[i][j]);
- }
- std::cout << "\n";
- }
- std::cout << "\n";
-
- std::cout << "----------------------------------------------" << std::endl;
- }
因为初始化部分就已经将不经过k点的情况给初始化了,所以在后面迭代过程中没有对这种情况进行讨论
- void testFloydWarshall()
- {
- const char *str = "12345";
- Graph<char, int, INT_MAX, true> g(str, strlen(str));
- g.AddEdge('1', '2', 3);
- g.AddEdge('1', '3', 8);
- g.AddEdge('1', '5', -4);
- g.AddEdge('2', '4', 1);
- g.AddEdge('2', '5', 7);
- g.AddEdge('3', '2', 4);
- g.AddEdge('4', '1', 2);
- g.AddEdge('4', '3', -5);
- g.AddEdge('5', '4', 6);
- std::vector<std::vector<int>> vvDist;
- std::vector<std::vector<int>> vvParentPath;
- g.FloydWarshall(vvDist, vvParentPath);
-
- // 打印任意两点之间的最短路径
- for (size_t i = 0; i < strlen(str); ++i)
- {
- g.PrintShortPath(str[i], vvDist[i], vvParentPath[i]);
- std::cout << std::endl;
- }
- }
以上就是今天要讲的内容,本文仅仅简单介绍了图论相关简单内容。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。