图论(Graph theory)

news2024/11/20 13:18:21

抽象数据结构类型

Graphic操作接口

操作接口功能描述操作接口功能描述
e()获取图的总边数n()顶点的总数
exits(v,u)判断v,u两个顶点是否存在边insert(v)
在顶点集 V 中插入新顶点 v
remove(v,u)删除从v 到u的 关联边
remove(v)
将顶点 v 从顶点集中删除
type(v,u)边所属的类型(主要用于遍历树~)
inDegree(v)
outDegree(v)
顶点 v的 入度、出度
edge(v,u)        该边所对应的 数据域
firstNbr(v)
顶点 v的 首个邻接顶点
weight(v,u)该边所对应的 权重
nextNbr(v, u)
v的 邻接顶点中, u 的后继
status(v)
 顶点的状态
parent(v)
顶点 v 在遍历树中的父节点

Graphic抽象数据结构设计

🎈 顶点类:

// 节点简化封装
template<typename Tv> 
struct Vertex
{
	// 顶点val
	Tv _data;
	// 入度、出度、 父节点
	int _inDegree, _outDegree, _parent;
	VStatus _stat;
		// 构造新节点
	Vertex(const Tv& data = (Tv)0) :_data(data), _inDegree(0), _outDegree(0), _stat(UNDISCOVERED), _parent(-1){}
};

        其包含该顶点表示的数据域,以及顶点状态、入、出度数量等

🎈 关联边类:

// 边对象优化
template<typename Te>
struct Edge
{
	//数据、权重、类型
	Te _data;
	int _weight;
	EType _type;
	Edge(const Te& data, int weight) :_data(data), _weight(weight), _type(UNDETERMINED){}
};

        关联边最直接的表现是两个顶点的连接情况~ 但在有些应用场景中,需要关联边的存储记录更详尽的数据,例如: 从四川开往北京的火车中,从成都 - 北京是一条线,从成都-西安-太原-北京又是另一条路线。这条线可以有这些数据: 路程长度、价格、时间长短,这就是关联便所能表示的数据域。

🎈 图类:

template<typename Tv,typename Te>
class Graph
{
protected:
	int _n = 0;  //顶点总数
	int _e = 0;	 //边总数

public:
	// 顶点
	virtual Tv& vertex(int) = 0;					 // 顶点v癿数据(诠顶点癿确存在)
	virtual int inDegree(int) = 0;					 // 顶点v癿入度(诠顶点癿确存在)
	virtual int outDegree(int) = 0;					 //顶点v癿出度(诠顶点癿确存在)
	virtual VStatus& status(int) = 0;				 //顶点v癿状态
	virtual int& parent(int) = 0;					 //顶点v在遍历树中癿父亲

	virtual int insert(Tv const&) = 0;				 // 插入顶点,返回编号
	virtual Tv remove(int) = 0;						 // 初除顶点及其关联边,返回顶点信息
	virtual int firstNbr(int) = 0;					 //顶点v癿首个邻接顶点
	virtual int nextNbr(int, int) = 0;				 //顶点v的(相对亍顶点j癿)下一邻接顶点

	// 边
	virtual bool exists(int, int) = 0;				    //边(v, u)是否存在
	virtual void insert(Te const&, int, int, int) = 0;  //在顶点v和u乀间揑入权重为w癿边e
	virtual Te remove(int, int) = 0;					//初除顶点v和u之间的边e,返回该边信息
	virtual EType& type(int, int) = 0;					//边(v, u)的类型
	virtual Te& edge(int, int) = 0;						//边(v, u)的数据 
	virtual int& weight(int, int) = 0;					//边(v, u) 的权重

    // ... 在这里我只列举一部分出来
};

        这里选择的是模板类的设计,是为了兼容不同图的存储方式。其中,声明大量的纯虚函数,其本质就是为了让 子类全然重写这些函数。

🎃 邻接矩阵

// 基类进行重写
template<typename Tv,typename Te>
class GraphMatrix :public Graph<Tv, Te>
{
private:
	// 数据抽象管理  —— 邻接表
	vector<Vertex<Tv>> _V;   // 顶点集合 —— 多少个顶点被插入
	vector<vector<Edge<Te>*>> _E; // 边集合

