图是一种非线性数据结构,由顶点集合(vertex)和边的集合组成的一种数据结构。
Graph=(V,E);
V={x|x是顶点集合}; V是顶点的集合
E={<x,y>|x,y属于V}; E是边的集合
图分为无向图和有向图。
1、无向图
2、有向图
完全图:在由n个顶点组成的无向图中,若有N(N-1)/2条边,则成为无向完全图。(也就是说任意两个点都有路径)
边的权重:权重表示对边赋予的权值。
邻接顶点:如果两个顶点之间有边,则这两个顶点就互为邻接顶点。
度:与顶点v关联的边的数目称为顶点v的度。
路径:在图G=(V,E)中,若从顶点v1出发,沿着边经过若干顶点v2、v3...到v8,则(v1,v2...v8)就是顶点v1到顶点v8的路径。
连通图:在无向图中,若任意两个顶点之间都有路径,则此图就是连通图。
强连通图:在有向图中,若每一对顶点之间都存在路径,则称此图就是强连通图。
图的存储:
图由顶点和边组成,所以在存储的时候我们不仅要存储图的顶点,还要存储图的边。图的存储有两种方式,邻接矩阵和邻接表。
邻接矩阵:将所有顶点的信息组织成一张一个顶点表,然后利用矩阵来存储顶点之间的关系。
例:
可以看到,邻接矩阵是非常耗费内存的,对于无向图来说我们可以只存下三角或者只存上三角来节省空间。但是如果我们要在邻接矩阵中寻找某一条边存在还是不存在,则邻接矩阵的效率就是O(1),直接用这两个顶点所对应的存储位置作为下标到矩阵中寻找就行了。用邻接矩阵存储边的话实际上是将所有的点都存储下来了。
邻接表:使用数组来存储顶点的集合,使用链表来存储顶点之间边的关系。
可以看到邻接表更节省空间,如果我们要求图中有多少条边的话,则用邻接表是非常快的。用邻接表存储边的话有多少条边就有多少个结点,所以它真正存储的就是边。
图的遍历:
图的遍历右两种方式,深度优先遍历(DFS)和广度(BFS)优先遍历。深度优先遍历就是沿着一条路径一直走到底,直到走不下去的时候再返回上一个路口寻找一条没走过的路径再走。广度优先遍历是一种层层递进的思想,以起点为中心,一层一层的访问。不过对于图来说,不管是哪种遍历,我们都可能再次遇到已经访问过的结点,所以在对图遍历的时候我们还需要用一个辅助数组来标记哪些结点已经访问过了,哪些结点还没有访问过。
深度优先遍历(DFS):
void DFS(const V& src) //深度优先遍历
{
vector<bool> visited(_vertax.size(),false);
_DFS(GetIndex(src),visited);
for (size_t i = 0; i < visited.size(); ++i) //如果图不是连通图的话
{
if (visited[i]==false)
_DFS(i, visited);
}
}
void _DFS(size_t src,vector<bool>& visited)
{
Node* cur = _tables[src];
printf("%s:->", _vertax[src].c_str());
visited[cur->_src] = true; //将已经访问过的结点标记起来
while (cur)
{
if (visited[cur->_dst] == false)
_DFS(cur->_dst, visited);
cur = cur->_next;
}
}
广度优先遍历(BFS):
void BFS(const V& src)
{
size_t index = GetIndex(src);
vector<bool> visited(_vertax.size(),false);
queue<size_t> q;
q.push(index); //将起点的坐标入队
while (!q.empty())
{
size_t front = q.front();
q.pop();
cout << _vertax[front].c_str() << "->";
visited[front] = true;
Node* cur = _tables[front];
while (cur)
{
if (visited[cur->_dst] == false)
{
q.push(cur->_dst);
visited[cur->_dst] = true; //如果这个元素已经入队了,则就标记为true
}
cur = cur->_next;
}
}
}
最小生成树:
首先要明白最小生成树的概念是针对于无向图而言的。对于一个连通图,图中有N个顶点。如果我们能在连通图中选取N-1条边将图的所有顶点连在一起且不构成回路,则这N-1条边就构成了一个生成树。如果这N-1条边的权值和最小,则就构成了最小生成树。
构造最小生成树时有两个算法,分别是Kruskal算法和Prim算法,这两个算法都使用了贪心法求解。但是要注意:使用贪心算法在求解最小生成树的时候总是得到了局部最优解,但是整体结果不一定最优解。
Kruskal算法:Kruskal算法是一种找边的思想。假设初始时有两个存放边的集合为E1和E2,E1里面存放了连通图的所有边,E2是空集合。
1、在E1里面找一条权值最小的边edge,并将edge从E1里面删除
2、判断edge是否与E2里面的边能形成回路。
3、如果构成回路则就丢弃这条边,从步骤1重新开始。
4、如果不构成回路,则就将edge加入到E2中。
5、如果E2中有N-1条边的话,则就停止,这时候最小生成树就已经找出来了。否则就重复上述步骤。
问:怎么找最短的边?
可以将利用最小堆来实现,将连通图的所有边构成一个最小堆。
问:怎么判断是否构成回路?
可以用一个并查集ufs,将连通图的所有顶点都加入到并查集里面,将E2里面的所有边的顶点都并入一个集合,这样的话如果edge的两个顶点是在一个集合中,则它就会构成回路,否则不会构成回路。
实现:将最小生成树存放在minTree里面
bool MinTree(GraphList<V,E>& minTree)
{
if (_isdirect) //如果是有向图的话,则就直接返回false
{
return false;
}
minTree._vertex = _vertex; //将这个图的所有顶点都放入到minTree里面
minTree._tables.resize(_tables.size());
struct Compare
{
bool operator()(Node* l,Node* r)
{
return (l->_weight) >(r->_weight);
}
};
//建立一个小堆,用来查找图里面最小的边
vector<Node*> minHeap;
for (size_t i = 0; i < _tables.size();++i)
{
Node* cur = _tables[i];
while (cur)
{
minHeap.push_back(cur);
cur = cur->_next;
}
}
make_heap(minHeap.begin(),minHeap.end(),Compare()); //建小堆
size_t n = 0;
UnionSet ufs(_vertex.size());
while (n < _tables.size() - 1)
{
if (minHeap.empty())
{
return false;
}
pop_heap(minHeap.begin(),minHeap.end(),Compare());
Node* minEdge = minHeap.back(); //得到最小边
minHeap.pop_back();
//到并查集里面判断加入这条边会不会形成环
size_t root1 = ufs.GetRoot(minEdge->_src);
size_t root2 = ufs.GetRoot(minEdge->_dst);
if (root1!=root2) //这两个结点不再并查集里面
{
ufs.Merge(minEdge->_src, minEdge->_dst); //把这条边添加到并查集里面
minTree._AddEdge(minEdge->_src, minEdge->_dst,minEdge->_weight); //添加这条边
n++;
}
}
return true;
}
Prim算法:Prim算法是一种找点的思想。初始时有两个存放点的集合V1和V2,V1里面存放了连通图里面任意的N-1个顶点,V2是里面存放了1个顶点。
1、在V1和V2里面各选一个点,将他们组成的边的所有情况存放在集合E里面。
2、假设v1和v2是分别从V1和V2里面选择出来的点,并且v1和v2构成的边是E里面最小的。
3、将v1加入到V2里面,并将v1从V1里面删除。
4、如果V1不为空,则重复上述步骤。
问:怎么找E里面最短的边?
可以将利用最小堆来实现,将E中的所有边构成一个最小堆。
问:怎么判断是否构成回路?
可以用一个并查集ufs,将连通图的所有顶点都加入到并查集里面,将E2里面的所有边的顶点都并入一个集合,这样的话如果edge的两个顶点是在一个集合中,则它就会构成回路,否则不会构成回路.
实现:将最小生成树存放在minTree里面
//最小生成树就是用N-1条边将图中的所有顶点都连接起来,而且这些边的权重之和要最小
//普里姆算法
bool MinTree(GraphList<V, E>& minTree)
{
if (_isdirect) //如果是有向图的话,则就直接返回false
{
return false;
}
minTree._vertex = _vertex; //将这个图的所有顶点都放入到minTree里面
minTree._tables.resize(_tables.size());
struct Compare
{
bool operator()(Node* l,Node* r)
{
return l->_weight > r->_weight;
}
};
vector<Node*> minHeap;
UnionSet ufs(_vertex.size()); //建立一个并查集,最开始每个顶点都是一个单独的集合
size_t n = 0;
Node* cur = _tables[0];
while (n<_tables.size()-1)
{
while (cur) //将这个点所有的边都放入最小堆中
{
minHeap.push_back(cur);
push_heap(minHeap.begin(),minHeap.end(),Compare());
cur = cur->_next;
}
pop_heap(minHeap.begin(),minHeap.end(),Compare());
if (minHeap.empty())
{
return false;
}
Node* minEdge = minHeap.back();
minHeap.pop_back();
size_t root1 =ufs.GetRoot(minEdge->_src);
size_t root2 =ufs.GetRoot(minEdge->_dst);
if (root1!=root2)
{
ufs.Merge(minEdge->_src, minEdge->_dst);
minTree._AddEdge(minEdge->_src, minEdge->_dst, minEdge->_weight);
cur = _tables[minEdge->_dst];
n++;
}
}
return true;
}
完整代码:
//邻接矩阵表示法
//邻接矩阵表示图
template<typename V,typename E>
class GraphMatrix
{
public:
GraphMatrix(V* vertax,size_t n,bool isdirect=false) //默认是无向图
:_size(n)
, _isdirect(isdirect)
{
_vertex = new V[n]; //开辟n个大小的数组用来表示图的顶点
_matrix = new E* [n]; //开辟一个n*n的二维数组用来保存边
for (size_t i = 0; i < n; ++i)
{
_vertex[i] = vertax[i];
_matrix[i] = new E[n];
for (size_t j = 0; j < n; ++j)
{
_matrix[i][j] =E();
}
}
}
~GraphMatrix()
{
delete[] _vertex;
for (size_t i = 0; i < _size; ++i)
{
delete[] _matrix[i];
}
delete [] _matrix;
}
void AddEdge(const V& src,const V& dst,const E& w)
{
size_t line = GetIndex(src);
size_t row = GetIndex(dst);
_matrix[line][row] = w;
if (_isdirect == false) //无向图
{
_matrix[row][line] = w;
}
}
protected:
size_t GetIndex(const V& src) //得到顶点所在_vertex中的下标
{
for (size_t i = 0; i < _size; ++i)
{
if (_vertex[i] == src)
return i;
}
assert(false);
return -1;
}
private:
V* _vertex; //用来保存顶点
E** _matrix; //用来保存边
size_t _size;
bool _isdirect; //判断是不是无向图
};
//邻接表表示法
template<typename V,typename E>
class GraphList
{
public:
struct Node //内部类,在邻接表上挂的结点的类型
{
size_t _src; //_tables[i]在_vertex中的位置
size_t _dst; //与_tables[i]有边的点在_vertex中的位置
E _weight;
Node* _next;
Node(size_t src,size_t dst,E w)
:_src(src)
, _dst(dst)
, _weight(w)
, _next(NULL)
{}
};
public:
GraphList(bool isdirect = false)
:_isdirect(isdirect)
{}
GraphList(V* vertex,size_t n,bool isdirect=false)
:_vertex(vertex,vertex+n)
, _isdirect(isdirect)
{
_tables.resize(n);
}
~GraphList()
{
for (size_t i = 0; i < _tables.size(); ++i)
{
Node* cur = _tables[i];
while(cur)
{
Node* del = cur;
cur = cur->_next;
delete del;
}
}
}
void AddEdge(const V& src,const V& dst,const E& w) //添加边
{
size_t index1 = GetIndex(src);
size_t index2 = GetIndex(dst);
if (_isdirect == false) //无向图
{
_AddEdge(index1,index2,w);
_AddEdge(index2,index1,w);
}
else //有向图
{
_AddEdge(index1,index2,w);
}
}
void DFS(const V& src) //深度优先遍历
{
vector<bool> visited(_vertex.size(),false);
_DFS(GetIndex(src),visited);
for (size_t i = 0; i < visited.size(); ++i) //如果图不是连通图的话
{
if (visited[i]==false)
_DFS(i, visited);
}
}
void BFS(const V& src)
{
size_t index = GetIndex(src);
vector<bool> visited(_vertex.size(),false);
queue<size_t> q;
q.push(index); //将起点的坐标入队
while (!q.empty())
{
size_t front = q.front();
q.pop();
cout << _vertex[front].c_str() << "->";
visited[front] = true;
Node* cur = _tables[front];
while (cur)
{
if (visited[cur->_dst] == false)
{
q.push(cur->_dst);
visited[cur->_dst] = true; //如果这个元素已经入队了,则就标记为true
}
cur = cur->_next;
}
}
}
////最小生成树就是在用N-1条边将一个连通图里面的所有顶点都连接起来,并且所有边的权重之和最小
////克鲁斯卡尔算法,寻找最短的边,如果不构成回路的话,则就是最小生成树中的一条边
//bool MinTree(GraphList<V,E>& minTree)
//{
// if (_isdirect) //如果是有向图的话,则就直接返回false
// {
// return false;
// }
// minTree._vertex = _vertex; //将这个图的所有顶点都放入到minTree里面
// minTree._tables.resize(_tables.size());
// struct Compare
// {
// bool operator()(Node* l,Node* r)
// {
// return (l->_weight) >(r->_weight);
// }
// };
// //建立一个小堆,用来查找图里面最小的边
// vector<Node*> minHeap;
// for (size_t i = 0; i < _tables.size();++i)
// {
// Node* cur = _tables[i];
// while (cur)
// {
// minHeap.push_back(cur);
// cur = cur->_next;
// }
// }
// make_heap(minHeap.begin(),minHeap.end(),Compare()); //建小堆
// size_t n = 0;
// UnionSet ufs(_vertex.size());
// while (n < _tables.size() - 1)
// {
// if (minHeap.empty())
// {
// return false;
// }
// pop_heap(minHeap.begin(),minHeap.end(),Compare());
// Node* minEdge = minHeap.back(); //得到最小边
// minHeap.pop_back();
// //到并查集里面判断加入这条边会不会形成环
// size_t root1 = ufs.GetRoot(minEdge->_src);
// size_t root2 = ufs.GetRoot(minEdge->_dst);
// if (root1!=root2) //这两个结点不再并查集里面
// {
// ufs.Merge(minEdge->_src, minEdge->_dst); //把这条边添加到并查集里面
// minTree._AddEdge(minEdge->_src, minEdge->_dst,minEdge->_weight); //添加这条边
// n++;
// }
// }
// return true;
//}
//最小生成树就是用N-1条边将图中的所有顶点都连接起来,而且这些边的权重之和要最小
//普里姆算法
bool MinTree(GraphList<V, E>& minTree)
{
if (_isdirect) //如果是有向图的话,则就直接返回false
{
return false;
}
minTree._vertex = _vertex; //将这个图的所有顶点都放入到minTree里面
minTree._tables.resize(_tables.size());
struct Compare
{
bool operator()(Node* l,Node* r)
{
return l->_weight > r->_weight;
}
};
vector<Node*> minHeap;
UnionSet ufs(_vertex.size()); //建立一个并查集,最开始每个顶点都是一个单独的集合
size_t n = 0;
Node* cur = _tables[0];
while (n<_tables.size()-1)
{
while (cur) //将这个点所有的边都放入最小堆中
{
minHeap.push_back(cur);
push_heap(minHeap.begin(),minHeap.end(),Compare());
cur = cur->_next;
}
pop_heap(minHeap.begin(),minHeap.end(),Compare());
if (minHeap.empty())
{
return false;
}
Node* minEdge = minHeap.back();
minHeap.pop_back();
size_t root1 =ufs.GetRoot(minEdge->_src);
size_t root2 =ufs.GetRoot(minEdge->_dst);
if (root1!=root2)
{
ufs.Merge(minEdge->_src, minEdge->_dst);
minTree._AddEdge(minEdge->_src, minEdge->_dst, minEdge->_weight);
cur = _tables[minEdge->_dst];
n++;
}
}
return true;
}
protected:
void _DFS(size_t src,vector<bool>& visited)
{
Node* cur = _tables[src];
printf("%s:->", _vertex[src].c_str());
visited[cur->_src] = true; //将已经访问过的结点标记起来
while (cur)
{
if (visited[cur->_dst] == false)
_DFS(cur->_dst, visited);
cur = cur->_next;
}
}
void _AddEdge(size_t src, size_t dst, const E& w)
{
Node* node = new Node(src,dst,w);
node->_next = _tables[src];
_tables[src] = node;
}
size_t GetIndex(const V& src)
{
for (size_t i = 0; i < _vertex.size(); ++i)
{
if (_vertex[i] == src)
return i;
}
assert(false);
return -1;
}
private:
vector<V> _vertex; //用来存储顶点
vector<Node*> _tables;
bool _isdirect;
};