图的算法都不算难,只不过coding的代价比较高
(1)先用自己最熟练的方式,实现图结构的表达
(2)在自己熟悉的结构上,实现所有常用的图算法作为模板
(3)把面试题提供的图结构转化为自己熟悉的图结构,再调用模板或改写即可
图的表示方法这么多种,并且每次给你的形式还可能不同,所以就有必要抽象一个自己的表示方法,以后对于不同的形式,写一个能转换为自己定义形式的方法即可(有种适配器的感觉),这样才能以不变应万变,把不熟悉的表示方法转换为自己熟悉的方法
下面仅做算法的实现,没有进行内存管理
图的表示
#include <vector>
#include <map>
#include <vector>
#include <memory>
#include <stack>
#include <set>
using namespace std;
using namespace std;
struct Edge;
struct Node;
struct Graph;
struct Node{
int val;
int in;
int out;
std::vector<Node *> nexts;
std::vector<Edge *> edge;
Node(int val, int in = 0, int out = 0) : val(val), in(in), out(out){
}
};
struct Edge{
int weight;
Node *from;
Node *to;
Edge(Node *from, Node *to, int weight = 0) : from(from), to(to), weight(weight){
}
};
struct Graph{
std::map<int, Node*> nodes;
std::set<Edge *> edges;
};
图的生成
// matrix 所有的边
// N*3 的矩阵
// [weight, from节点上面的值,to节点上面的值]
//
// [ 5 , 0 , 7]
// [ 3 , 0, 1]
Graph *createGraph(std::vector<std::vector<int>> &m){
Graph *graph = new Graph;
for(auto vec : m){
int weight = vec[0], fLabel = vec[1], tLabel = vec[2];
if(!graph->nodes.count(fLabel)){
graph->nodes[fLabel] = new Node(fLabel);
}
if(!graph->nodes.count(tLabel)){
graph->nodes[tLabel] = new Node(tLabel);
}
auto fNode = graph->nodes[fLabel], tNode = graph->nodes[tLabel];
auto edge = new Edge(fNode, tNode, weight);
fNode->out++;
tNode->in++;
fNode->nexts.push_back(tNode);
fNode->edge.push_back(edge);
graph->edges.emplace(edge);
}
return graph;
}
int main(){
std::vector<std::vector<int>> matrix;
matrix.emplace_back(std::vector<int>{5, 0, 7});
matrix.emplace_back(std::vector<int>{3, 0, 1});
createGraph(matrix);
}
图的遍历
广度优先遍历
(1)准备一个队列,一个Set(存放遍历过的节点,登记表),出发节点为A,把A放到队列和Set中
(2)弹出队列的顶点M,打印M的值。获取M的所有邻居节点next,查看Set中有没有这些next节点,无则放到Set和队列中,有则跳过此next节点
(3)一直执行第2步,直到队列为空
// 从node出发,进行宽度优先遍历
void bfs(Node *start){
if(start == nullptr){
return ;
}
std::queue<Node *> queue;
std::set<Node *> set;
queue.push(start);
set.emplace(start);
while (!queue.empty()){
auto cur = queue.front(); queue.pop();
std::cout << cur->val <<"\n";
for(auto &next : cur->nexts){
if(!set.count(next)){
queue.push(next);
set.emplace(next);
}
}
}
}
怎么调用:
int main(){
std::vector<std::vector<int>> matrix;
matrix.emplace_back(std::vector<int>{5, 0, 7});
matrix.emplace_back(std::vector<int>{3, 0, 1});
matrix.emplace_back(std::vector<int>{3, 1, 2});
matrix.emplace_back(std::vector<int>{3, 2, 7});
auto graph = createGraph(matrix);
bfs(graph->nodes[0]);
}
深度优先遍历
一条路没走完就一直走,走完了就往回走,看哪些岔路还没有走。(不能走出环路,走过的地方就不能再走了)
(1)准备一个栈(存放目前的整条路径),一个Set(存放遍历过的节点,登记表),出发节点为A,把A放到栈和Set中,同时打印A的值(入栈就打印)
(2)弹出栈顶元素M,遍历M的所有邻居节点next,查看Set中有没有这些next节点,无则将M和此时的next节点入栈、next放到Set中,打印next的值(入栈就打印),终止遍历next节点(即只入栈一个Set中不包含的节点)
(3)一直执行第2步,直到栈为空
void dfs(Node *node){
if(node == nullptr){
return ;
}
std::stack<Node *> stack;
std::set<Node *> set;
stack.emplace(node);
set.emplace(node);
printf("%d\n", node->val);
while (!stack.empty()){
auto cur = stack.top(); stack.pop();
for(const auto& next : cur->nexts){
if(!set.count(next)){
stack.push(cur);
stack.push(next);
set.emplace(next);
printf("%d\n", next->val);
break;
}
}
}
}
图的拓扑排序
std::vector<Node *> sortedTopology(Graph *graph){
std::map<Node *, int> inMap; // key 某个节点 value 剩余的入度
std::queue<Node *> zeroQueue; // 只有剩余入度为0的点,才进入这个队列
//1. 先将入度为0的点加入队列
for(auto &node : graph->nodes){
inMap[node.second] = node.second->in;
if(node.second->in == 0){
zeroQueue.push(node.second);
}
}
// 开始删边
std::vector<Node *> result;
while (!zeroQueue.empty()){
auto cur = zeroQueue.front(); zeroQueue.pop();
result.emplace_back(cur);
for(auto &next : cur->nexts){
inMap[next] = inMap[next] - 1;
if(inMap[next] == 0){
zeroQueue.emplace(next);
}
}
}
return result;
}
Dijkstra算法 (迪杰斯特拉算法)
Dijkstra算法(迪杰斯特拉算法)是很有代表性的最短路径算法,用于计算一个结点到其他结点的最短路径。该算法指定一个点(源点)到其余各个结点的最短路径,因此也叫做单源最短路径算法。该算法是由荷兰计算机科学家Edsger W.Dijkstra于1959年发表。
- Dijkstra算法是一种用于计算带权有向图中单源最短路径算法,不存在回溯的过程,因此它还不适用于带有负权重的情况。
- 如果权值存在负数,那么被派生出来的可能是更短的路径,这就需要过程可以回溯,之前的路径需要被更短的路径替换掉
- 而Dijkstra算法是不能回溯的,它的每一步都是以当前最优选择为前提的。
- Dijkstra算法的思想是广度优先搜索(BFS) 贪心策略。对于计算非加权图中的最短路径,也可使用BFS算法。
- Dijkstra算法是对BFS算法的推广,以起始点为中心向外层层扩展,并且每一次都选择最优的结点进行扩展,直到扩展到终点为止。
- Dijkstra算法可以划归为贪心算法,下一条路径都是由当前更短的路径派生出来的更长的路径。
例子
- 创建距离表。第1列是结点名称,第2列是从起点A到对应结点的已知最短距离。开始我们并不知道A到其它结点的最短距离是多少,默认初始距离是无穷大。
- 遍历起点A的所有相邻结点,找到起点A的邻接结点B和C。从A到B的距离是5,从A到C的距离是2,刷新距离表中起点A到各结点的最短距离(绿色表示刷新)。
- 从距离表中找到从A出发距离最短的点,也就是结点C(最小距离是2)。遍历结点C的所有相邻结点,找到结点C的相邻结点D和F(A已经遍历过,不需要考虑)。从C到D的距离是1,所以A到D的距离是A-C-D=2 1=3;从C到F的距离是8;从A到F的距离是A-C-F=2 8=10。然后刷新距离表(绿色表示刷新)。
- 从距离表中找到从A出发距离最短的点(红色结点C已经遍历过,不需要考虑),也就是结点D(最小距离是3)。遍历结点D的所有相邻结点,找到相邻结点B、E和F(C已遍历过,不考虑)。从A-C-D-B的距离是3+1=4;从A-C-D-E的距离是3+1=4;从A-C-D-F的距离是3+ 2=5。刷新距离表中起点A到各结点的最短距离
- 从距离表中找到从A出发距离最短的点(红色结点C、D已经遍历过,不需要考虑),也就是结点B和E(最小距离是4)。遍历结点B的所有相邻结点,找到相邻结点E(D遍历过,不考虑),从A-C-D-B-E的距离为10,比当前A到E的最小距离4要大,不考虑。遍历结点E的所有相邻结点,找到相邻结点G、B(D遍历过,不考虑),从A-C-D-E-G的距离为4 7=11<∞, 刷新距离表;A-C-D-E-B的距离4+6=10>4,不考虑
- 从距离表中找到从A出发距离最短的点(红色结点B、C、D、E已经遍历过,不需要考虑),也就是结点F(最小距离是5)。从A-C-D-F-G的距离为8, 比当前最小距离11要小,刷新距离表。
就这样,除终点以外的全部结点都已经遍历完毕,距离表中存储的是从起点A到所有结点的最短距离。
实现一
Node *getMinDistanceAndUnselectedNode(
std::map<Node *, int > &distanceMap,
std::set<Node *> &selectNodes){
Node *minNode = nullptr;
int minDistance = INT_MAX;
for(auto &entry : distanceMap){
auto node = entry.first;
auto distance = entry.second;
if(!selectNodes.count(node) && distance < minDistance){
minNode = node;
minDistance = distance;
}
}
return minNode;
}
std::map<Node *, int> dijkstra(Node *from){
std::map<Node *, int > distanceMap;
std::set<Node *> selectNodes;
distanceMap[from] = 0;
auto minNode = getMinDistanceAndUnselectedNode(distanceMap, selectNodes);
while (minNode != nullptr){
// 原始点 -> minNode(跳转点) 最小距离distance
int dis = distanceMap[minNode];
for(auto &edge : minNode->edge){
auto toNode = edge->to;
if(!distanceMap.count(toNode)){
distanceMap[toNode] = dis + edge->weight;
}else{
distanceMap[toNode] = std::min(distanceMap[toNode], dis + edge->weight);
}
}
selectNodes.insert(minNode);
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectNodes);
}
return distanceMap;
}
实现二(加强堆优化)
struct NodeRecord{
std::shared_ptr<Node> node;
int distance;
NodeRecord(std::shared_ptr<Node>& node, int distance) : node(node), distance(distance){
}
};
class NodeHeap{
std::vector<std::shared_ptr<Node>> nodes; // 实际的堆结构
std::map<std::shared_ptr<Node>, int> heapIndexMap; // key 某一个node, value 上面堆中的位置
std::map<std::shared_ptr<Node>, int> distanceMap; // key 某一个节点, value 从源节点出发到该节点的目前最小距离
int size; // 堆上有多少个点
void swap(int index1, int index2){
heapIndexMap[nodes[index1]] = index2;
heapIndexMap[nodes[index2]] = index1;
std::swap(nodes[index1], nodes[index2]);
}
bool isEntered(std::shared_ptr<Node> &node){
return heapIndexMap.count(node);
}
bool inHeap(std::shared_ptr<Node> & node){
return isEntered(node) && heapIndexMap[node] != -1;
}
void insertHeapify(int index) {
while (distanceMap[nodes[index]] < distanceMap[nodes[(index - 1) / 2]]) {
swap(index, (index - 1) / 2);
index = (index - 1) / 2;
}
}
void heapify(int index){
int left = index * 2 + 1;
while (left < size){
int smallest = left + 1 < size && distanceMap[nodes[left + 1]] < distanceMap[nodes[left]]
? left + 1
: left;
smallest = distanceMap[nodes[smallest]] < distanceMap[nodes[index]] ? smallest : index;
if (smallest == index) {
break;
}
swap(smallest, index);
index = smallest;
left = index * 2 + 1;
}
}
public:
explicit NodeHeap(int max_size){
nodes.resize(max_size);
size = 0;
}
bool isEmpty() const{
return size == 0;
}
// 有一个点叫node,现在发现了一个从源节点出发到达node的距离为distance
// 判断要不要更新,如果需要的话,就更新
void addOrUpdateIgnore(std::shared_ptr<Node> &node, int distance){
if (inHeap(node)) {
distanceMap[node] = std::min(distanceMap[node], distance);
insertHeapify(heapIndexMap[node]);
}
if (!isEntered(node)) {
nodes[size] = node;
heapIndexMap[node] = size;
distanceMap[node] = distance;
insertHeapify(size++);
}
}
std::shared_ptr<NodeRecord> pop(){
auto nodeRecode = std::make_shared<NodeRecord>(nodes[0], distanceMap[nodes[0]]);
swap(0, size - 1);
heapIndexMap[nodes[size - 1]] = -1;
distanceMap.erase(nodes[size - 1]);
nodes[size - 1] = nullptr;
size--;
heapify(0);
return nodeRecode;
}
};
// 改进后的dijkstra算法
// 从head出发,所有head能到达的节点,生成到达每个节点的最小路径记录并返回
std::map<std::shared_ptr<Node>, int> dijkstra(std::shared_ptr<Node> from, int size){
std::shared_ptr<NodeHeap> nodeHeap = std::make_shared<NodeHeap>(size);
nodeHeap->addOrUpdateIgnore(from, 0);
std::map<std::shared_ptr<Node>, int> result;
while (!nodeHeap->isEmpty()){
auto recode = nodeHeap->pop();
auto cur = recode->node;
int distance = recode->distance;
for(auto &edge : cur->edges){
nodeHeap->addOrUpdateIgnore(edge->to, edge->weight);
}
result[cur] = distance;
}
return result;
}
最小生成树
根据所有顶点之间是否存在通路,图存储结构可以细分为连通图和非连通图。举个例子:
图 2 a) 是一个非连通图,比如图中找不到一条从 a 到 c 的路径。图 2 b) 是一个连通图,因为从一个顶点到另一个顶点都至少存在一条通路,比如从 a 到 c 的通路可以为 a-f-c、a-b-c 等。
所谓生成树,指的是具备以下条件的连通图:
- 包含图中所有的顶点;
- 任意顶点之间有且仅有一条通路。
图 2 b) 是一个连通图,其对应的生成树有很多种,例如:
一个连通图可能对应着多种不同的生成树。
在连通网的所有生成树中,所有边的代价和最小的生成树,称为最小生成树。
kruskal算法
此算法可以称为“加边法”,初始最小生成树边数为0,每迭代一次就选择一条满足条件的最小代价边,加入到最小生成树的边集合里。
- 将所有的边按照他们的边权由小到大排序
- 把图中的n个顶点看成独立的n棵树组成的森林;
- 按权值从小到大选择边,所选的边连接的两个顶点ui,vi,应属于两颗不同的树,则成为最小生成树的一条边,并将这两颗树合并作为一颗树。
- 重复(3),直到所有顶点都在一颗树内或者有n-1条边为止。
其实kruskal就是并查集和贪心的结合,仔细看看图的话并不难理解。
注意:
- 总是从权值最小的边开始考虑,依次考察权值依次变大的边
- 当前的边要么进入最小生成树的集合,要么丢弃
- 如果当前的边进入最小生成树的集合中不会形成环,就要当前边
- 如果当前的边进入最小生成树的集合中会形成环,就不要当前边
- 考察完所有边之后,最小生成树的集合也得到了
实现:
#include <vector>
#include <map>
#include <vector>
#include <memory>
#include <stack>
#include <set>
#include <queue>
using namespace std;
// 点结构的描述
struct Node;
struct Edge;
struct Node{
int value;
int in;
int out;
std::vector<std::shared_ptr<Node>> nexts;
std::vector<std::shared_ptr<Edge>> edges;
Node(int value) : value(value), in(0), out(0){
}
};
struct Edge{
int weight;
std::shared_ptr<Node> from;
std::shared_ptr<Node> to;
Edge(int weight, std::shared_ptr<Node> &from, std::shared_ptr<Node> &to) : weight(weight), from(from), to(to) {
}
};
bool operator < (std::shared_ptr<Edge> & a, std::shared_ptr<Edge> & b){ //返回true时,说明a的优先级低于b
return a->weight < b->weight;
}
// 当priority_queue的元素类型为指针的时候,重载< 的方法不能有效的给指针元素排序。这时候可以考虑以下的解决方案,定义cmp结构体类型,在内部重载`()`
struct cmp {
bool operator ()(std::shared_ptr<Edge> &a, std::shared_ptr<Edge> &b){
return a->weight > b->weight;
}
};
struct Graph{
std::map<int, std::shared_ptr<Node>> nodes;
std::set<std::shared_ptr<Edge>> edges;
Graph(){
}
};
class Solution {
class UnionFind{
// key 某一个节点, value key节点往上的节点
std::map<std::shared_ptr<Node>, std::shared_ptr<Node>> fatherMap;
// key 某一个集合的代表节点, value key所在集合的节点个数
std::map<std::shared_ptr<Node>, int> sizeMap;
std::shared_ptr<Node> findRoot(std::shared_ptr<Node>& node){
std::stack<std::shared_ptr<Node>> path;
while (node != fatherMap[node]){
path.emplace(node);
node = fatherMap[node];
}
while (!path.empty()){
fatherMap[path.top()] = node;
path.pop();
}
return node;
}
public:
UnionFind()= default;
// 初始化集合
void makeSets(const std::map<int, std::shared_ptr<Node>>& nodes){
fatherMap.clear();
sizeMap.clear();
for(const auto& it : nodes){
fatherMap[it.second] = it.second;
sizeMap[it.second] = 1;
}
}
bool isSameSet(std::shared_ptr<Node> &a, std::shared_ptr<Node> &b){
return findRoot(a) == findRoot(b);
}
void merge(std::shared_ptr<Node> &a, std::shared_ptr<Node> &b){
if(a == nullptr || b == nullptr){
return;
}
auto aRoot = findRoot(a);
auto bRoot = findRoot(b);
if(aRoot != bRoot){
int aSetSize = sizeMap[aRoot], bSetSize = sizeMap[bRoot];
if(aSetSize <= bSetSize){
fatherMap[aRoot] = bRoot;
sizeMap[bRoot] = aSetSize + bSetSize;
sizeMap.erase(aRoot);
}else{
fatherMap[bRoot] = aRoot;
sizeMap[aRoot] = aSetSize + bSetSize;
sizeMap.erase(bRoot);
}
}
}
};
public:
std::set<std::shared_ptr<Edge>> kruskalMST(std::shared_ptr<Graph> graph){
auto unionFind = std::make_shared<UnionFind>();
unionFind->makeSets(graph->nodes);
// 从小的边到大的边,依次弹出,小根堆!
std::priority_queue<std::shared_ptr<Edge>, std::vector<std::shared_ptr<Edge>>, cmp> priorityQueue;
// std::priority_queue<std::shared_ptr<Edge>, std::vector<std::shared_ptr<Edge>>, std::greater<>> priorityQueue;
// 小根堆放入所有的边
for(auto &edge : graph->edges){
priorityQueue.emplace(edge);
}
std::set<std::shared_ptr<Edge>> result;
while (!priorityQueue.empty()){
auto edge = priorityQueue.top(); priorityQueue.pop();
printf("%d\t", edge->weight);
// 小根堆堆顶的边对应的节点不会形成环(即两个点不在同一个集合中),才往结果集中添加,否则舍弃
if(!unionFind->isSameSet(edge->from, edge->to)){
result.emplace(edge);
unionFind->merge(edge->from, edge->to);
}
}
printf("\n");
return result;
}
};
std::shared_ptr<Graph> createGraph(std::vector<std::vector<int>> matrix){
auto graph = std::make_shared<Graph>();
for (int i = 0; i < matrix.size(); ++i) {
int weight = matrix[i][0], from = matrix[i][1], to = matrix[i][2];
if(!graph->nodes.count(from)){
graph->nodes[from] = std::make_shared<Node>(from);
}
if(!graph->nodes.count(to)){
graph->nodes[to] = std::make_shared<Node>(to);
}
std::shared_ptr<Node> fromNode = graph->nodes[from], toNode = graph->nodes[to];
auto newEdge = std::make_shared<Edge>(weight, fromNode, toNode);
fromNode->nexts.emplace_back(toNode);
fromNode->out++;
toNode->in++;
fromNode->edges.emplace_back(newEdge);
graph->edges.emplace(newEdge);
}
return graph;
}
int main(){
Solution a;
set<shared_ptr<Edge>> i ;
std::vector<std::vector<int>> matrix;
matrix.clear();
matrix.emplace_back(std::vector<int>{3, 1, 3});
matrix.emplace_back(std::vector<int>{1, 2, 3});
matrix.emplace_back(std::vector<int>{9, 3, 4});
matrix.emplace_back(std::vector<int>{7, 4, 5});
matrix.emplace_back(std::vector<int>{0, 5, 1});
auto graph = createGraph(matrix);
i = a.kruskalMST(graph);
std::shared_ptr<Node> n = std::make_shared<Node>(1);
std::priority_queue<std::shared_ptr<Edge>, vector<std::shared_ptr<Edge>>, cmp> priorityQueue;
priorityQueue.emplace(std::make_shared<Edge>(3, n, n));
priorityQueue.emplace(std::make_shared<Edge>(1, n, n));
priorityQueue.emplace(std::make_shared<Edge>(7, n, n));
priorityQueue.emplace(std::make_shared<Edge>(9, n, n));
while (!priorityQueue.empty()){
auto top = priorityQueue.top(); priorityQueue.pop();
printf("%d\t", top->weight);
}
}
Prim算法
此算法可以称为“加点法”,每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。
int dist[n],state[n],pre[n];
dist[1] = 0;
for(i : 1 ~ n)
{
t <- 没有连通起来,但是距离连通部分最近的点;
state[t] = 1;
更新 dist 和 pre;
}
注意:
- 可以从任意节点出发来寻找最小生成树
- 某个点加入到被选取的点中后,解锁这个点出发的所有新的边
- 在所有解锁的边中选最小的边,然后看看这个边会不会形成环
- 如果会,不要当前边,继续考察剩下解锁的边中最小的边,重复3)
- 如果不会,要当前边,将该边的指向点加入到被选取的点中,重复2)
- 当所有点都被选取,最小生成树就得到了
实现:
class Solution {
public:
static std::set<std::shared_ptr<Edge>> primMST(Graph * graph){
std::priority_queue<std::shared_ptr<Edge>> priorityQueue; // 解锁的边按照权重值标准放到小根堆中
std::set<std::shared_ptr<Node>> nodeSet; // 已经解锁的点
std::set<std::shared_ptr<Edge>> result; // 依次挑选的的边在result里
for(const auto& it : graph->nodes){ // 1.从任意节点出发来寻找最小生成树
// node 是开始点
std::shared_ptr<Node> node = it.second;
if(!nodeSet.count(node)){
nodeSet.emplace(node);
for(auto &edge : node->edges){ // 2.此点连接的所有边解锁
priorityQueue.emplace(edge);
}
while (!priorityQueue.empty()){
// 3.在所有解锁的边中选最小的边,然后看这个边加入到被选取的点中后会不会形成环
auto edge = priorityQueue.top(); priorityQueue.top();
auto toNode = edge->to; // 可能的一个新的点
if (!nodeSet.count(toNode)) { // 该边的指向点未解锁,则解锁
nodeSet.emplace(toNode);
result.emplace(edge);
for (const auto& nextEdge : toNode->edges) { // 指向点连接的所有边解锁
priorityQueue.emplace(nextEdge);
}
}
// 该边的指向点已经解锁,直接舍弃此边
}
// 为了防止森林,所以不break
// 如果明确知道不会出现森林(或不需要防止森林),可以break
// break;
}
}
return result;
}
};