    // ...  完成 虚函数重写~
}
接口函数实现:
{
	// 顶点操作
	virtual Tv& vertex(int i) { return _V[i]._data; }
	virtual int inDegree(int i) { return _V[i].__inDegree; }
	virtual int outDegree(int i) { return _V[i]._outDegree; }
	virtual VStatus& status(int i) { return _V[i]._status; }
	virtual int& parent(int i) { return _V[i]._parent; }
	virtual int firstNbr(int i) { return nextNbr(i, _n); }

    // 边的操作
	virtual EType& type(int i, int j) { return _E[i][j]->_type; }   // 边(i, j)的类型
	virtual Te& edge(int i, int j) { return _E[i][j]->_data; }	  //边(i, j)的数据
	virtual int& weight(int i, int j) { return _E[i][j]->_weight; } //边(i, j)的权重
	virtual bool exists(int i, int j)
	{
				// 保证不越界
		return ((i >= 0) && (i < _n) && (j >= 0) && (j < _n)) && _E[i][j] != nullptr;
	}

}
          接口插入与删除的实现:        
{
		// 顶点动态插入  返回的是插入节点的 迭代器
		virtual int insert(const Tv& vertex)
		{
			// 每个顶点新曾一列  —— 用于 关联这个新节点
			for (int j = 0;j < _nVertexs;++j) _E[j].insert(nullptr);
		
			// 创建该顶点 以及对应的 关联边默认为:nullptr
			//vector<Edge*> new_matrix(n, nullptr);
			//_E.push_back(new_matrix);
			++_nVertexs;
			_E.insert(_nVertexs, _nVertexs, (Edge<Te>*)nullptr);
			return _V.insert(Vertex<Tv>(vertex));
		}

		// 顶点移除 初除第i个顶点及其兲联边(0 <= i < n)
		virtual Tv remove(int i)
		{
			// 删除其所有的 入边
			// i -> j
			for (int j = 0;j < _nVertexs;++j)
				if (exists(i, j)) { delete _E[i][j]; _V[j]._inDegree--; }	// 当我们要删除某个顶点时,先将其边删除
 
			// 删除其所有的 出边
			// j -> i
			for (int j = 0;j < _nVertexs;++j)
				if (Edge<Te>* e = _E[j].erase(i)) { delete e; _V[j]._outDegree--; }

			// 移除边
			_E.erase(i);
			_nVertexs--;

			// 获取备份 + 从顶点集中移除
			Tv vbak = vertex(i);
			_V.erase(i);

			return vbak; // 返回被删除顶点的信息
		}
    
/
		// 边的动态操作 针对两点 之间的边操作~
		virtual void insert(const Te& edge,int w,int i,int j)  // 针对带 权值的边e
		{
			if (exists(i, j)) return; // 已经存在边 不能插入了~
			_E[i][j] = new Edge<Te>(edge, w);	// 创建新边
			++_eEdges;
			// 更新关联点的 出入度
			// [i,j]  i --> j
			_V[i]._outDegree++;
			_V[j]._inDegree++;
		}

		virtual Te remove(int i, int j)
		{
			Te ebak = edge(i, j);
			delete _E[i][j];
			_E[i][j] = nullptr;

			++_eEdges;
			_V[i]._outDegree--;
			_V[j]._inDegree--;
			return ebak;
		}
}

      

🎃 邻接表

        关于邻接表如何进行的存储以及接口实现并不在此表述,只是稍微进行写一份伪代码~

	template<typename Tv, typename Te>
	class GraphTables :public Graph<Tv, Te>
	{
	private:
		vector<Vertex<Tv>> _V; // 顶点集合
		vector<Edge<Te>*> _tables; // 与顶点集合的下标对应 存储 真正的临界表~
		unordered_map<Vertex<Tv>,int> _tablesMap; // 这里我们采用哈希结构 快速找到与顶点V 相关联的边

	public:
		virtual int insert(const Tv& vertex)
		{
			++_n;
			// 直接入 顶点集合即可~ 
			return _V.insert(Vertex<Tv>(vertex));
		}

		virtual void insert(const Te& edge, int w, int i, int j)
		{
			if (exists(i, j)) return;
			Edge<Te>* new_edge = new Edge<Te>(edge, w);
			// 你要进入哪个表?
			int idx = _tablesMap[vertex(i, j)];
			// 进行头插
			new_edge->_next = _tables[idx];
			_tables[idx] = new_edge;
			
			int idx2 = _tablesMap[vertex(j, i)];
			_V[idx]._outDegree++;
			_V[idx]._inDegree++;
			++_e;
		}
        // .... and so on ....
	};

 注: 这里的所有实现本身就是一份伪代码,只是具有部分参考意义~


