图算法基础

图算法基础

一,图模板(有/无向图)(邻接表/邻接矩阵)

GraphBase.h
c++ 复制代码
#pragma once
#include <iostream>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "Edge.h"
#include "Node.h"

using namespace std;

class Graph {
public:
    // serial number & practical structure
    unordered_map<int, Node*>* nodes;
    unordered_set<Edge*>* edges;

    Graph() {
        nodes = new unordered_map<int, Node*>();
        edges = new unordered_set<Edge*>();
    }

    ~Graph() {
        delete nodes;
        delete edges;
    }
};
Node.h
c++ 复制代码
#pragma once
#include <iostream>
#include <vector>
using namespace std;

// 前向声明 Edge 类
class Edge;

class Node {
public:
    int value;
    int in;  // in-degree
    int out; // out-degree
    vector<Node*>* nexts;
    vector<Edge*>* edges;

    Node() {

    }

    Node(int value) {
        this->value = value;
        in = 0;
        out = 0;
        nexts = new vector<Node*>();
        edges = new vector<Edge*>();
    }

    ~Node() {
        delete nexts;
        delete edges;
    }
};
Edge.h
c++ 复制代码
#pragma once
#include <iostream>
#include "Node.h"
#include <vector>
using namespace std;

class Edge {
public:
    int weight;
    Node* from;
    Node* to;

    Edge(int weight, Node* from, Node* to) {
        this->weight = weight;
        this->from = from;
        this->to = to;
    }
};

大面积使用指针的主要目的是为了避免不必要的深拷贝 ,提高性能和灵活性,避免栈上对象的生命周期问题 ,以及便于在图的节点和边之间进行共享和相互引用。尤其是在处理图这样的复杂数据结构时,指针提供了更大的灵活性来高效地管理内存和对象之间的关系。

第十三章 DFS与BFS(保姆级教学!!超级详细的图示!!)_dfs bfs-CSDN博客

二,宽度优先遍历(BFS)

c++ 复制代码
class BreadthFirstSearch {
public:
	void bfs(Node* node) {
		if (node == NULL) {
			return;
		}

		//直接使用 栈上对象 亦可
		queue<Node*>* q = new queue<Node*>();
		unordered_set<Node*>* set = new unordered_set<Node*>();

		//在数据量小时,可以用数组替换哈希表
		q->push(node);
		set->insert(node);
		while (!q->empty()) {
			Node* cur = q->front();
			cout << cur->value;
			for (Node* next : *(cur->nexts)) {
				if (set->find(next)==set->end()) {
					q->push(next);
					set->insert(next);
				}
			}
		}
	}
};

三,深度优先遍历(DFS)

c++ 复制代码
#include<stack>

class DepthFirstSearch {
public:

	void dfs(Node* node) {
		if (node == NULL) {
			return;
		}

		//直接用栈上元素
		stack<Node*> s;
		unordered_set<Node*> set;

		s.push(node);
		set.insert(node);
		cout << node->value;
		while (!s.empty()) {
			Node* cur = s.top();
			s.pop();
			for (Node* next : *(cur->nexts)) {
				if (set.find(next) == set.end()) {
					s.push(cur);
					s.push(next);
					set.insert(next);
					cout << next->value;
					break;
				}
			}
		}
	}
};

四,拓扑排序

找到入度为0的点输出,擦除其及其影响,在剩下的点中找入度为0的点。

c++ 复制代码
class TopologicalSorting {
public:

	vector<Node*> sortTopo(Graph* graph) {
		unordered_map<Node*, int> inMap;
		queue<Node*> zeroInQueue;

		for (auto& pair : *(graph->nodes)) {
			Node* node = pair.second;
			inMap.insert({ node, node->in });
			if (node->in == 0) {
				zeroInQueue.push(node);
			}
		}

		vector<Node*> result;
		while (!zeroInQueue.empty()) {
			Node* cur = zeroInQueue.front();
			zeroInQueue.pop();
			result.push_back(cur);
			for (Node* next : *(cur->nexts)) {
				inMap.insert({ next, inMap.find(next)->second - 1 });
				if (inMap.find(next)->second == 0) {
					zeroInQueue.push(next);
				}
			}
		}
		return result;
	}
};

五,最小生成树K算法:要求无向图

Kurskal

从最小的边(最小堆)开始考虑,添加加边是否形成环。(如何判断成环?------集合查询结构)

{单点集合}判断from,to是否在一个集合中,合并from,to形成{两点集合}。。。如果from,to不在一个集合中,则不会形成环。

c++ 复制代码
//稍后说明,并查集
#include "UnionFindSet.h"

struct compare {
	//定义最小堆
	bool operator()(Edge* o1, Edge* o2) {
		return o1->weight > o2->weight;
	}
};

class algorithmKruskal {
public:

