图就是边和点的集合
任何图都可以看做是有向图
图的两种表示方法:邻接表和邻接矩阵
最常用的表示方法还有用二维数组表示:每一行第一个表示边的权重,第二个表示from结点的值,第三个表示to结点的值
点的结构
class Node {
public:
int value;//结点的值
int in;//入度
int out;//出度
list<Node*>nexts;//邻居结点(只包括由本身指向的结点)
list<Edge*>edges;//从本身指出的边
Node(int value) :value(value) {
in = 0;
out = 0;
}
};
边的结构
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;
}
};
完整代码如下
#include<iostream>
#include<list>
#include<algorithm>
#include<unordered_map>
#include<unordered_set>
using namespace std;
class Edge;
class Node {
public:
int value;
int in;//入度
int out;//出度
list<Node*>nexts;//邻居结点(只包括由本身指向的结点)
list<Edge*>edges;//从本身指出的边
Node(int value) :value(value) {
in = 0;
out = 0;
}
};
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;
}
};
class Graph {
public:
unordered_map<int, Node*>nodes;//点的集合(通过key找到特定的结点,key就是结点上的值)
unordered_set<Edge*>edges;//边的集合
};
void creatGraph(vector<vector<int>>graph,Graph& gra) {//由二维数组构造图
for (int i = 0; i < graph.size(); i++) {
int weight = graph[i][0];
if (!gra.nodes.count(graph[i][1])) {//没有from结点
Node* from = new Node(graph[i][1]);
gra.nodes.emplace(graph[i][1], from);
}
if (!gra.nodes.count(graph[i][2])) {//没有to结点
Node* to = new Node(graph[i][2]);
gra.nodes.emplace(graph[i][2], to);
}
Node* from = gra.nodes[graph[i][1]];
Node* to = gra.nodes[graph[i][2]];
Edge* edge = new Edge(graph[i][0], from, to);
from->out++;
from->nexts.push_back(to);
from->edges.push_back(edge);
to->in++;
gra.edges.emplace(edge);
}
}
int main()
{
Graph gra;
vector<vector<int>>graph = { {1,1,3},{2,3,2},{3,2,1} };//N*3的二维数组,每一行[边的权重,from结点到值,to结点的值],所以N就代表N条边
creatGraph(graph, gra);
return 0;
}
遍历方式:
- 宽度优先遍历
数据结构:队列和set
用set记录访问过的结点,避免同一个结点多次访问
void bfs(Node* start) {
if (start == nullptr) {
return;
}
queue<Node*>q;
unordered_set<Node*>set;
q.push(start);
set.emplace(start);
while (!q.empty()) {
Node* cur = q.front();//当前队首元素
cout << cur->value << endl;
q.pop();
for (Node* next : cur->nexts) {
if (set.find(next) == set.end()) {//如果next结点之前没有遍历过,就加入队列
q.push(next);
set.emplace(next);
}
}
}
}
- 深度优先遍历
递归实现
void dfs(Node* start,unordered_set<Node*>& set) {
cout << start->value << endl;
set.emplace(start);
for (Node* next : start->nexts) {
if (set.find(next) == set.end()) {//找到第一个没有访问过的邻居结点
dfs(next, set);
}
}
}
迭代
在任何时刻,栈里放的是当前走过的结点(从底向上)
unordered_set<Node*>set;
stack<Node*>st;
st.push(gra.nodes[1]);
cout << gra.nodes[1]->value << endl;
set.emplace(gra.nodes[1]);
while (!st.empty()) {
Node* cur = st.top();
st.pop();
for (Node* next : cur->nexts) {
if (set.find(next) == set.end()) {
cout << next->value << endl;
st.push(cur);//因为回退还会回到cur结点
st.push(next);
set.emplace(next);
break;//找到一个邻居结点就重新找下一个结点
}
}
}
拓扑序:有向无环图
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序。
应用:拓扑排序常用来确定一个依赖关系集中,事物发生的顺序。例如,在日常工作中,可能会将项目拆分成A、B、C、D四个子部分来完成,但A依赖于B和D,C依赖于D。为了计算这个项目进行的顺序,可对这个关系集进行拓扑排序,得出一个线性的序列,则排在前面的任务就是需要先完成的任务。
注意:这里得到的排序并不是唯一的!就好像你早上穿衣服可以先穿上衣也可以先穿裤子,只要里面的衣服在外面的衣服之前穿就行。
实现一:利用入度,每次选择入度为0的点,然后消除该结点的影响,重复该操作,直到没有入度为0的结点。
list<Node*> DAG(Graph graph) {
list<Node*>result;
queue<Node*>q;//只要入度为0的点才会放入队列,因此队列的结点都是不依赖其他结点的
unordered_map<Node*, int>inMap;//key为某个结点,value为结点的入度(加快查询速度)
for (pair<const int,Node*> next : graph.nodes) {
inMap.emplace(next.second, next.second->in);
if (next.second->in == 0) {//初始将入度为0的点加入队列,如果没有入度为0的点,就没有拓扑排序
q.push(next.second);
}
}
while (!q.empty()) {
Node* cur = q.front();
result.push_back(cur);
q.pop();
for (Node* next : cur->nexts) {
inMap[next]--;//邻居结点入度减一;
if (inMap[next] == 0) {//入度为0的点加入队列
q.push(next);
}
}
}
return result;
}
实现二:利用点次,点次大的结点在前
127 · 拓扑排序
先解释一下本题的图表示
{1,2,4#2,1,4#3,5#4,1,2#5,3}
用#来分割各个结点,这是内部已经写好了,不需要管怎么分割的
上例可以看出一共有5个结点,1,2,3,4,5
1结点的直接邻居有2,4
2结点的直接邻居有1,4
……
它这种其实就是邻接表法表示
分析:如果X结点能走过的结点个数>Y结点能走过的结点个数,那么拓扑排序X一定在Y前面
因此:先计算所有结点的点次,然后将结点按点次从大到小排,这个顺序就是拓扑序
priority_queue的队头元素:top()
/**
* Definition for Directed graph.
* struct DirectedGraphNode {
* int label;
* vector<DirectedGraphNode *> neighbors;
* DirectedGraphNode(int x) : label(x) {};
* };
*/
class Record{//点次
public:
DirectedGraphNode* node;
long nodes;//node结点能走过几个结点
Record(DirectedGraphNode*node,long nodes){
this->node=node;
this->nodes=nodes;
}
};
class Solution {
public:
/**
* @param graph: A list of Directed graph node
* @return: Any topological order for the given graph.
*/
//当前来到cur点,返回cur可以走过的所有的点次
//缓存:order,key:cur的点次算过了,value:该结点的点次
Record* f(DirectedGraphNode* cur,unordered_map<DirectedGraphNode*,Record*>&order){
if(order.count(cur)){//cur结点的点次算过了就直接返回
return order[cur];
}
//计算cur的点次
long nodes=0;
for(DirectedGraphNode* next:cur->neighbors){
nodes+=f(next,order)->nodes;
}
Record* ans=new Record(cur,nodes+1);//再加上本身就是该结点对应的点次
order.emplace(cur,ans);//在缓存上填写cur的点次
return ans;
}
static bool cmp(Record* &r1,Record* &r2){
return r1->nodes > r2->nodes;//按点次从高到低排
}
vector<DirectedGraphNode*> topSort(vector<DirectedGraphNode*> graph) {
// write your code here
unordered_map<DirectedGraphNode*,Record*>order;
for(DirectedGraphNode* cur:graph){
f(cur,order);//计算每一个结点的点次,order里存的就是所有点的点次
}
vector<DirectedGraphNode*>result;//存放结果
vector<Record*>recordAll;//所有结点的点次
for(pair<const DirectedGraphNode*,Record*> cur:order){//拿出所有点的点次
recordAll.push_back(cur.second);
}
sort(recordAll.begin(),recordAll.end(),cmp);//将点次按从大到小排序,即为拓扑序
for(Record* r:recordAll){
result.push_back(r->node);
}
return result;
}
};
实现三:深度法,深度大的结点排前面
分析:和点次法步骤一致,也是先计算所有结点的最大深度,深度大的结点排前面。
/**
* Definition for Directed graph.
* struct DirectedGraphNode {
* int label;
* vector<DirectedGraphNode *> neighbors;
* DirectedGraphNode(int x) : label(x) {};
* };
*/
class MaxDepth{
public:
DirectedGraphNode* node;
long maxDepth;
MaxDepth(DirectedGraphNode* node,long maxDepth){
this->node=node;
this->maxDepth=maxDepth;
}
};
class Solution {
public:
/**
* @param graph: A list of Directed graph node
* @return: Any topological order for the given graph.
*/
static bool cmp(MaxDepth* md1,MaxDepth* md2){
return md1->maxDepth>md2->maxDepth;
}
MaxDepth* f(DirectedGraphNode* cur,unordered_map<DirectedGraphNode*,MaxDepth*>& order){
if(order.count(cur)){
return order[cur];
}
//计算cur的最大深度
long maxdep=0;
for(DirectedGraphNode* next:cur->neighbors){
maxdep=maxdep>f(next,order)->maxDepth?maxdep:f(next,order)->maxDepth;
}
MaxDepth* mdp=new MaxDepth(cur,maxdep+1);
order.emplace(cur,mdp);
return mdp;
}
vector<DirectedGraphNode*> topSort(vector<DirectedGraphNode*> graph) {
// write your code here
unordered_map<DirectedGraphNode*,MaxDepth*>order;
for(DirectedGraphNode* cur:graph){
f(cur,order);//计算每一个结点的最大深度
}
vector<DirectedGraphNode*>result;
vector<MaxDepth*>maxAll;//存放所有点的最大深度
for(pair<const DirectedGraphNode*,MaxDepth*> md:order){
maxAll.push_back(md.second);
}
sort(maxAll.begin(),maxAll.end(),cmp);
for(MaxDepth* cur:maxAll){
result.push_back(cur->node);
}
return result;
}
};
最小生成树:边的权重之和最小且无环的连通图。
最小生成树一般是针对无向图而言的。
方法1:Kruskal(适用于边小于点)
最初一个结点一个集合,然后按边的权重从小到大排序,先选权重小的边连通的两个结点,如果这两个结点不在一个集合,就把它们放在结果集中,同时合并两个结点所在的集合。最后判断集合总数是否为1,只有集合个数为1才存在最小生成树。
需要的数据结构:优先队列;选择最小的边
并查集:判断两个结点是否在同一个集合里
这里的图结构是最初的结构,并查集用的最初的结构
vector<Edge*>Kruskal() {
vector<Edge*>result;
priority_queue<Edge*,vector<Edge*>,MyCompare>pq;
unordered_set<int>nodes;
for (pair<const int, Node*> cur : this->nodes) {//统计所有的结点
nodes.emplace(cur.second->val);
}
for (Edge* edge : this->edges) {//将所有的边入队
pq.push(edge);
}
vector<int>value(nodes.begin(), nodes.end());
UnionSet unionset(value);
while (!pq.empty()) {
Edge* cur = pq.top();
pq.pop();
Node* from = cur->from;
Node* to = cur->to;
if (!unionset.isSameSet(from->val, to->val)) {
unionset.unionSet(from->val, to->val);
result.push_back(cur);
}
}
if (unionset.sizeMap.size() == 1) {//只有最终集合的个数为1个时才存在最小生成树
return result;
}
else {
return {};
}
}
方法2:prim(适用于边大于点)
随机选择一个结点,并将该结点连接的边加入队列,然后选择最小边然后从队列弹出并解锁该边指向的结点,并将该结点连接的边加入队列,再从队列中选择最小的边,如果该边指向的结点没有被解锁过就要这个点(如果该点被解锁了,要这个点就会产生环),同时将该结点连接的边加入队列,直到所有的点都被解锁。
需要的数据结构:优先队列:选择最小的边
set:判断结点是否被解锁
vector<Edge*>prim() {
vector<Edge*>result;
priority_queue<Edge*, vector<Edge*>, MyCompare>pq;
unordered_set<Node*>nodes;//保存解锁的结点
for (pair<const int,Node* >cur: this->nodes) {
Node* start = cur.second;//选择一个结点出发
nodes.emplace(start);//解锁该结点
for (Edge* edge : start->edges) {
pq.push(edge);
}
while (!pq.empty()) {
Edge* cur = pq.top();
pq.pop();
Node* toNode = cur->to;//该边指向的结点
if (nodes.find(toNode) == nodes.end()) {//判断指向的这个结点是否被解锁,只有没被解锁才要这个结点
nodes.emplace(toNode);//解锁该结点
result.push_back(cur);//收集结果
for (Edge* edge : toNode->edges) {//将该结点指出的边加入队列
pq.push(edge);
}
}
}
//break;//如果整个图是由多棵树也即森林组成,每次选择的结点产生的结果是不同的
}
if (nodes.size() == this->nodes.size()) {//最后所有的结点都被解锁才存在最小生成树
return result;
}
else {
return {};
}
}
629 · 最小生成树
分析:
此题用方法1方便
准备一个优先队列,按cost从小到大排序
初始化并查集,每一个城市一个集合
每次选择队列队首元素,如果这两个城市不在同一个集合,就放入结果集,同时连接他们。
class UnionSet {
public:
unordered_map<string, string>parents;//key的父亲是value
unordered_map<string, int>sizeMap;//代表结点所在集合的元素个数
UnionSet(set<string>& cities) {
for (set<string>::iterator it = cities.begin(); it != cities.end(); it++) {
string cur = *it;
parents[cur] = cur;
sizeMap[cur] = 1;
}
}
string findFather(string& cur) {
stack<string>paths;
while (cur != parents[cur]) {
paths.push(cur);
cur = parents[cur];
}
while (!paths.empty()) {
string i = paths.top();
paths.pop();
parents[i] = cur;
}
return cur;
}
bool isSameSet(string i, string j) {
return findFather(i) == findFather(j);
}
void unionSet(string& i, string& j) {
string iHead = findFather(i);
string jHead = findFather(j);
if (iHead != jHead) {
if (sizeMap[iHead] >= sizeMap[jHead]) {
parents[jHead] = iHead;
sizeMap[iHead] += sizeMap[jHead];
sizeMap.erase(jHead);
}
else {
parents[iHead] = jHead;
sizeMap[jHead] += sizeMap[iHead];
sizeMap.erase(iHead);
}
}
}
};
class Mycompare {
public:
bool operator()(Connection& c1, Connection& c2) {//小顶堆(cost小的在上面)
if(c1.cost>c2.cost){
return true;
}
else if(c1.cost==c2.cost && c1.city1>c2.city1){
return true;
}
else if(c1.cost==c2.cost && c1.city1==c2.city1 && c1.city2>c2.city2){
return true;
}
else{
return false;
}
}
};
class Solution {
public:
/**
* @param connections given a list of connections include two cities and cost
* @return a list of connections from results
*/
vector<Connection> lowestCost(vector<Connection>& connections) {
// Write your code here
vector<Connection>result;
priority_queue<Connection, vector<Connection>, Mycompare>pq;//按cost从小到大放入优先队列中
set<string>cities;
for (Connection c : connections) {
cities.emplace(c.city1);
cities.emplace(c.city2);
pq.push(c);
}
UnionSet unionset(cities);
while (!pq.empty()) {//如果队首的两个城市不在同一个集合,就将他们放入结果中,同时连接他们
Connection cur = pq.top();
pq.pop();
string city1 = cur.city1;
string city2 = cur.city2;
if (!unionset.isSameSet(city1, city2)) {
unionset.unionSet(city1, city2);
result.push_back(cur);
}
}
if(unionset.sizeMap.size()==1){//所有的城市都联通当且仅当所有的城市都在同一个集合
return result;
}
else{
return {};
}
}
};
迪杰斯特拉算法:单源最短路径(有向无负权重图)
迪杰斯特拉不允许环的累计和为负。
图结构依然采用的是开始的结构
初始形式的代码
Node* getMinDistanceAndUnselectedNode(unordered_map<Node*, int>& distanceMap, unordered_set<Node*>& selectedNodes) {//返回当前从from出发距离最近的结点,也就是distanceMap中int最小对应的结点
Node* minNode = NULL;
for (pair<Node*, int>p : distanceMap) {
Node* node = p.first;
if (!selectedNodes.count(node)) {//node不在selectedNode中
if (minNode == NULL) {
minNode = node;
}
else {
minNode = distanceMap[minNode] <= distanceMap[node] ? minNode : node;//更更新最小距离的结点
}
}
}
return minNode;
}
//从from结点开始到各个结点的最短路径
void dijiesitela(Node* from, unordered_map<Node*, int>& distanceMap) {//从frrom结点出发,距离放在distanceMap中
distanceMap.emplace(from, 0);
unordered_set<Node*>selectedNodes;//打过对号的结点(已选过的结点)
Node* minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);//从from出发距离最小且为打对号的结点
while (minNode != NULL) {
//此时minNode作为跳转点,到达与minNode连通且未打对号的结点的最小距离
int distance = distanceMap[minNode];//获取最小距离
for (Edge* edge : minNode->edges) {//与minNode连通的所有边都计算
Node* toNode = edge->to;//从minNode经edge到达的结点
if (!distanceMap.count(toNode)) {//初始时规定从from到各个点的距离是无穷大,所以如果distanceMap里没有toNode的记录,说明当前from到toNode的最小距离是无穷大
distanceMap.emplace(toNode, distance + edge->weight);//minNode作为跳转点到toNode的距离
}
else {
distanceMap[toNode] = min(distanceMap[toNode], distance + edge->weight);//如果经过minNode到达toNode的距离比之前算过的小就更新,不可以重复添加键值相同的键值对
}
}
selectedNodes.emplace(minNode);//minNode已选过了,下次不能再选minNode了
minNode = getMinDistanceAndUnselectedNode(distanceMap, selectedNodes);//找下一个从from出发距离最小且为打对号的结点
}
}
该算法的改进需要重写堆,这方面还没弄懂,以后再补充改进写法
/**
* Definition for undirected graph.
* struct UndirectedGraphNode {
* int label;
* vector<UndirectedGraphNode *> neighbors;
* UndirectedGraphNode(int x) : label(x) {};
* };
*/
class Solution {
public:
/**
* @param graph: a list of Undirected graph node
* @param A: nodeA
* @param B: nodeB
* @return: the length of the shortest path
*/
UndirectedGraphNode* getMinNode(unordered_map<UndirectedGraphNode*,int>& distanceMap,unordered_set<UndirectedGraphNode*>& selectedNode){
UndirectedGraphNode* minNode=NULL;
for(pair<UndirectedGraphNode*,int>p:distanceMap){
UndirectedGraphNode* curNode=p.first;
if(!selectedNode.count(curNode)){
if(minNode==NULL){
minNode=curNode;
}else{
minNode=distanceMap[minNode]<=distanceMap[curNode]?minNode:curNode;
}
}
}
return minNode;
}
void dijiesi(UndirectedGraphNode* from,unordered_map<UndirectedGraphNode*,int>& distanceMap){
unordered_set<UndirectedGraphNode*>selectedNode;
distanceMap.emplace(from,0);
UndirectedGraphNode* minNode=getMinNode(distanceMap,selectedNode);
while(minNode!=NULL){
int distance=distanceMap[minNode];
for(UndirectedGraphNode* neighbor:minNode->neighbors){
if(!distanceMap.count(neighbor)){
distanceMap[neighbor]=distance+1;
}else{
distanceMap[neighbor]=min(distanceMap[neighbor],distance+1);
}
}
selectedNode.emplace(minNode);
minNode=getMinNode(distanceMap,selectedNode);
}
}
int shortestPath(vector<UndirectedGraphNode*> graph, UndirectedGraphNode* A, UndirectedGraphNode* B) {
// Write your code here
unordered_map<UndirectedGraphNode*,int>distanceMap;
dijiesi(A,distanceMap);
return distanceMap[B];
}
};
本文详细介绍了图的概念,包括有向图、邻接表和邻接矩阵的表示方法。重点讲解了图的遍历,如宽度优先遍历(BFS)和深度优先遍历(DFS)的实现,以及拓扑排序在有向无环图(DAG)中的应用。此外,还涵盖了最小生成树的Kruskal和Prim算法以及迪杰斯特拉算法求解单源最短路径问题。
277

被折叠的 条评论
为什么被折叠?