图的搜索遍历

广度优先搜索

template<typename Tv, typename Te>
void Graph<Tv, Te>::bfs(int s)
{
	// 从每一个节点进行遍历~
	int v = s; // s: 0 ~ n
	do
	{
		if (status(v) == UNDISCOVERED) {
			// 该节点没有被遍历 进行BFS
			BFS(v);
		}
	} while (s != (v = (++v % n));	// 这里本质就是从第一个节点开始向后遍历~ 当v==n-1时 就会退出

	
}

template<typename Tv, typename Te>
void Graph<Tv, Te>::BFS(int v)
{
	// 引入辅助队列 进行层序遍历~
	queue<int> que;
	status(v) = DISCOVERED; // 标记v已经被访问
	while (!que.empty())
	{
		int front = que.front();
		que.pop();

		// firstNbr: u从n开始-- 直到找到 有关联的节点v
		// 否则返回 -1
		for (int u = firstNbr(front); -1 < u; u = nextNbr(front, u))
		{
			if (status(u) == UNDISCOVERED) {
				// 该顶点尚未被 发掘插入进队列
				status(u) = DISCOVERED;
				que.push(u);

				// 记录其他信息 这些步骤 对于简单的BFS或DFS可以不用
				parent(u) = v;
				type(v, u) = TREE;	// 引入树边拓展支撑树
			}
			else {
				type(v, u) = CROSS	// 该节点 跨边了
			}
		}
		status(front) = VISITED; // 该节点被访问了~
	}
}

        "bfs()" 将所有存在于顶点集中的 顶点挨个进行迭代,每个顶点是否需要进行“BFS()”的根据在于是否处于 "UNDISCOVERED".

        “BFS()”的每一步迭代,都需要从队列que中取出当前的 “首顶点”v,再对其邻居u顶点逐一筛查 —— 如果是“UNDISCOVERED” 入队列,否则记录 “CROSS”状态(这个状态再当前不考虑)~

        BFS()遍历结束后,所有访问过的顶点通过 parent[]指针依次联接,从整体上给出了原图 某一连通或可达域的一棵遍历树。

深度优先搜索

template<typename Tv, typename Te>
void Graph<Tv, Te>::dfs(int s)
{
	int v = s; // s: 0 ~ n
	do
	{
		if (status(v) == UNDISCOVERED) {
			BFS(v);
		}
	} while (s != (v = (++v % n));
}

template<typename Tv, typename Te>
void Graph<Tv, Te>::DFS(int v)
{
	status(v) = DISCOVERED;  // 标记该 顶点已经被遍历
	for (int u = firstNbr(v); -1 < u; u = nextNbr(v, u))
	{
		// 找到自己邻居顶点u
		switch (u)
		{
			case UNDISCOVERED:
				// u尚未被发现 就以它进行 拓展~
				type(v, u) = TREE;
				parent(u) = v;
				DFS(u);
				break;
			case DISCOVERED:
				// u尚未访问完毕 v是u的祖先节点~
				type(v, u) = BACKWARD;
				break;
			default:
				break;
		}
	}
	status(v) = VISITED; // 访问完毕~ ——> 该顶点的所有邻居顶点都完成了访问~
}

        深度优先搜索dfs通常都是使用递归方法来实现的,每一递归实例中,都先将当前节点v标记为 “DISCOVERED”,再逐一核对其各邻居u的状态并做相应处理。待其所有邻居均已处理完毕之后,将顶点v置为VISITED(访问完毕)状态,便可回溯。 

        若顶点u尚处于UNDISCOVERED,归类(v,u)的父子关系后,继续以u作为首顶点进行遍历。

        DFS(s)返回后,所有访问过的顶点通过parent[]指针依次联接从整体上给出了顶点s所属连通或可达分量的一棵遍历树。

注: 这里的讨论仅限于 ”无向图“


有关图的其他应用

拓扑排序

template <typename Tv, typename Te>
stack<Tv>* Graph<Tv, Te>::tSort(int s)
{
	// 用于记录 拓扑顺序
	stack<Tv>* st = new  stack<Tv>(); 
	// 我们用每个节点进行拓扑~
	do {
		if (UNDISCOVERED == status(v)) {
			if (!TSort(v, st)) {
				while (!st->empty())
					st->pop();
			}
		}
	}while (s != (v = (++v % n)));
	return st;
}

template <typename Tv, typename Te>
bool Graph<Tv, Te>::TSort(int v,stack<Tv>* st)
{
	// 枚举v所在的顶点
	for (int u = firstNbr(v), -1 < u;u = nextNbr(v, u))
	{
		switch (status(u))
		{
			case UNDISCOVERED:
				parent(u) = v;
				type(u, v) = TREE;
				// 继续以u进行 拓扑
				if (!TSort(u, st)) {
					return false;
				}
				break;
			case DISCOVERED:
				// 出现了指向前驱 顶点的指针
				type(v, u) = BACKWARD;
				return false;
			default:
				// visited
				return false;
		}
	}
	// 说明以该顶点进行拓扑是顺利的
	status(v) = VISITED;
	st->push(v);
	return true;
}

        相对于标准的DFS搜索算法,这里增设了一个栈结构。一旦某个顶点被标记为VISITED状态, 便随即令其入栈。如此,当搜索终止时,所有顶点即按照被访问完毕的次序,在栈中自顶向下而排列!

        如果你感兴趣,可以试试这道考察图拓扑结构的题目: 课程表 ...

双连通域分解

template <typename Tv, typename Te>
void Graph<Tv, Te>::bcc(int s)
{
	int v = s;
	stack<int>* st = new stack<int>();
	do {
		if (UNDISCOVERED == status(v)) {
			BCC(v, st);
			// 访问完成
			st->pop();
		}
	} while (s != 0);
}
#define hca(x) (fTime(x)) // 利用此处闲置的fTime[]充当hca[]
template<typename Te,typename Tv>
void Graph<Te, Tv>::BCC(int v, stack<int>* st)
{
	// 标志更新
	status(v) = DISCOVERED;
	st->push(v);

	// 遍历邻里节点
	for (int u = firstNbr(v);u > -1;u = nextNbr(v, u))
	{
		switch (status(u))
		{
			case UNDISCOVERED:
				parent(u) = v;
				type(v, u) = TREE;
				BCC(u, st);
				if (hac(u) < dtime(v)) {
					// u遍历回来后 发现自己可以指向v的先祖节点 那么v压根不能称为关节点
					hca(v) = min(hca(v), hca(u));  // 进行更新v的先祖节点
				}
				else {
					// v 是关节点 
					while (v != st->top()) {
						st->pop();
					}
				}
				break;
			case DISCOVERED:
				type(v, u) = BACKWARD; // 表示u一定是v的 先祖先
				if (u != parent(v)) hac(v) = min(hca(v), dtime(u)); // 重新找先祖先
				break;
			default:
				// visiatd
				type(v, u) = (dtime(v) < dtime(u)) ? FOWARD : CROSS;
				break;
		}
	}
	status(v) = VISITED;
}

        DFS搜索在顶点v的孩子u处返回之后,通过比较hca[u]与dTime[v]的大小,即可判断v是否关节点。通过比较hca[u]与dTime[v],则说明u及其后代无法通过后向边与v的真祖先连通,故v为关节点。

        既然栈S 存有搜索过的顶点,与该关节点相对应的双连通域内的顶点,此时都应集中存放于S顶部,故可依次弹出这些顶点。v本身必然最后弹出,作为多个连通域的联接枢纽,它应重新进栈,访问另一个连通域。

最小生成树

Edge类封装:

		struct Edge
		{
			size_t _srci;
			size_t _dsti;
			W _w;
			Edge(size_t srci, size_t dsti, const W& w)
				:_srci(srci)
				, _dsti(dsti)
				, _w(w)
			{}

			bool operator>(const Edge& e) const
			{
				return _w > e._w;
			}
		};
Kruskal算法:

		W Kruskal(Self& minTree)
		{
			// 初始化
			int n = _vertex.size();
			minTree._vertex = _vertex;
			minTree._tablemap = _tablemap;
			minTree._edges.resize(_edges.size());
			for (size_t i = 0;i < minTree._edges.size();++i)
				minTree._edges[i].resize(n, MAX_W);

			// 凭借优先级队列 选出权值最短的两条边
			priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
			for(int i=0;i<n;++i)
				for (int j = 0;j < n;++j)
				{
					if (i < j && _edges[i][j] != MAX_W)
					{
						minque.push(Edge(i, j, _edges[i][j]));
					}
				}
		
			// 选边
			size_t size = 0; // 是否满足生成树条件
			W totalW = W();
			UnionFindSet ufs(n);

			while (!minque.empty())
			{
				auto min = minque.top();
				minque.pop();
				if (!ufs.isSet(min._srci, min._dsti))
				{
					minTree._AddEdge(min._srci, min._dsti, min._w);
					ufs.Union(min._srci, min._dsti);
					size++;
					totalW += min._w;
				}
			}
			if (size == n - 1) return totalW;
			else return W();
		}
Prim算法:
		W Prim(Self& minTree, const V& vv)
		{
			size_t srci = getVertexIndex(vv);

			// 进行初始化
			int n = _vertex.size();
			minTree._vertex = _vertex;
			minTree._tablemap = _tablemap;
			minTree._edges.resize(n);
			for (int i = 0;i < n;++i)
				minTree._edges[i].resize(n,MAX_W);
			
			// 以vv作为顶点纳入minqueue
			priority_queue<Edge, vector<Edge>, greater<Edge>> minque;
			// 将与vv有关的边加入队列
			for (int i = 0;i < n;++i)
				if (_edges[srci][i] != MAX_W)
					minque.push(Edge(srci, i, _edges[srci][i]));

			// 开始选边
			size_t size = 0; // 记录选择的边数 n - 1 结束
			W totalW = W();   // 按照这个方式选择的边权数

			// 生成树其本质就是一种树形结构
			// 判断一个子图是否构成回路 即看它能否通过某条路径回到原点
			vector<int> start(n, false);
			vector<int> end(n, true);
			start[srci] = true; // 作为起点开始
			end[srci] = false; // 不会作为终边

			while (!minque.empty())
			{
				auto min = minque.top();
				minque.pop();

				// 现在是以dsti 作为顶点 是否被访问过
				if (!start[min._dsti]){
					// 选择出了一条边min
					minTree._AddEdge(min._srci, min._dsti, min._w);
					// 注意这里需要更新 desi!
					start[min._dsti] = true;
					end[min._dsti] = false; // 自己不能再称为ends

					size++;
					totalW += min._w;
					if (size == n - 1) break;
				
					// 将新纳入的顶点 dsti 关联边也纳入
					for (int i = 0;i < n;++i)
					{
						// 该边必须存在 且这个顶点不能是 作为作为终点的!
						if (_edges[min._dsti][i] != MAX_W && end[i])
						{
							minque.push(Edge(min._dsti, i, _edges[min._dsti][i]));
						}
					}
				}
				else {
					// ... 不构成边 ...
				}
			}

			if (size == n - 1) return totalW;
			else return W();
		}

最短路径

最短路径算法DijstrakBellman-fordFloyd-warshall
时间复杂度O(n^2)O(e*n)O(n^3)
空间复杂度O(m)O(m)O(n^2)
适用情况稠密图 稀疏图(暴力更新边)稠密图
负权存在负权问题
Dijkstra算法:

几个问题:

🎨 我们需要从除S集合内以外的点中,找到最短边,是不是仍然可以使用 优先级队列帮我们完成这部分排序?

        使用优先级队列当然能为我们在选取阶段,一定能选到 ”边值最小边“,但我们最终是通过选“顶点”来规划处 “最短路径”。优先级队列首要的缺陷就在于其不能 ”随意(random)访问“,后面还会牵涉到更新最小路径重新建堆等问题,优先级队列在这里并不适用~

        所以,我们使用vector来保存顶点到顶点的最短路径值,使用直接映射的方式,使得我们在更细最短路径值时,也会变得十分简单~

🎨 更新出最短路径值就好了吗? 我咋知道你更新的结果是否正确呢? 换言之,我们需要去记录最短路径 —— 到底过了哪些顶点。

        我们同样采用vector数组,顶点与下标直接映射的方式,每一个位置保存着前驱下标。所以,我们能看到最终更新出的结果,该路径数组是倒序的。

		void DijKstra(const V& vv, vector<W>& distance, vector<int>& path)
		{
			size_t n = _vertex.size();
			size_t srci = getVertexIndex(vv);
			distance.resize(n,MAX_W);	// 最短路径集
			path.resize(n, -1);		// 记录路径
			vector<bool> S(n, false); // 纳入集合中的点
		
			// 进行初始化
			distance[srci] = 0; // 以srci为起点
			path[srci] = srci;

			// 遍历j个顶点
			for (size_t j = 0;j < n;++j)
			{
				// 进行归纳
				int u = 0; // 要纳入的顶点
				W min = MAX_W; // 每次选取最小边
				for (size_t i = 0;i < n;++i)
				{
					// 首次进入 选取的u == srci
					if (S[i] == false && distance[i] < min)
					{
						u = i;
						min = distance[i];
					}
				}
				// 纳入集合S
				S[u] = true;

				// 松弛操作
				// 从u开始,朝其未被纳入S集合的 顶点更新最短距离
				for (size_t v = 0;v < n;++v)
				{
					// 该点是没被纳入集合S的  && 该顶点与u是有关联边的 && 它们的当前值一定是最小的
					if (S[v] == false && _edges[u][v] != MAX_W && _edges[u][v] + distance[u] < distance[v]) {
						distance[v] = _edges[u][v] + distance[u];
						// 记录路径
						path[v] = u;  // 从u->v
					}
				}
			}
		}

        当Dijkstra遇到负权路径时,就可能发生失效~

 

Bellman-ford算法:

        Bellman-ford每次循环都会对有向图G内的所有顶点进行权值更新。每次循环迭代都不一样,所以需要更新多次~

		bool BellmanFord(const V& src, vector<W>& dist, vector<int>& pPath)
		{
			size_t n = _vertex.size();
			size_t srci = getVertexIndex(src);
			dist.resize(n, MAX_W);
			pPath.resize(n, -1);

			// 初始化
			dist[srci] = W(); // 给定一个初始值
			// 现在不用记录更新哪些顶点 因为每个顶点都得更新!
			for (int k = 0;k < n;++k)
			{
				// 性能优化 如果不发生更新 说明已经更新完成了 不必再循环了
				bool update = false;

				// 以每一个顶点作为 srci进行遍历
				// i --> j
				for (size_t i = 0; i < n; ++i)
				{
					for (size_t j = 0;j < n;++j)
					{
						if (_edges[i][j] != MAX_W && _edges[i][j] + dist[i] < dist[j]) {
							// 满足min 进行更新
							update = true;
							dist[j] = _edges[i][j] + dist[i];
							pPath[j] = i;
						}
					}
				}
				if (update == false) break;
			}
		
			// 判断负权回路
			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])
					{
						return false;
					}
				}
			}
			return true;
		}

         

