查询两个个元素,是否在一个集合当中
,这里的集合
用树
的形式进行表示。森林
, 即多棵树。我们再来简单的举个例子:
下面我们用如上例子进行展开讨论:
- 宿舍六人,即六个人,如何判断两个人在同一个集合? 如何进行实现?
- 先来解决第一个问题,六个人,选出一个宿舍长,只要两个人的宿舍长是一样的,即可判断两个人在一个集合。
- 再来解决第二个问题,既然宿舍长有了,我们都与这个宿舍长产生关联即可,即用树的形式进行表示,至于如何表示,我们可以用双亲表示法进行表示,即每个人记住其宿舍长的名字即可。更为形象的我们可以用下图进行表示:
- 更进一步,如何用计算机存储这种结构呢?我们只需对每个人名生成一个下标连续,用计算机进行存储即可。用下图进行直观的理解:
- 对这张图我们再说明一点,除0下标以外的其他位置存放的是指向代表孙八的下标,这个0处下标存的是集合的所有元素的个数,且存放的是负数形式,这样存有一个好处,我们可以由这个并查集中有多少负数,从而判断这个并查集中有多少个集合。
- 两个人产生关联,本质上是两个宿舍(集合)之间产生了关联,那两个宿舍如何进行关联起来呢?
根据上面的描述,我们可以作出大致总结:
由以上信息我们先可以搭建出实现并查集的大致框架:
#include
#include
#include
using namespace std;
template<class T>
class UnionFindSet
{
public:
UnionFindSet(const T* arr, size_t size);//构造函数
int GetValueIndex(const T& val);//获取val所代表的下标。
void GetRoot(const T& val);
//获取根节点的下标
void Union(const T& x1, const T& x2);
//将两个元素的集合进行合并。
bool IsSameSet(const T& x1, const T& x2);
//判断两个元素是否在同一个集合中
int GetSetSize();
//获取集合的元素
private:
map<T, int> _indexHash;
//map或者unordered_map都可以。用于快速将T转换为对应的下标。
vector<T> _createIndex;
//用此数组对T类型元素生成下标。
vetor<int> _aggregate;
//用于存放集合元素,即森林。
};
UnionFindSet(const T* arr, size_t size)
{
_aggreagte.resize(size, -1);
//对存放集合的元素初始化,表示每个元素存放一个元素(负数表示)。
_createIndex.resize(size);
for (size_t i = 0; i < size; i++)
{
_createIndex[i] = arr[i];
_indexHash[arr[i]] = i;//生成下标。
}
}
int GetValueIndex(const T& val)
{
auto it = _indexHash.find(val);
//最好判断一下val是否存在对应的下标。
if (it == _indexHash.end())
{
throw invalid_argument("不存在所对应的下标");
return -1;
}
return it->second;
}
int GetRoot(const T& val)
{
int index = GetValueIndex(val);
//找不到小于0的下标指向的位置就一直向上进行找。
while (_aggregate[index] >= 0)
{
index = _aggregate[index];
}
return index;
}
bool IsSameSet(const T& x1, const T& x2)/
{
int index1 = GetRoot(x1);
int index2 = GetRoot(x2);
return index1 == index2;
}
void Union(const T& x1, const T& x2)//将两个元素的集合进行合并。
{
if (!IsSameSet(x1, x2))
{
//不在同一个集合再进行合并。
int index1 = GetRoot(x1);
int index2 = GetRoot(x2);
//进行一步优化,即元素少的合并到元素多的集合当中
//此处我们假设index1为元素多的集合,index2为元素少的集合。
if (abs(index1) < abs(index2))
{
swap(index1, index2);
}
//即将index2(少)合并到index1(多)上
//将index2的元素加到index2上
_aggregate[index1] += _aggregate[index2];
//将index2的父路径指向index1
_aggregate[index2] = index1;
}
}
int GetSetSize()//获取并查集的集合个数
{
int sum = 0;
for (auto e : _aggregate)
{
//计算小于0的元素个数即可。
if (e < 0)
{
sum++;
}
}
return sum;
}
所谓路径压缩,其实解决存在这样的集合:
所引发的问题:如果数据足够的多
,我们之前写的GetRoot函数的效率会急剧的降低
,因此才需要路径压缩帮助我们进行优化。
我们只需要找到根节点之后,再找一遍,此时将cur路径上的结点链接到root即可,这样方便了后续的查找。
优化之后的GetRoot
int GetRoot(const T& val)//获取根节点的下标
{
int index = GetValueIndex(val);
int root = index;
//找不到小于0的下标指向的位置就一直向上进行找。
while (_aggregate[root] >= 0)
{
root = _aggregate[root];
}
//路径压缩进行优化。
while (index != root)
{
//先保存之前父路径的下标
int parent = _aggregate[index];
//再将当前结点的父路径改为root
_aggregate[index] = root;
//继续往上迭代
index = parent;
}
return root;
}
#include
#include
#include
using namespace std;
template<class T>
class UnionFindSet
{
public:
UnionFindSet(const T* arr, size_t size)
{
_aggregate.resize(size, -1);
//对存放集合的元素初始化,表示每个元素存放一个元素(负数表示)。
_createIndex.resize(size);
for (size_t i = 0; i < size; i++)
{
_createIndex[i] = arr[i];
_indexHash[arr[i]] = i;//生成下标。
}
}
int GetValueIndex(const T& val)//获取val所代表的下标。
{
auto it = _indexHash.find(val);
if (it == _indexHash.end())
{
throw invalid_argument("不存在所对应的下标");
return -1;
}
return it->second;
}
int GetRoot(const T& val)//获取根节点的下标
{
int index = GetValueIndex(val);
int root = index;
//找不到小于0的下标指向的位置就一直向上进行找。
while (_aggregate[root] >= 0)
{
root = _aggregate[root];
}
//路径压缩进行优化。
while (index != root)
{
//先保存之前父路径的下标
int parent = _aggregate[index];
//再将当前结点的父路径改为root
_aggregate[index] = root;
//继续往上迭代
index = parent;
}
return root;
}
void Union(const T& x1, const T& x2)//将两个元素的集合进行合并。
{
if (!IsSameSet(x1, x2))
{
//不在同一个集合再进行合并。
int index1 = GetRoot(x1);
int index2 = GetRoot(x2);
//进行一步优化,即元素少的合并到元素多的集合当中
//此处我们假设index1为元素多的集合,index2为元素少的集合。
if (abs(index1) < abs(index2))
{
swap(index1, index2);
}
//即将index2(少)合并到index1(多)上
//将index2的元素加到index2上
_aggregate[index1] += _aggregate[index2];
//将index2的父路径指向index1
_aggregate[index2] = index1;
}
}
//判断两个元素是否在同一个集合中
bool IsSameSet(const T& x1, const T& x2)
{
int index1 = GetRoot(x1);
int index2 = GetRoot(x2);
return index1 == index2;
}
int GetSetSize()//获取并查集的集合个数
{
int sum = 0;
for (auto e : _aggregate)
{
if (e < 0)
{
sum++;
}
}
return sum;
}
private:
map<T, int> _indexHash;
//map或者unordered_map都可以,用于快速将T转换为对应的下标。
vector<T> _createIndex;//用此数组对T类型元素生成下标。
vector<int> _aggregate; //用于存放集合元素,即森林。
};
#include"UnionFindSet.hpp"
int main()
{
string str[] = { "张三","李四","王五","赵六","周七" };
UnionFindSet<string> ufs(str, sizeof(str) / sizeof(str[0]));
ufs.Union("张三", "李四");
ufs.Union("王五", "赵六");
cout << "集合数为:" << ufs.GetSetSize() << endl;
return 0;
}
并查集习题:
- 省份数量
- .等式方程的可满足性
并查集可以帮助起到判环的作用
,因此我们这里放到一块进行讲解。
- 图有两个基本元素:
- 顶点, 我们可以将具体的顶点抽象成下标,从而用下标进行表示。
- 边,两个顶点即可确定一条边,因此我们可以用二维矩阵的方式进行表示;每个顶点都有与其相连的边,因此,我们可以单独每个顶点所连接的边抽象成桶的形式(类似于哈希桶)进行表示。
/*
V(vertex) 表示实际存储边的类型,W(weight)表示边的权重,
W_MAX 表示权重的不可能取值。
Direction false表示是无向的,true表示是有向的。
*/
template<class V, class W, W W_MAX = INT_MAX,
bool Direction = false>
class Graph
{
public:
/*
构造函数,传入的参数为V类型的指针指向的是V类型数组,
以及数组的元素个数。
*/
Graph(const V* a, size_t n)//有多少个顶点
{
//初始化边,以及生成边的下标
_vertexs.resize(n);
for (size_t i = 0; i < n; i++)
{
_vertexs[i] = a[i];
_indexMap[a[i]] = i;
}
//将矩阵进行初始化
_matrices.resize(n);
for (size_t i = 0; i < n; i++)
{
//没有权值,我们初始化为W_MAX,表示最开始顶点之间不互相连通。
_matrices[i].resize(n, W_MAX);
}
}
//将实际的顶点转换为对应的下标
int GetVertexIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it == _indexMap.end())
{
//找不到
throw invalid_argument("顶点不存在");//抛出异常
return -1;
}
return it->second;
}
//添加边
void AddEdge(const V& src, const V& dst, const W& w)
{
int srci = GetVertexIndex(src);
int dsti = GetVertexIndex(dst);
_AddEdge(srci, dsti, w);
}
//这里我们写一个子函数,方便内部接口进行使用。
void _AddEdge(int srci, int dsti, const W& w)
{
_matrices[srci][dsti] = w;
if (Direction == false)
{
//说明是无向图
_matrices[dsti][srci] = w;
}
}
//为了方便进行测试,这里博主将打印函数给出。
void Print()
{
for (size_t i = 0; i < _vertexs.size(); i++)
{
printf("[%d]->", i);
cout << _vertexs[i] << endl;
//下标对应的边
}
cout << " ";
for (size_t i = 0; i < _matrices.size(); i++)
printf("%-4d", i);
cout << endl;
for (size_t i = 0; i < _matrices.size(); i++)
{
printf("%-4d",i);
for (size_t j = 0; j < _matrices[i].size(); j++)
{
if (_matrices[i][j] != W_MAX)
printf("%-4d", _matrices[i][j]);
else
printf("%-4c", '*');
}
cout << endl;
}
cout << endl;
}
vector<V> _vertexs;//顶点
map<V, int> _indexMap;//顶点所对应的下标
vector<vector<W>> _matrices; //矩阵的英文
};
- 如果边带有权值,并且两个节点之间是连通的,边的关系就用权值代替。
- 如果两个顶点不通,则使用无穷大代替,即W_MAX。
void TestGraph()
{
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();
}
int main()
{
TestGraph();
return 0;
}
namespace link
{
/*
因为要存顶点与边的关系,因此我们需要一个结构体来保存对应
的相连的顶点与边的权值。
*/
template<class V,class W>
struct Edge
{
V _dst;//目标顶点
W _w;//权值
Edge<V, W>* _next;
//构造函数
Edge(const V& dst, const W w)
:_dst(dst),_w(w),_next(nullptr)
{}
};
template<class V, class W, bool Direction = false>
class Graph
{
public:
typedef Edge<V, W> Edge;
Graph(const V* a, size_t n)//有多少个顶点
{
//初始化边,以及生成对应的下标
_vertexs.resize(n);
for (size_t i = 0; i < n; i++)
{
_vertexs[i] = a[i];
_indexMap[a[i]] = i;
}
//将矩阵进行初始化,为空表示最开始顶点没有边与之相连。
_link.resize(n,nullptr);
}
//添加边
void AddEdge(const V& src, const V& dst, const W& w)
{
int srci = GetVertexIndex(src);
int dsti = GetVertexIndex(dst);
Edge* node = new Edge(dst, w);
node->_next = _link[srci];
_link[srci] = node;
if (Direction == false)
{
//说明是无向图
Edge* node = new Edge(src, w);
node->_next = _link[dsti];
_link[dsti] = node;
}
}
//获取顶点的下标。
int GetVertexIndex(const V& v)
{
auto it = _indexMap.find(v);
if (it == _indexMap.end())
{
//找不到
throw invalid_argument("顶点不存在");//抛出异常
return -1;
}
return it->second;
}
//打印的时候我们按照链表的形式打印即可。
void Print()
{
for (size_t i = 0; i < _link.size(); i++)
{
cout << "[" << i << ":" << _vertexs[i] << "]->";
Edge* cur = _link[i];
while (cur)
{
cout << "[" << cur->_dst << ":"
<< _indexMap[cur->_dst] << ":"
<< cur->_w << "]->";
cur = cur->_next;
}
cout << "nullptr" << endl;
}
cout << endl;
}
private:
vector<V> _vertexs;//顶点
map<V, int> _indexMap;//顶点所对应的下标
vector<Edge*> _link; //邻接表
};
}
void TestGraph()
{
string a[] = { "张三", "李四", "王五", "赵六" };
Graph<string, int,true> g1(a, 4);
g1.AddEdge("张三", "李四", 100);
g1.AddEdge("张三", "王五", 200);
g1.AddEdge("王五", "赵六", 30);
g1.Print();
}
运行结果:
我们再来分析一下流程,这里是以A为起点,进行广度遍历。
- 先遍历A,。
- 然后遍历与A相连的BCD。
- 其次在遍历与BCD相连的EF,此时就需要注意之前访问过的结点不能在接着继续访问了。
- 接着遍历与EF相连的HG,此时也需注意同样的问题。
- 最后遍历与H相连的I,此时同理。
实现方式:
void BFS(const V& src)
{
int srci = GetVertexIndex(src);
int n = _vertexs.size();
vector<int> is_visited(n, false);
//防止重复结点入队列,以免形成回路。
queue<int> que;
que.push(srci);
is_visited[srci] = true;
int levelsize = 1;//第一层就srci.
while (!que.empty())
{
for (int i = 0; i < levelsize; i++)
{
int front = que.front();
que.pop();
cout << front << ":" << _vertexs[front] << " ";
//将与front相关的边进行入队列
for (int i = 0; i < n; i++)
{
if (_matrices[front][i] != W_MAX &&
is_visited[i] == false)
{
que.push(i);
is_visited[i] = true;
}
}
//这一层for循环式暴力遍历矩阵的所在行,确认是否有
//没被访问的边。如果是邻接表就直接取较为方便,不过
//稠密图倒是矩阵更优一点,能更好的确认两点的关系。
}
cout << endl;
//更新层结点的个数。
levelsize = que.size();
}
}
void TestBFS()
{
string a[] = { "A", "B", "C", "D", "E","F","G","H","I" };
Graph<string, int> g1(a, sizeof(a) / sizeof(string));
g1.AddEdge("A", "B", 1);
g1.AddEdge("A", "C", 1);
g1.AddEdge("A", "D", 1);
g1.AddEdge("B", "E", 1);
g1.AddEdge("B", "C", 1);
g1.AddEdge("C", "F", 1);
g1.AddEdge("C", "B", 1);
g1.AddEdge("D", "F", 1);
g1.AddEdge("E", "G", 1);
g1.AddEdge("F", "H", 1);
g1.AddEdge("H", "I", 1);
g1.BFS("A");
}
我们再来分析一下流程,这里是以A为起点,进行深度遍历。
说明:已经访问过的结点我们是不再进行访问的。
- 先访问A相邻的B, 再访问与B相连的C, 再访问与C相连的F, 再访问与F相连的D。
- D相邻的A我们是不再进行访问的,因此又回到F, 接着访问H,紧接着访问与H相连的I,I没有访问过的结点,回退到H, H也没有访问过的结点回退到 F。
- F也没有与未访问的结点,回退到C,C也没有未访问的结点,于是回退到B。
- 接着访问与B相连的E, 更深一步访问与E相连的G,G没有未访问过的结点,回退到E, E此时也没有未访问过的结点回退到B, B此时也没有未访问过的结点,回退到A.
- 访问结束。
void _DFS(int srci,vector<bool>& is_visted)
{
for (size_t i = 0; i < is_visted.size(); i++)
{
if (_matrices[srci][i] != W_MAX &&
is_visted[i] == false)
{
//此处打印的目的是便于测试。
cout << "[" << _vertexs[srci] << "->"
<< _vertexs[i] << "]" << endl;
is_visted[i] = true;
_DFS(i, is_visted);
}
}
}
void DFS(const V& src)
{
int srci = GetVertexIndex(src);
vector<bool> is_visted(_vertexs.size(), false);
is_visted[srci] = true;
_DFS(srci,is_visted);
}
void TestDFS()
{
string a[] = { "A", "B", "C", "D", "E","F","G","H","I" };
Graph<string, int> g1(a, sizeof(a) / sizeof(string));
g1.AddEdge("A", "B", 1);
g1.AddEdge("A", "C", 1);
g1.AddEdge("A", "D", 1);
g1.AddEdge("B", "E", 1);
g1.AddEdge("B", "C", 1);
g1.AddEdge("C", "F", 1);
g1.AddEdge("C", "B", 1);
g1.AddEdge("D", "F", 1);
g1.AddEdge("E", "G", 1);
g1.AddEdge("F", "H", 1);
g1.AddEdge("H", "I", 1);
g1.DFS("A");
}
/*主函数就自由发挥吧。*/
先来熟悉一下概念:
简单的说就是从由n个顶点组成的连通图中选择n-1条边,子图连通且所边的权值相加最小。
实现方法下面介绍克鲁斯卡尔和普里姆两种算法。
- 首先将所有的边管理起来,每次取出最小的边。
- 判断已经选出的边是否构环,如果构成就弃置再从中选最小的边。
- (n个顶点构成的图)选择n-1条边即可。
/*
为方便读者进行阅读,此处博主贴了一份并查集的简略代码。
*/
template<class T>
class UnionFindSet
{
public:
//初始化大小,以及赋初值
UnionFindSet(size_t size)
:_pPath(size, -1)
{}
//将两个数进行合并
void Union(int x1, int x2)
{
//找两个数的父结点
int index1 = find(x1);
int index2 = find(x2);
//如果相同则说明已经在同一个集合下,无需进行合并
if (index1 == index2) return;
//将小的和在大的身上(优化防止路径过长)
if (_pPath[index1] < _pPath[index2])
{
swap(index1, index2);
swap(x1, x2);
}
//此处保证index1的父节点的数量多,index2的数量小
_pPath[index1] += _pPath[index2];
_pPath[index2] = index1;
}
//找根
int GetValueIndex(int x)
{
//第一步:转换为下标
int index = x;
//第二步:根据下标找父节点
while (_pPath[index] >= 0)
{
index = _pPath[index];
}
//找到父路径进行返回。
//路径压缩
while (x != index)
{
int parent = _pPath[x];
_pPath[x] = index;
x = parent;
}
return index;
}
int setsize()
{
int n = 0;
for (int e : _pPath)
if (e < 0) n++;
return n;
}
private:
vector<int> _pPath;
};
/*
此结构体用于存放边的信息,放入优先级队列中便于进行管理。
*/
template<class W>
struct Edge
{
int _srci;
int _dsti;
W _w;
Edge(const int srci, const int dsti, const W& w)
:_srci(srci), _dsti(dsti), _w(w)
{}
bool operator >(const Edge e) const
{
return _w > e._w;
}
};
W Kruskal(self& min)
{
min._vertexs = _vertexs;
//第一步,用优先级队列存放所有的边
priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
size_t n = _vertexs.size();
//无向图,只需存放一半的图的信息即可。
for (size_t i = 0; i < n; i++)
{
for (size_t j = 0; j < i; j++)
{
if (_matrices[i][j] != W_MAX)
{
minque.push(Edge(i, j, _matrices[i][j]));
}
}
}
//第二步,选边,最小生成树,选择的边为 n-1条边
size_t size = 0;
UnionFindSet<int> u(n);
W total = W();
while (!minque.empty() && size != n-1)
{
Edge top = minque.top();
minque.pop();
if (u.find(top._dsti) != u.find(top._srci))
{
//说明不构成环,选择此边,并将其加入到并查集和表中
//此处是为了方便测试。
cout << _vertexs[top._dsti] << "->"
<< _vertexs[top._srci]<< ":" << top._w << endl;
u.Union(top._dsti, top._srci);
min._AddEdge(top._dsti, top._srci, top._w);
size++;
total += top._w;
}
}
//队列为空跳出循环,因此需要判断一下看是否选出了n-1条边。
if (size != n - 1)
{
//表明不能选出来
return W();
}
return total;
}
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(strlen(str));
cout << "Kruskal:" << g.Kruskal(kminTree) << endl;
}
/*main函数自由发挥吧*/
- 将顶点分为两个集合,设一个集合为X, 一个集合为Y。
- 选择一个起始点,放入X集合,剩余的顶点放入Y集合。
- 每次选择从Y中选择与X相连的最小的边,并将其相连的顶点放入X集合,从Y中丢弃此顶点。
- 直到选择 n - 1条边为止。
W Prim(self& min,const V& src)
{
size_t n = _vertexs.size();
min._vertexs = _vertexs;
/*
第一步:选择顶点,作为起始顶点。分为两个数组,一个为起始数组
,一个为选边数组
*/
int srci = GetVertexIndex(src);
vector<bool> X(n,false);
vector<bool> Y(n,true);
X[srci] = true;
Y[srci] = false;
//第二步:将与srci相关的边入队列中。
priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
for (size_t i = 0; i < n; i++)
{
//将边进行入队列
if (_matrices[srci][i] != W_MAX)
{
minque.push(Edge(srci, i, _matrices[srci][i]));
}
}
//第三步进行选边
W total = W();
size_t size = 0;
while (!minque.empty())
{
Edge front = minque.top();
minque.pop();
//判断边的终点是否在X中
if (X[front._dsti])
{
//说明构成环。
cout << "构成环:";
cout << _vertexs[front._srci] << "->"
<< _vertexs[front._dsti] << endl;
}
else
{
cout << _vertexs[front._srci] << "->"
<< _vertexs[front._dsti] << endl;
++size;
total += front._w;
//将边添加到最小生成树里面,并将与dsti相连的边入队列
min._AddEdge(front._srci, front._dsti, front._w);
//将desi所在的集合进行删除与添加
Y[front._dsti] = false;
X[front._dsti] = true;
//将dsti所连的边进行入队列
for (size_t i = 0; i < n; i++)
{
//避免将已经入过的边再进行入队列
if (_matrices[front._dsti][i] != W_MAX
&& Y[i])
{
//不在X[i] 即将在Y[i]进行入队列。
minque.push(Edge(front._dsti, i,
_matrices[front._dsti][i]));
}
}
}
}
//如果不能生成最小生成树。
if (size != n - 1)
{
return W();
}
return total;
}
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> pminTree(strlen(str));
cout << "Prim:" << g.Prim(pminTree, 'a') << endl;
pminTree.Print();
}
/*main 函数只需调用此函数即可*/
运行结果:
- 举个例子,在现实世界中我们已经不关心两个地方能不能到的问题了,我们主要关系的是两个地方如何规划路程最短或者花费最低,诸如此类的问题,抽象到计算机即转换为了两个顶点所经过的路径的权值之和如何才能最短。
由此,我们引出迪杰斯特拉(Dijkstra), 贝尔曼福特(Bellman-Ford), 弗洛伊德(floyd warshall) 三种算法。
此算法主要求的是不带负权值最小路径。
算法思想主要在单源最短路径中进行体现。
- 确定一个起始点,更新与其
直接
相连的顶点的路径。- 选择路径和最短的那一个,此处
确定
了第一条路径最短的边。
- 确定两字我们此处再稍作解释,由于已经选择了起始点直接到路径最短的顶点。因此不可能再出现,从起始点到另一个顶点再经过其它顶点到此点的路径和更短,更简单的表述是两点直接连着已经最短的了,再通过其它点绕远路只会更长,不会更短。
- 此处用数学的语言进行描述或许更加直观。
- 再由最短的那个顶点,再更新(如果更小再进行更新)与其直接相连的边,再确定一条路径最短的边的顶点。由此顶点再进行更新。
- 如此往复,直到没有顶点可以更新,就结束。
void Dijkstra(const V& src, vector<W>& dst, vector<int>& pPath)
{
//将边与路径进行初始化
size_t n = _vertexs.size();
int srci = GetVertexIndex(src);
//值初始化为W_MAX
dst.resize(n, W_MAX);
//路径初始化为-1
pPath.resize(n, -1);
//src->src路径值初始化为W(),路径初始化为srci
dst[srci] = W();
pPath[srci] = srci;
//创建一个bool的vector使得每个结点只访问一次
vector<bool> is_visted(n, false);
for (size_t i = 0; i < n; i++)
{
W min = W_MAX;
int vertexi = 0;
//先选出没被访问过的最小的边
for (size_t j = 0; j < n; j++)
{
if (!is_visted[j] && dst[j] < min)
{
min = dst[j];
vertexi = j;
}
}
//选出之后标记为选过的边
is_visted[vertexi] = true;
//再进行松弛更新与其相连的边
for (size_t j = 0; j < n; j++)
{
/*
首先得有边,且是顶点没有访问的点,并且
srci->vertex + vertex->j < srci->j,再进行更新
*/
if (_matrices[vertexi][j] != W_MAX && !is_visted[j]
&& dst[vertexi] + _matrices[vertexi][j] < dst[j])
{
//更新j的父路径和srci->j的距离
pPath[j] = vertexi;
dst[j] = dst[vertexi] + _matrices[vertexi][j];
}
}
}
}
- 此处对这里的pPath进行说明一下,是将路径进行压缩从二维降到了一维,但其实也很简单,本质与并查集的路径表示大致一样,下标存的是父节点的下标。
- 另外,这里打印时因为每个结点表示的是父结点的下标,因此我们还需将路径倒着找到之后,再翻转成正向的,再进行打印。
void PrinrtShotPath(const V& src, vector<W>& dst, vector<int>& pPath)
{
int srci = GetVertexIndex(src);
size_t n = _vertexs.size();
//先找到路径再进行逆置
for (size_t i = 0; i < n; i++)
{
//不能是srci,要不然就陷入环了。
if (i != srci)
{
vector<int> path;
int parent = i;
while (parent != srci)
{
path.push_back(parent);
parent = pPath[parent];
}
//最后将srci根结点入进去
path.push_back(srci);
//逆转path得到路径
reverse(path.begin(), path.end());
for (auto index : path)
{
cout << _vertexs[index] << "->";
}
//最后打印出路径值
cout << "最短路径值为:" << dst[i] << 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);
vector<int> dist;
vector<int> parentPath;
g.Dijkstra('s', dist, parentPath);
g.PrinrtShotPath('s', dist, parentPath);
}
用处:单源最短路径的负权值(不带负权回路)的图
思想:暴力枚举遍历
- 由于只会更新出更短的路径,我们可以采取暴力枚举的方法。
- 将所有的边进行遍历,之后再遍历 n - 1 次进行修正。
- 重点就在于: 为什么
再遍历n - 1次
?我们先来讨论一下,假设你再某次更新s->x->t->z 之后,s->x->t 出现了更短的路径(存在负权值,就有可能),更新成了s->y->t,但是原来已经更新的s->x->t->z虽然路径随着s->y->t更新,但是其s->t的权值并没有进行更新,这就导致了数据对不上的问题,因此我们需要再进行更新一轮,使之数据一致。而再次更新,有可能会导致其它最短路径的权值对不上,因此还要再进行更新,直到所有的最短路径都对上为止,因此最多要n-1次,带上最开始的那一次,总共n次。
bool BellmanFord(const V& src, vector<W>& dst, vector<int>& pPath)
{
//将边与路径进行初始化
size_t n = _vertexs.size();
int srci = GetVertexIndex(src);
//值初始化为W_MAX
dst.resize(n, W_MAX);
//路径初始化为-1
pPath.resize(n, -1);
//src->src路径值初始化为W(),路径初始化为srci
dst[srci] = W();
pPath[srci] = srci;
for (size_t k = 0; k < n; k++)
{
//更新n轮,因为一个路径更新出更短的路径,会影响其它路径的权值,
//因此需要再次更新。
//一轮之后,更新出最短路径,则其它路径的权值需要暴力更新一遍。
//不带第一轮,最多更新n-1轮->其中每一轮都更新出了最短路径。
bool update = false;
for (size_t i = 0; i < n; i++)
{
for (size_t j = 0; j < n; j++)
{
//边存在,并且 s->i + i->j < s->j
if (_matrices[i][j] != W_MAX
&& dst[i] + _matrices[i][j] < dst[j])
{
update = true;
//更新父路径和权值
pPath[j] = i;
dst[j] = dst[i] + _matrices[i][j];
}
}
}
if (!update)
{
break;
}
}
//检查负权回路
//再次更新一轮,检查是否能更新,如果还能更新,则存在负权回路。
//如果没有更新,则为false,即
bool is_existed = false;
for (size_t i = 0; i < n; i++)
{
for (size_t j = 0; j < n; j++)
{
//边存在,并且 s->i + i->j < s->j
if (_matrices[i][j] != W_MAX
&& dst[i] + _matrices[i][j] < dst[j])
{
is_existed = true;
}
}
}
if (is_existed)
{
return false;
}
return true;
}
void TestGraphBellmanFord()
{
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);
vector<int> dist;
vector<int> parentPath;
if (g.BellmanFord('s', dist, parentPath))
{
g.Print();
g.PrinrtShotPath('s', dist, parentPath);
}
else
{
cout << "存在负权回路" << endl;
}
}
说明:暴力更新,调试着看数据的变化效果更好。
测试用例2:
void TestGraphBellmanFord()
{
// 微调图结构,带有负权回路的测试
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', 'x', -3);
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);
vector<int> dist;
vector<int> parentPath;
if (g.BellmanFord('s', dist, parentPath))
{
g.PrinrtShotPath('s', dist, parentPath);
}
else
{
cout << "存在负权回路" << endl;
}
}
用处:多源最短路径的负权值(不带负权回路)的图
算法思想(dp):
- 拆分子问题:分为两种情况
- 所有的边经过点K.
- 所有的边不经过点K.
- 这里的K可能是所有的顶点。
- 因此求前两种情况的所有情况的最小值即可。
void FloydWarshall(vector<vector<W>>& vvdst,
vector<vector<int>>& vvpPath)
{
size_t n = _vertexs.size();
//初始化dst与pPath
vvdst.resize(n);
vvpPath.resize(n);
for (size_t i = 0; i < n; i++)
{
vvdst[i].resize(n, W_MAX);
vvpPath[i].resize(n, -1);
}
//再对边进行初始化,即将i直接到j的边先放在des数组中
for (size_t i = 0; i < n; i++)
{
for (size_t j = 0; j < n; j++)
{
if (_matrices[i][j] != W_MAX)
{
vvdst[i][j] = _matrices[i][j];
vvpPath[i][j] = i;
}
if (i == j)
{
//与此同时由于是距离,所以i == j 即 i->i 的距离为0
vvdst[i][j] = 0;
}
}
}
for (size_t k = 0; k < n; k++)
{
//其中暴力选择k做为中间的边,分析是选择还是不选
for (size_t i = 0; i < n; i++)
{
//从中进行选则两端的边
for (size_t j = 0; j < n; j++)
{
//选择k作为中间的边,如果i->k,k->j < i->j
//即分析是取k小还是不取k小,这里的k采用暴力枚举的方式。
if (vvdst[i][k] != W_MAX && vvdst[k][j] != W_MAX
&& vvdst[i][k] + vvdst[k][j] < vvdst[i][j])
{
//则需要更新dst[i][j]的父路径以及权值
vvdst[i][j] = vvdst[i][k] + vvdst[k][j];
/*
i->k 更新 k->j,应为pPath[k][j]
如果k->j中间没有其他结点,则说明 pPath[k][j] == k
如果k->……->x->j中间经过了其它结点,则 pPath[k][j]==x
*/
vvpPath[i][j] = vvpPath[k][j];
}
}
}
}
//此处我们打印出权值和路径的矩阵
cout << " ";
for (size_t i = 0; i < n; i++)
{
printf("%-3d", i);
}
cout << endl;
//1.权值矩阵
for (size_t i = 0; i < n; i++)
{
printf("%-3d", i);
for (size_t j = 0; j < n; j++)
{
if (vvdst[i][j] == W_MAX)
{
printf("%-3c", '*');
}
else
{
printf("%-3d", vvdst[i][j]);
}
}
cout << endl;
}
printf("=============================================\n");
//2.路径矩阵
cout << " ";
for (size_t i = 0; i < n; i++)
{
cout << i << " ";
}
cout << endl;
for (size_t i = 0; i < n; i++)
{
cout << i << " ";
for (size_t j = 0; j < n; j++)
{
cout << vvpPath[i][j] << " ";
}
cout << endl;
}
}
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);
vector<vector<int>> vvDist;
vector<vector<int>> vvParentPath;
g.FloydWarshall(vvDist, vvParentPath);
// 打印任意两点之间的最短路径
for (size_t i = 0; i < strlen(str); ++i)
{
g.PrinrtShotPath(str[i], vvDist[i], vvParentPath[i]);
cout << endl;
}
}
说明:这里II的矩阵表示的数字是
真实下标对应的数字
,我们这里打印的父路径的矩阵表示的数字是下标,因此还需要对不为-1的数加上1才对的上。
我是舜华,期待与你的下一次相遇!