1.图论基础
一个图由顶点集(V)和边集(E)组成,每一条边就是一个点对(v,w),其中。如果点对是有序的(即v,w与w,v不同),那么图就是有向图,否则称作无向图(无向图是特殊的有向图)。如果在边集中允许出现(w,w)这种一个顶点的点对,这种边称为自环边。如果边集中允许出现多组相同的点对,这样的边称为平行边。
2.图的表示
图的表示实际上就是对图中边的存储,每条边由两个顶点组成
(1)邻接矩阵:假设图中有n个顶点,那么图中的边有(n*n)种可能,使用n*n的二维矩阵(bool[][])来存储边,每一行(列)表示当前顶点与图中所有其他顶点之间的邻接关系,[i][j]为true表示图中存在点对(i,j)的边,否则表示不存在。由于在二维矩阵中[i][j]有且只有一个位置与之对应,因此邻接矩阵的实现消除了平行边
(2)邻接表:假设图中有n个顶点,使用n个int数组存储与当前顶点邻接的所有点
在实际应用中,对于稠密图一般使用邻接矩阵来实现,对于稀疏图一般使用邻接表来实现。(判断是稠密图还是稀疏图的标准是每个顶点的实际连接数与理论最大连接数相比)。
代码实现如下:
SparseGraph(稀疏图)
//稀疏图,用邻接表实现(允许存在平行边,允许存在自环边)
class SparseGraph
{
private:
int mp_vertexCount; //顶点个数
int mp_edgeCount; //边条数
bool isDirected; //是否为有向图
vector<vector<int>> mp_graph;
public:
SparseGraph(int n,bool isDirected)
{
mp_vertexCount = n;
mp_edgeCount = 0;
this->isDirected = isDirected;
mp_graph = vector<vector<int>>(n,vector<int>());
}
~SparseGraph(void){}
int V(){return mp_vertexCount;} //返回顶点个数
int E(){return mp_edgeCount;} //返回边条数
//添加一条边
void addEdge(int v,int w)
{
mp_graph[v].push_back(w); //不判断是否已经存在边,过于损耗程序性能,因此该实现允许存在平行边
if(v!=w && !isDirected) //对于自环边,不需要再次插入反向边
mp_graph[w].push_back(v);
mp_edgeCount++;
}
// 验证图中是否有从v到w的边,最差情况下时间复杂度O(V)
bool hasEdge( int v , int w ){
//对于无向边,由于在插入时保证了反向边的加入,因此在判断是否存在边时无须再次区分有(无)向图
assert( v >= 0 && v < n );
assert( w >= 0 && w < n );
for( int i = 0 ; i < mp_graph[v].size() ; i ++ )
if( mp_graph[v][i] == w )
return true;
return false;
}
//打印输出边
void show()
{
for(int i=0;i<mp_vertexCount;i++)
{
cout<<i<<": ";
for(int j=0;j<mp_graph[i].size();j++)
{
cout<<mp_graph[i][j]<<",";
}
cout<<endl;
}
}
//稀疏图迭代器,传入一个图和一个顶点
class adjIterator
{
private:
//目标顶点所在图
SparseGraph &G;
//指示当前遍历的位置
int index;
//要遍历的顶点
int v;
public:
adjIterator(SparseGraph& graph,int v):G(graph)
{
this->index = 0;
this->v = v;
}
~adjIterator()
{
}
//返回目标顶点的邻接表中第一个顶点
int begin()
{
this->index = 0; //begin可能会多次调用
if(G.mp_graph[v].size() >0)
return G.mp_graph[v][index];
return -1;
}
//是否已经遍历到目标顶点邻接表尾部
bool end()
{
return this->index >= G.mp_graph[v].size();
}
//返回目标顶点邻接表中的下一个顶点
int next()
{
index++;
if(index < G.mp_graph[v].size())
{
return G.mp_graph[v][index];
}
return -1;
}
};
};
DenseGraph(稠密图)
//稠密图,使用邻接矩阵实现(不存在平行边,允许存在自环边)
class DenseGraph
{
private:
//图的总顶点个数
int mp_vertexCount;
//图的边计数
int mp_edgeCount;
//是否为有向图
bool mp_isDirected;
//图的具体数据(顶点之间的连接关系),一个mp_vertexCount*mp_vertexCount矩阵
vector<vector<bool>> mp_graph;
public:
//vertexCount为顶点数,isDirected是否为有向图
DenseGraph(int vertexCount,bool isDirected)
{
mp_vertexCount = vertexCount;
mp_edgeCount = 0;
mp_isDirected = isDirected;
mp_graph = vector<vector<bool>>(vertexCount,vector<bool>(vertexCount,false)); //初始化顶点之间全不连接(即没有边)
}
~DenseGraph(void)
{
}
int E(){return mp_edgeCount;} //返回边条数
int V(){return mp_vertexCount;} //返顶点的个数
//向图中添加边
void addEdge(int v,int w)
{
assert(v>=0 && v<=mp_vertexCount-1);
assert(w>=0 && w<=mp_vertexCount-1);
if(hasEdge(v,w)) //添加边已存在
return;
mp_graph[v][w] = true;
if(!mp_isDirected) //无向图,添加反向连接
{
mp_graph[w][v] = true;
}
mp_edgeCount++;
}
//判断两顶点是否连接,时间复杂度O(1)
bool hasEdge(int v,int w)
{
assert(v>=0 && v<=mp_vertexCount-1);
assert(w>=0 && w<=mp_vertexCount-1);
return mp_graph[v][w];
}
//打印输出图中所有边
void show()
{
for(int i=0;i<mp_vertexCount;i++)
{
cout<<i<<": ";
for(int j=0;j<mp_vertexCount;j++)
{
cout<<mp_graph[i][j]<<",";
}
cout<<endl;
}
}
class adjIterator
{
private:
//目标顶点所在图
DenseGraph& G;
//当前遍历位置
int index;
//遍历的目标顶点
int v;
public:
adjIterator(DenseGraph& graph,int v):G(graph)
{
assert(v>=0 && v<graph.V());
this->v = v;
this->index = -1; //邻接矩阵中,顶点所在行的第一个连接顶点不一定从0开始
}
~adjIterator()
{}
//邻接矩阵中
int begin()
{
index = -1; //begin可能会多次调用
return next();
}
bool end()
{
return index >= G.V();
}
int next()
{
for(index++;index<G.V();index++) //遍历邻接矩阵中v顶点所在行,返回值为true的索引
{
if(G.mp_graph[v][index])
return index;
}
return -1;
}
};
};
3.图的深度优先遍历:
基本思路:递归遍历图中指定顶点的邻接顶点,用bool数组标记图中顶点是否已被遍历过。深度遍历可用于求图的联通分量,借助于联通分量我们又可以求得两顶点是否存在路径,使用int数组记录每个顶点所属的联通分量。
template <typename Graph>
class Component
{
public:
//构造函数中求出联通分量
Component(Graph& graph):G(graph)
{
visited = new bool[G.V()];
id = new int[G.V()];
for(int i=0;i<G.V();i++)
{
visited[i] = false;
id[i] = -1;
}
ccount = 0;
for(int i=0;i<G.V();i++)
{
if(!visited[i])
{
depthFirstSearch(i);
ccount++;
}
}
}
~Component()
{
if(visited)
{
delete[] visited;
}
if(id)
{
delete[] id;
}
}
//判断两顶点是否联通(是否属于同一联通分量)
bool isConnected(int v,int w)
{
assert(v>=0 && v<G.V());
assert(w>=0 && w<G.V());
return id[v]==id[w];
}
int count()
{
return ccount;
}
private:
//深度优先遍历
void depthFirstSearch(int i)
{
id[i] = ccount; //赋值联通分量
visited[i] = true; //标记当前节点为已访问过
Graph::adjIterator ite(G,i);
for(int i=ite.begin();!ite.end();i=ite.next())
{
if(!visited[i])
depthFirstSearch(i); //使用递归实现深度优先,继续遍历连接节点的连接节点
}
}
private:
//要遍历的图
Graph& G;
//记录元素是否被访问过
bool* visited;
//联通分量
int ccount;
//每个顶点对应的联通分量标记
int* id;
};
4.获取图中的路径
在遍历的基础上,我们可以使用一个int数组来记录当前顶点是由哪个顶点遍历而来,即int* from,from[i]=v表示在当前图的遍历中,顶点i是在遍历顶点v的邻接顶时被访问到。如果要求到任意顶点m的路径,只需要根据from[i] = m反推回去,就可以得到一条完整的路径。下面给出代码实现:
template<typename Graph>
class Path
{
public:
Path(Graph& graph,int source):G(graph)
{
//初始化
this->s = source;
visited = new int[G.V()];
from = new int[G.V()];
for(int i=0;i<G.V();i++)
{
visited[i] = false;
from[i] = -1;
}
//深度优先遍历source源顶点
depthFirstSearch(source);
}
~Path(void)
{
delete[] visited;
delete[] from;
}
//顶点v与源顶点是否存在连通路径
bool hasPath(int v)
{
return visited[v];
}
//获取顶点v与源顶点连通路径
void path(int v,vector<int> &vec)
{
stack<int> _stack;
int p = v;
while(p!=-1)
{
_stack.push(p);
p = from[p];
}
vec.clear();
while(!_stack.empty())
{
vec.push_back(_stack.top());
_stack.pop();
}
}
// 打印出从s点到w点的路径
void showPath(int w)
{
assert( hasPath(w) );
vector<int> vec;
path( w , vec );
for( int i = 0 ; i < vec.size() ; i ++ ){
cout<<vec[i];
if( i == vec.size() - 1 )
cout<<endl;
else
cout<<" -> ";
}
}
private:
//深度优先遍历
void depthFirstSearch(int v)
{
visited[v] = true; //标记当前节点为已访问过
Graph::adjIterator ite(G,v);
for(int i=ite.begin();!ite.end();i=ite.next())
{
if(!visited[i])
{
from[i] = v;
depthFirstSearch(i); //使用递归实现深度优先,继续遍历连接节点的连接节点
}
}
}
private:
//寻路的图
Graph& G;
//起始点
int s;
//标记顶点是否被访问过的数据
int* visited;
//记录遍历到的每个顶点的从哪个顶点索引到
int* from;
};
5.获取最短路径(广度优先遍历)
基本思路:遍历到每一个顶点时先将其所有邻接顶点加入遍历队列中并出队当前顶点,按照队列中的顺序遍历顶点,队列为空时遍历完成。代码实现如下:
//获取无权图中两顶点间的最短路径(使用广度优先遍历)
template<typename Graph>
class ShortestPath
{
private:
Graph& G; //要遍历的图
int s; //起始点
bool* visited; //标记顶点是否在遍历队列中
int* from; //记录路径,from[i]表示查找路径上i的上一个顶点
int* ord; //记录路径中,节点的次序(即距离起始顶点的距离)
public:
ShortestPath(Graph& graph,int s):G(graph)
{
this->s = s;
visited = new bool[G.V()];
from = new int [G.V()];
ord = new int[G.V()];
for(int i=0;i<G.V();i++)
{
visited[i] = false;
from[i] = -1;
ord[i] = -1;
}
//遍历队列
std::queue<int> queue;
queue.push(s);
ord[s] = 0;
visited[s] = true;
while(!queue.empty())
{
int v = queue.front();
queue.pop();
Graph::adjIterator adjIte(G,v);
for(int i=adjIte.begin();!adjIte.end();i=adjIte.next())
{
if(!visited[i])
{
queue.push(i);
from[i] = v;
visited[i] = true;
ord[i] = ord[v]+1;
}
}
}
}
~ShortestPath(void)
{
delete[] visited;
delete[] from;
delete[] ord;
}
//获取顶点v与源顶点连通路径
void path(int v,vector<int> &vec)
{
stack<int> _stack;
int p = v;
while(p!=-1)
{
_stack.push(p);
p = from[p];
}
vec.clear();
while(!_stack.empty())
{
vec.push_back(_stack.top());
_stack.pop();
}
}
// 打印出从s点到w点的路径
void showPath(int w)
{
vector<int> vec;
path( w , vec );
for( int i = 0 ; i < vec.size() ; i ++ ){
cout<<vec[i];
if( i == vec.size() - 1 )
cout<<endl;
else
cout<<" -> ";
}
}