Floyd-warshall算法:

 

		void FloydWarshall(vector<vector<W>>& vvDist, vector<vector<int>>& vvpPath)
		{
			size_t n = _vertex.size();
			vvDist.resize(n);
			vvpPath.resize(n);

			// 初始化
			for (size_t i = 0; i < n; ++i)
			{
				vvDist[i].resize(n, MAX_W);
				vvpPath[i].resize(n, -1);
			}

			// 更新直接相连的边
			for (size_t i = 0;i < n;++i)
			{
				for (size_t j = 0;j < n;++j)
				{
					if (_edges[i][j] != MAX_W)
					{
						vvDist[i][j] = _edges[i][j];
						vvpPath[i][j] = i;
					}
				}
			}

			// 动态规划 假设k个转点
			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 (vvDist[i][k] != MAX_W && vvDist[k][j] != MAX_W &&
							vvDist[i][k] + vvDist[k][j] < vvDist[i][j])
						{
							vvDist[i][j] = vvDist[i][k] + vvDist[k][j];
							// (i,j) 作为子节点 由 [k][j] 进行遍历
							vvpPath[i][j] = vvpPath[k][j];
						}
					}
				}
			}
		}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1566792.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

蓝桥杯物联网竞赛_STM32L071_15_ADC/脉冲模块