	unordered_set<Edge*> Kruskal(Graph* graph) {

		//获取所有点,用他们初始化并查集
		vector<Node*>nodeValues;
		for (auto& pair : *(graph->nodes)) {
			nodeValues.push_back(pair.second);
		}
		UFS unionFind(nodeValues);

		//创建边的最小堆,之后从最小边开始依次分析
		priority_queue<Edge*, vector<Edge*>, compare> pq;
		for (Edge* edge : *(graph->edges)) {
			pq.push(edge);
		}

		unordered_set<Edge*> result;
		while (!pq.empty()) {
			Edge* edge = pq.top();
			pq.pop();
			//如果加入这条边,已经连接的点不会成环
			if (!unionFind.isSameSet(edge->from, edge->to)) {
				result.insert(edge);
				unionFind.unionSet(edge->from, edge->to);
			}
		}
		return result;
	}

};

在写上述算法时,所使用的简易并查集实现:

c++ 复制代码
class UFS {
public:
	unordered_map<Node*, vector<Node*>> setMap;
	UFS(vector<Node* >nodes) {
		for (Node* cur : nodes) {
			vector<Node*> set;
			set.push_back(cur);
			setMap.insert({ cur,set });
		}
	}

	bool isSameSet(Node* from, Node* to) {
		vector<Node*> fromSet = setMap.find(from)->second;
		vector<Node*> toSet = setMap.find(to)->second;
		//检查集合的内存地址是否相等,依此判断是否为同一个集合
		return fromSet == toSet;
	}

	void unionSet(Node* from, Node* to) {
		vector<Node*> fromSet = setMap.find(from)->second;
		vector<Node*> toSet = setMap.find(to)->second;

		/*for (Node* toNode : toSet) {
			fromSet.push_back(toNode);
			setMap.insert({ toNode, fromSet });
		}*/

		// 将 toSet 中的节点添加到 fromSet 中
		fromSet.insert(fromSet.end(), toSet.begin(), toSet.end());

		// 更新 setMap 中 toSet 所有元素的集合为 fromSet
		for (Node* toNode : toSet) {
			setMap[toNode] = fromSet;
		}
	}
};

5.5 并查集结构(功能实现)(并查集)

1,功能实现:

想办法实现类java.values()获取所有值那样的效果,有两种方法,一种是在实际算法中用auto自动推导,将全部数据先放进pair中,再用pair.second依次获取所有值,一种是直接修改头文件添加自定义的getvalues成员函数。这两种方法实际上大同小异。

(1),添加成员函数:
c++ 复制代码
vector<Node*> getValues(Graph* graph) {
    vector<Node*> nodeValues;
    for (auto& pair : *(graph->nodes)) {
        nodeValues.push_back(pair.second); // pair.second 是 Node* 值
    }
    return nodeValues;
}
(2),在实际算法中处理:
c++ 复制代码
algorithmKruskal(Graph* graph) {
	vector<Node*>nodeValues;
	for (auto& pair : *(graph->nodes)) {
		nodeValues.push_back(pair.second);
	}
	UFS unionFind(nodeValues);
}
2,并查集

theSerein大佬讲得并查集结构真是使人醍醐灌顶!通透【算法与数据结构】------ 并查集-CSDN博客 我将以此为蓝本重新规划应用于Node类型的并查集。

c++ 复制代码
class newUFS {
	//指定并查集能够包含的元素个数,由题意决定
	static const int N = 1000;

	unordered_map<Node*, Node*> preNode;
	unordered_map<Node*, int>nodeRanks;

	newUFS(vector<Node*> nodes) {
		for (Node* node : nodes) {
			preNode[node] = node;
			nodeRanks[node] = 1;
		}
	}

	Node* find(Node* node) {
		if (preNode[node] == node) return node;
		return find(preNode[node]);
	}

	bool isSame(Node* from, Node* to) {
		return find(from) == find(to);
	}

	bool join(Node* from, Node* to) {
		from = find(from);
		to = find(to);
		if (from == to) return false;
		if (nodeRanks[from] > nodeRanks[to]) {
			preNode[to] = from;
		}
		else {
			if (nodeRanks[from] == nodeRanks[to]) {
				nodeRanks[to]++;
			}
			preNode[from] = to;
		}
		return true;
	}
};

六,最小生成树P算法

Prim

从一个点开始,将之视为已经解锁的点,它的边视为已经解锁的边,将已经解锁的点和已经解锁的边分别放入哈希表和最小堆中,每次分析最小的已经解锁边,边上的to点是否指向一个未解锁的点,如果是,则解锁该点,将当前最小边加入结果Edge哈希集合,将该点的所有边加入最小堆。

