算法学习之--图论基础

1.图论基础

  一个图由顶点集(V)和边集(E)组成,每一条边就是一个点对(v,w),其中v,w\epsilon V。如果点对是有序的(即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<<" -> ";
		}
	}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
学习算法需要掌握一些数学基础知识,以下是一些常见的数学概念和技能,对于学习算法会有帮助: 1. 线性代数:线性代数是研究向量空间和线性映射的数学分支。了解向量、矩阵、矩阵运算、线性方程组、特征值和特征向量等概念对于理解和实现许多机器学习算法至关重要。 2. 概率与统计:概率和统计是机器学习中的核心概念。掌握概率理论、条件概率、贝叶斯定理、随机变量、概率分布、统计推断等内容,能够帮助理解概率模型、参数估计、假设检验等统计相关的算法。 3. 微积分:微积分是研究函数、极限、导数和积分的数学分支。了解导数、偏导数、梯度、极值等概念对于理解优化算法(如梯度下降)和深度学习中的反向传播算法非常重要。 4. 离散数学:离散数学是研究离散结构(如集合、图论、逻辑等)的数学分支。了解离散数学的概念和技巧对于理解算法的复杂度分析、图算法、搜索算法等非常有帮助。 此外,编程能力也是学习算法的关键技能。熟悉至少一种编程语言(如Python、Java、C++等)以及基本的数据结构和算法(如数组、链表、排序、查找等)也是必备的。 需要注意的是,数学基础只是学习算法的一部分,实践和动手能力同样重要。通过实际应用和实现算法,才能真正理解和掌握它们的原理和应用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值