ADC模块用的是RP1不用多说了&#xff0c;主要是脉冲模块&#xff0c;这个模块没考过 这个脉冲模块放出脉冲&#xff0c;主要能用TIM捕获到这个脉冲的高电平持续时间即可 CubMx配置&#xff1a; 脉冲模块的引脚与PB0相连&#xff0c;所以用PB0读取上升沿记的数和下降沿记的数&am…

【第十一届大唐杯全国大学生新一代信息通信技术大赛】赛题分析

赛道一 一等奖 7% 二等奖 15% 三等奖 25% 赛道二 参考文档&#xff1a; 《第十一届大唐杯全国大学生新一代信息通信技术大赛&#xff08;产教融合5G创新应用设计&#xff09;专项赛说明.pdf》 一等奖&#xff1a;7% 二等奖&#xff1a;10% 三等奖&#xff1a;20% 赛项一&am…

【SQL】1873. 计算特殊奖金(CASE WHEN;IF())

题目描述 leetcode题目&#xff1a;1873. 计算特殊奖金 Code 写法一&#xff1a; CASE WHEN select employee_id, (case when employee_id % 2 0 or name like M% then salary 0 else salary end) as bonus from Employees order by employee_id写法二 &#xff1a;IF() …

大数据毕业设计hadoop+spark旅游推荐系统 旅游可视化系统 地方旅游网站 旅游爬虫 旅游管理系统 计算机毕业设计 机器学习 深度学习 知识图谱