c++ 复制代码
class algorithmPrim {
public:
	unordered_set<Edge*> primMST(Graph* graph) {
		//已经解锁的边的最小堆,注意,所有的点所解锁的边都会放进去。
		priority_queue<Edge*, vector<Edge*>, compare> pq;
		//已经解锁的点的集合
		unordered_set<Node*> nodeSet;
		//最小生成树结果边的集合
		unordered_set<Edge*> res;

		//如果不能保证所有点连通,则依次分析每个点
		for (auto& pair : *(graph->nodes)) {
			Node* node = pair.second;
			
			if (nodeSet.find(node) == nodeSet.end()) {
				nodeSet.insert(node);
				//将初始点的所有边放入最小堆
				for (Edge* edge : *(node->edges)) {
					pq.push(edge);
				}

				while (!pq.empty()) {
					//取得最小边
					Edge* edge = pq.top();
					pq.pop();
					Node* toNode = edge->to;
					//判断边上to点是否是已经解锁的点,若未解锁,则将之纳入已经解锁的点集中
					if (nodeSet.find(toNode) == nodeSet.end()) {
						nodeSet.insert(toNode);
						res.insert(edge);
						for (Edge* nextEdge : *(toNode->edges)) {
							pq.push(nextEdge);
						}
					}
				}
			}
		}
		return res;
	}

};

七,单源最短路径Dijikstra算法

通过每次分析确定(锁)一条最短路径,更新其它最短路径。因此,该算法可以有权值为负数的边,但是不能有累加和为负数的环。 如果存在,那么每在该环上转一圈,最短路径都会减少一些,直到变成负无穷。破坏了已经确定的最短路径。

c++ 复制代码
class Dijikstra {
public:
	//初始化一个哈希表,存储从原点到所有点的距离
	unordered_map<Node*, int> dijkstra1(Node* head) {
		unordered_map<Node*, int> distanceMap;
		distanceMap.insert({ head, 0 });
		//存储已经确定的最小距离,"不再改变",同时为后续的"找未确定的最小距离的点"的函数提供参数
		unordered_set<Node*> selectedNodes;
		Node* minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
		//原点向各个方向延申,
		while (minNode != NULL) {
			//当前minNode进行延申,for循环跑完,原点到minNode最小距离确定
			int distance = distanceMap.find(minNode)->second;
			for (Edge* edge : *(minNode->edges)) {
				Node* toNode = edge->to;
				//检查指向点是否有记录,实际上就是在做更新
				if (distanceMap.find(toNode) == distanceMap.end()) {
					distanceMap.insert({ toNode, distance + edge->weight});
				}
				//判断原点到to点的距离是否可以做更新
				else {
					distanceMap.find(edge->to)->second = min(distanceMap.find(toNode)->second,
						                                     distance + edge->weight);
				}
			}
			//将minNode加入已经确定的点
			selectedNodes.insert(minNode);
			//更新minNode
			minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);
		}
		return distanceMap;
	}

	Node* getMinDistanceAndUnselectedNode(const unordered_map<Node*, int> &distanceMap, 
		                                  const unordered_set<Node*> &selectedNodes) 
	{
		Node* minNode = NULL;
		int minDistance = INT_MAX;
		for (pair<Node*, int> pairs : distanceMap) {
			Node* node = pairs.first;
			int distance = pairs.second;
			if (selectedNodes.find(node) == selectedNodes.end() && distance < minDistance) {
				minNode = node;
				minDistance = distance;
			}
		}
		return minNode;
	}
};

最小堆优化:

如果使用自带最小堆,将原本用选择法实现的函数 getMinDistanceAndUnselectedNode 改用最小堆(参数为距离)实现,则在minNode延申过程中可能更写distanceMap中某些距离,改变了最小堆中既有元素的值,所以需要重写堆。

相关代码上传至github:github.com/kair998/Alg...

相关推荐
说码解字几秒前
快速排序算法及其优化
算法
Lounger664 分钟前
107.二叉树的层序遍历II- 力扣(LeetCode)
python·算法·leetcode
IT猿手6 分钟前
动态多目标优化:基于可学习预测的动态多目标进化算法(DIP-DMOEA)求解CEC2018(DF1-DF14),提供MATLAB代码
学习·算法·matlab·动态多目标优化·动态多目标进化算法
竹下为生9 分钟前
LeetCode --- 444 周赛
算法·leetcode·职场和发展
我是前端小学生19 分钟前
默克尔树:区块链中的高效数据结构
数据结构
同勉共进32 分钟前
虚函数表里有什么?(三)——普通多继承下的虚函数表
c++·多继承·虚函数表·内存布局·rtti·non-virtual thunk·__vmi_class_type_info
明月看潮生36 分钟前
青少年编程与数学 02-016 Python数据结构与算法 14课题、动态规划
python·算法·青少年编程·动态规划·编程与数学
明月看潮生36 分钟前
青少年编程与数学 02-016 Python数据结构与算法 11课题、分治
python·算法·青少年编程·编程与数学
源客z1 小时前
SD + Contronet,扩散模型V1.5+约束条件后续优化:保存Canny边缘图,便于视觉理解——stable diffusion项目学习笔记
图像处理·算法·计算机视觉
李匠20241 小时前
C++学习之工厂模式-套接字通信
c++·学习