基于hive数据仓库的贵州旅游景点数据分析系统的设计与实现 摘 要 随着旅游业的快速发展和数字化转型&#xff0c;旅游数据的收集和分析变得越来越重要。贵州省作为一个拥有丰富旅游资源的地区&#xff0c;旅游数据的分析对于促进旅游业的发展和提升旅游体验具有重要意义。基…

Docker 部署开源远程桌面工具 RustDesk

RustDesk是一款远程控制&#xff0c;远程协助的开源软件。完美替代TeamViewer &#xff0c;ToDesk&#xff0c;向日葵等平台。关键支持自建服务器&#xff0c;更安全私密远程控制电脑&#xff01;官网地址&#xff1a;https://rustdesk.com/ 环境准备 1、阿里云服务器一 台&a…

Jenkins磁盘空间自动清理

背景介绍 Jenkins作为一款广泛应用于自动化构建、测试与部署的强大工具&#xff0c;在长时间运作后&#xff0c;其服务器上积累了大量的构建历史、工作空间、日志文件以及其他临时数据。每次构建都会生成一系列的工作产物&#xff0c;随着时间的推移&#xff0c;这些数据逐渐占…

RabbitMQ3.x之六_RabbitMQ使用场景

RabbitMQ3.x之六_RabbitMQ使用场景 文章目录 RabbitMQ3.x之六_RabbitMQ使用场景1. 为什么选择 RabbitMQ&#xff1f;1. 可互操作2. 灵活3. 可靠 2. 常见用户案例1. 服务解耦2. 远程过程调用3. 流处理4. 物联网 1. 为什么选择 RabbitMQ&#xff1f; RabbitMQ 是一个可靠且成熟的…

微服务连接不上rabbitmq解决

1.把端口port: 15672改成port&#xff1a;5672 2&#xff1a;virtual-host: my_vhost一定对应上

云计算存在的安全隐患

目录 一、概述 二、ENISA云安全漏洞分析 三、云计算相关系统漏洞 3.1 概述 3.2 漏洞分析 3.2.1 Hypervisor漏洞 3.2.1.1 CVE-2018-16882 3.2.1.2 CVE-2017-17563 3.2.1.3 CVE-2010-1225 3.2.2 虚拟机漏洞 3.2.2.1 CVE-2019-14835 3.2.2.2 CVE-2019-5514 3.2.2.3 CV…

OpenHarmony实战开发-多设备自适应能力

介绍 本示例是《一次开发&#xff0c;多端部署》的配套示例代码&#xff0c;展示了页面开发的一多能力&#xff0c;包括自适应布局、响应式布局、典型布局场景以及资源文件使用。 说明&#xff1a; 自适应布局能力仅可以保证在外部容器大小在一定范围内变化时&#xff0c;容…

国外媒体推广软文宣发:促进海外宣发新风尚,迈向国际舞台

大舍传媒http://www.dashemeijie.com 序言 伴随全球经济一体化发展趋向&#xff0c;越来越多的中国企业希望在国际舞台上表现自己的总体水平。而国外媒体软文发稿作为一种全新的海外宣传方式&#xff0c;正逐渐成为促进海外宣发新风尚的主要常用工具。接下来我们就探讨国外媒…

Acrel-1000DP光伏监控系统在尚雷仕(湖北)健康科技有限公司5.98MW分布式光伏10KV并网系统的应用

摘 要&#xff1a;分布式光伏发电特指在用户场地附近建设&#xff0c;运行方式多为自发自用&#xff0c;余电上网&#xff0c;部分项目采用全额上网模式。分布式光伏全额上网的优点是可以充分利用分布式光伏发电系统的发电量&#xff0c;提高分布式光伏发电系统的利用率。发展分…

构建企业级微服务平台:实现可扩展性、弹性和高效性

在软件开发的快速发展领域中&#xff0c;企业不断努力构建健壮、可扩展和高效的系统。随着微服务架构的出现&#xff0c;再加上云原生技术的应用&#xff0c;创建敏捷且具有弹性的平台的可能性是无限的。在本指南中&#xff0c;我们将深入探讨使用强大的工具和技术组合&#xf…

苹果商店审核指南:确保Flutter应用顺利通过审核的关键步骤

引言 Flutter是一款由Google推出的跨平台移动应用开发框架&#xff0c;其强大的性能和流畅的用户体验使其备受开发者青睐。然而&#xff0c;开发一款应用只是第一步&#xff0c;将其成功上架到苹果商店才是实现商业目标的关键一步。本文将详细介绍如何使用Flutter将应用程序上…

​如何使用ArcGIS Pro进行洪水淹没分析

洪水淹没分析是一种常见的水文地理信息系统应用&#xff0c;用于模拟和预测洪水事件中可能受到淹没影响的地区&#xff0c;这里为大家介绍一下ArcGIS Pro进行洪水淹没分析的方法&#xff0c;希望能对你有所帮助。 数据来源 教程所使用的数据是从水经微图中下载的DEM数据&…

2012年认证杯SPSSPRO杯数学建模A题(第二阶段)蜘蛛网全过程文档及程序

2012年认证杯SPSSPRO杯数学建模 A题 蜘蛛网 原题再现&#xff1a; 第二阶段问题   现在我们假设一个具体的环境。假设有一个凸多边形的区域&#xff0c;蜘蛛准备在这个区域&#xff08;或其一部分&#xff09;上结一张网。   问题一&#xff1a; 在区域的边界上安置有若干…

06-编辑器

gedit编辑器 gedit是Ubuntu系统自带的编辑器&#xff0c;可以用来轻度编辑和记录一些内容。 在终端中我们通过以下命令打开&#xff1a; gedit 要打开或者新建的文件名虽然Ubuntu的图形界面也能通过gedit打开文件&#xff0c;但是用终端打开gedit可以动用更高的权限&#xff…

localPosiTion和WorldPosition的mask应用。

localPosiTion和WorldPosition的mask应用。 2021-07-22 17:39 1. local Position应用 2. WorldPosition应用 shader的连接&#xff1a;localposition:localposition>mask(B)>ADD>1-x>multiply>opacity …

Spring Boot中前端通过请求接口下载后端存放的Excel模板

导出工具类 package com.yutu.garden.utils;import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; import org.apache.commons.io.IOUtils; import org.apache.poi.hssf.util.HSSFColor; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.slf4j.Logger;…

音频转换工具 Bigasoft FLAC Converter for Mac

Bigasoft FLAC Converter for Mac是一款专为Mac用户设计的音频转换工具&#xff0c;它能够将FLAC音频文件高效、高质量地转换为其他常见的音频格式&#xff0c;如MP3、AAC等。这款软件具有直观易用的界面&#xff0c;使用户能够轻松上手&#xff0c;无需复杂的操作步骤即可完成…