一、图的结构

以下代码只是提供一个思路,本人并没有实际运行(造例子太麻烦,等刷到题再说吧,咔咔咔咔咔咔,我个菜鸡)

与图有关的问题的关键在于图是如何进行储存的,所以我们应该有自己熟悉的图的结构,当遇到问题的时候应该将题目所给的图结构转化为自己熟悉的结构

通常图的存储方法有邻接表和邻接矩阵,下面是本人借鉴大佬的比较完整的图的结构:

//图结构,从节点和边两个角度进行考虑
//因为是一个图,所以要保持一致
//那么在储存的时候我们需要保存的是指针
//这样的话,一个地方进行更改,其余的也会跟着改变

//图的边结构
struct node;
struct edge
{
	int weight;
	node* from;
	node* to;
	edge(int weight, node* from, node* to)
	{
		this->weight = weight;
		this->from = from;
		this->to = to;
	}
};
//图的节点的数据结构
struct node
{
	int value;//值
	int in;//入度
	int out;//出度
	vector<node*> nodes;//该点为起点,与该点相连的节点
	vector<edge*> edges;//以该点为起点的边
	node(int value)
	{
		this->value = value;
		in = 0;
		out = 0;
	}
};

//图的数据结构
struct graph
{
	map<int, node*> nodes;
	vector<edge*> edges;
};


//示例,将题中所给的图转化为以上方式的图
//matrix为一个数组,其为n*3的矩阵,每一行的结构为[weight,from,to]
graph build(vector<vector<int> > matrix)
{
	graph gra;
	int len = matrix.size();
	for (int i = 0; i < len; ++i)
	{
		int weight = matrix[i][0];
		int from = matrix[i][1];
		int to = matrix[i][2];
		//将结点加入到图中
		if(gra.nodes.count(from)==0)
		gra.nodes[from] = new node(from);
		if(gra.nodes.count(to)==0)
		gra.nodes[to] = new node(to);
		
		//增加结点的信息
		node* fromnode = gra.nodes[from];
		node* tonode = gra.nodes[to];
		edge* e = new edge(weight, fromnode, tonode);
		fromnode->out++;
		tonode->in++;
		fromnode->nodes.push_back(tonode);
		fromnode->edges.push_back(e);
		gra.edges.push_back(e);
	}
	return gra;
}
int main()
{
	vector<vector<int> > matrix = { {5,1,2},{6,2,4},{8,1,3},{7,3,4} };
	graph gra = build(matrix);
	int len = gra.nodes.size();
	cout << len << endl;
	map<int, node*>::iterator itr = gra.nodes.begin();
	while (itr != gra.nodes.end())
	{
		cout << itr->second->value << ' ';
		itr++;
	}
	cout << endl;
	len = gra.edges.size();
	for (int i = 0; i < len; ++i)
		cout << gra.edges[i]->from->value << ' ' << gra.edges[i]->to->value << ' ' << gra.edges[i]->weight << endl;
	system("pause");
	return 0;
}
(1)图的遍历,bfs和dfs

比较需要关注的是非递归的方式实现的深度优先遍历。
图的遍历需要进行标记,即标记结点是否遍历过来防止结点的重复遍历。

//对图进行宽度优先遍历
//使用队列
//上述的图的结构中使用得到的信息是nodes

//从当前结点开始进行bfs
void bfs(int cur,graph gra)
{
	node* tmp = gra.nodes[cur];//获得当前结点的信息
	if (tmp == NULL)
		return;
	queue<node*> q;
	set<node*> s;//用来记录结点是否遍历过,防止重复遍历
	q.push(tmp);
	s.insert(tmp);
	while (!q.empty())
	{
		tmp = q.front();
		q.pop();
		cout << tmp->value << endl;
		int len = tmp->nodes.size();
		for (int i = 0; i < len; ++i)
		{
			//如果该结点还没有遍历过,那么将其标记并加入到set,标记集合中
			if (s.count(tmp->nodes[i]) == 0)
			{
				s.insert(tmp->nodes[i]);
				q.push(tmp->nodes[i]);
			}
		}
	}
}

//对图的深度优先遍历
//使用栈,同样需要标记是否会重复访问
//从值为cur的节点开始进行深度优先遍历
void dfs(int cur, graph gra)
{
	node* tmp = gra.nodes[cur];
	if (tmp == NULL)
		return;
	stack<node*> s1;
	set<node*> s2;
	s1.push(tmp);
	s2.insert(tmp);
	cout << tmp->value << endl;
	while (!s1.empty())
	{
		tmp = s1.top();
		s1.pop();
		int len = tmp->nodes.size();
		for (int i = 0; i < len; ++i)
		{
			//还没有遍历过
			//将原来的节点和本节点入栈并且标记
			if (s2.count(tmp->nodes[i]) == 0)
			{
				s1.push(tmp);
				s1.push(tmp->nodes[i]);
				s2.insert(tmp->nodes[i]);
				cout << tmp->nodes[i]->value;
				break;
			}
		}
	}
}

(2)图的拓扑排序
//图的拓扑排序
//适用的图是没有环并且是有向图
//需要的是图的节点信息
//需要节点的in信息和nodes信息
vector<int> sortedTopology(graph gra)
{
	//得到图的节点和其入度信息
	map<node*, int> inmap;
	//入度为0的节点
	queue<node*> zeroInQueue;
	int len = gra.nodes.size();
	for (int i = 0; i < len; ++i)
	{
		inmap[gra.nodes[i]] = gra.nodes[i]->in;
		if (gra.nodes[i]->in == 0)
			zeroInQueue.push(gra.nodes[i]);
	}
	vector<int> res;
	node* cur;
	while (!zeroInQueue.empty())
	{
		cur = zeroInQueue.front();
		zeroInQueue.pop();
		res.push_back(cur->value);
		len = cur->nodes.size();
		for (int i = 0; i < len; ++i)
		{
			inmap[cur->nodes[i]]--;
			//只将入度为0的加入队列
			if (inmap[cur->nodes[i]] == 0)
				zeroInQueue.push(cur->nodes[i]);
		}
	}
	return res;
}

今天是划水的一天,呃呃呃呃呃。。。。
在这里插入图片描述

(3)最小生成树
a、Kruskal算法

思路:其解决的是无向图的最小生成树问题。首先将所有边根据代价从小到大进行排序,然后对边进行遍历。如果加入当前的这条边,得到的图没有环,那么就说明可以使用这条边,那么就留下该条边。
对于是否有环则使用并查集进行判断。
在解决问题的过程中使用到之前定义的图的结构中的点集合和边界。点集合用来初始化并查集并判断是否形成环,边集合用来根据代价进行排序得到最后的结果。

代码

#include<iostream>
#include<string>
#include<cstring>
#include<memory>
#include<set>
#include<map>
#include<vector>
#include<stack>
#include<queue>
using namespace std;
struct edge;
struct node
{
	int value;
	int in;
	int out;
	vector<edge*> edges;
	vector<node*> nodes;
	node(int value)
	{
		this->value = value;
		in = 0;
		out = 0;
	}
};
struct edge
{
	node* from;
	node* to;
	int weight;
	edge(node* from,node* to,int weight)
	{
		this->from = from;
		this->to = to;
		this->weight = weight;
	}
};
struct graph
{
	vector<node*> nodes;
	vector<edge*> edges;
};

//并查集
//结点的value对应的parent 
map<int,int> parent; 
//记录每个结合的数量
//主要是在merge的时候将数量少的合并到数量多的 
map<int,int> total;

//初始化并查集
void ini(vector<int> nodes)
{
	int len = nodes.size();
	for(int i = 0;i<len;++i)
	{
		map[nodes[i]] = i;
		total[nodes[i]] = 1;
	}
 } 
 
 //查找结合的根
 int getRoot(int x)
 {
 	if(x!=parent[x])
 	{
 		parent[x] = getRoot(parent[x]);
	 }
	 return parent[x];
  } 
  //没有化简的情况
  /*
  int getRoot(int x)
  {
  	if(x==parent[x])
  	return x;
  	return getRoot(parent[x]);
   } */
   
  
  //将两个结点所在集合进行合并
  void merge(int x,int y)
  {
  	int px = getRoot(x);
  	int py = getRoot(y);
  	if(px!=px)
  	{
  		//将数量少的挂到数量多的树下 
  		if(total[px]<total[py])
  		{
  			parent[px] = py;
  			total[py] += total[px];
		  }
		  else
		  {
		  	parent[py] = px;
		  	total[px] += total[py];
		  }
	  }
   } 
   
   //判断两个点是否在同一个集合中
   bool isSame(int x,int y)
   {
   	int px = getRoot(x);
   	int py = getRoot(y);
   	if(px==py)
   	return true;
   	return false;
	} 
	
	//Kruskal算法
	//返回的是最小生成树涉及的边
	int cmp(edge* x,edge* y)
	{
		return x->weight<y->weight;
	}
	
	vector<edge*> kruskalMST(graph gra)
	{
		vector<int> nodes;
		int len = gra.nodes.size();
		for(int i = 0;i<len;++i)
		   nodes.push_back(gra.nodes[i]->value);
		vector<edge*> edges;
		int len = gra.edges.size();
		for(int i = 0;i<len;++i)
		   edges.push_back(gra.edges[i]);
		//将边从小到大进行排列 
		sort(edges,0,len,cmp);
		//逐条边进行遍历,看是否能将边加入找到最小生成树
		bool flag = false;
		vector<edge*> res;
		for(int i = 0;i<len;++i)
		{
			flag = isSame(edges[i]->from,edges[i]->to);
			//不在同一个集合中,也就是说这条边是可以用的 
			if(!flag)
			{
				merge(edges[i]->from,edges[i]->to);
				res.push_back(edges[i]);
			 } 
		 } 
		 return res;
	 } 
b、Prime算法

思路:Prime算法从点出发。从一个点开始,选择以其为起点的边中代价最小的边,然后改变解锁了新的节点,然后再选择新解锁节点连接的代价最小的边,最终解锁的节点集合如果是所有的节点,那么选择的边就组成了最小生成树。

实现:因为要选择代价最小的边,所以我们在这里使用小根堆。

C++中可以使用优先队列实现堆的结构:
大根堆:priority_queue< int> q;
小根堆:priority_queue< int,vector< int>,greater< int>> q;
头文件:queue,functional

代码

	 //Prime算法求最小生成树
	 //参数表示的是从gra图的n节点开始寻找最小生成树 
	 struct cmp
	 {
	 	bool operator()(edge* x,edge* y)
	 	{
	 		return x->weight>y.weight;
		 }
	 }
	 vector<edge*> primeMST(graph gra,node* n)
	 {
	 	vector<edge*> res;
	 	int num = gra.nodes.size();
	 	//小根堆,每次返回代价最小的边 
	 	priority_queue<int,vector<int>,cmp >q;
	 	//解锁的节点 
	 	set<node*> s;
	 	set.insert(n);
	 	int len = n->edges.size();
	 	for(int i = 0;i<len;++i)
	 	q.push(n->edges[i]);
	 	edeg* tmp = NULL;
	 	node* cur = NULL;
	 	while(!q.empty())
	 	{
	 	//找到了所有的节点 
	 		if(s.size()==num)
	 		break;
	 	//选取最小的边,解锁新的节点 
	 		tmp = q.top();
	 		q.pop();
	 		cur = tmp->to;
	 		if(s.count(cur)==0)
	 		{
	 			res.push_back(tmp);
	 			s.insert(cur);
	 			len = cur->edges.size();
	 			for(int i = 0;i<len;++i)
	 			q.push(cur->edges[i]);
			 }
		 }
		 return res; 
	  } 

总结:
可以看出两个算法中Kruskal是以边为主要的考虑核心
Prime算法则是以点出发解决问题

今天又是划水的一天,开始运动,明天开始好好学习
web永远是我学习的强劲动力!!!
在这里插入图片描述

(4)单源最短路径:Dijkstra算法

解决的问题:求解给定源点到图上的其余点的最短的距离。

思路:根据已知的源点到点的最短距离更新源点到新的点的最短距离,直到所有已知条件都使用完,得到的就是源点到各点的最短距离。

要求:Dijkstra算法要求图中不能有边权重为负的边形成的环,但是与实际问题相结合,一般不存在代价为负的问题,所以要求降低为图中不能出现权值为负的边。

a、方法一:

思路:通过遍历的方法得到现在已知的最短的距离
代码

//方法一:通过遍历的方法得到距离最短的点,根据这个点来更新其他点的信息
//map:key为node,value为源点到该点的当前的最短距离
//set用来判断该点的信息是否已经用来进行过更新
//求解的是从head到其余点的最短距离 

//返回未使用过的到源点距离最短的点
//此处使用的是遍历的方法 
node* getMinDIstanceAndUnselectedNode(map<node*,int> distanceMap,set<node*> selectedNodes)
{
	node* minNode = NULL;
	int distance = 0x7fffffff;
	map<node*,int>::iterator itr = distancesMap.begin();
	while(itr!=distanceMap.end())
	{
		if(itr->second<distance&&selectedNode.count(itr->first)==0)
		{
			minNode = itr->first;
			distance = itr->second;
		}
		itr++;
	 } 
	 return minNode;
 } 
map<node*,int> Dijkstra1(node* head)
{
	map<node*,int> distanceMap;
	set<node*> selectedNodes
	//开始的时候只有head到head本身的距离 
	distanceMap[head] = 0;
	
	//选择距离最短并且没有使用过的点进行更新 
	node* minNode = getMinDistanceAndUnselectedNode(distanceMap,selectedNodes);
	node* node_to = NULL;
	while(minNode!=NULL)
	{
		int len = minNode.edges.size();
		int distance = distanceMap[minNode];
		for(int i = 0;i<len;++i)
		{
			node_to = minNode.edges[i]->to;
			//还没有出现源点到该点的距离的路径 
			if(distanceMap.count(node_to)==0)
			distanceMap[node_to] = distance+minNode.edges[i]->weight;
			//否则判断新出现的路径的距离是否比原来的短 
			else
			{
				distanceMap[node_to] = min(distanceMap[node_to],distanc+minNode.edges[i]->weight);
			}
			//使用过的便不再使用,将其加入set中标记 
			selectedNodes.insert(minNode);
			minNode = getMinDistanceAndUnselectedNode(distanceMap,selecedNodes);
		}
		return distanceMap;
	}
 } 
b、方法二

思路:方法一中我们是通过遍历的方式找到目前的到源点距离最短的点,那么时间复杂度就是O(N),每次返回的是距离最小的点,那么我们就可以思考是否能使用小根堆。

系统提供的小根堆,在我们将对象放入堆以后就不能对其进行修改。
但是在Dijkstra算法中我们需要根据这个得到的最短的距离更新堆中的对象,所以我们需要自己实现堆。

方法:首先通过数组实现对的二叉树结构,然后记录每个点在堆中的位置。更新时使用更新数组中的值和位置数组完成对堆的更新和插入操作。

代码

//通过堆得到当前已知的到源点距离最短的点
class NodeHeap{
	private nodes*[10000];//使用数组实现的堆结构
	private map<node*,int> heapIndexMap;//结点在堆中的位置
	private map<node*,int> distanceMap;//每个点当前到源点的距离
	private int size;//堆的大小
	
	//构造函数 
	public NodeHeap()
	{
		size = 0;
	}
	//判断堆是否为空 
	public bool isEmpty()
	{
		return size==0;
	}
	//交换堆中两个点的位置
	//需要改变点在数组中的位置以及其下标 
	private void swap(int index1,int index2)
	{
		node* tmp = nodes[index1];
		nodes[index1] = nodes[index2];
		nodes[index2] = tmp;
		indexHeapMap[nodes[index1]] = index1;
		indexHeapMap[nodes[index2]] = index2;
	}
	
	//更新了堆中某点的值
	//在本问题中更新值可能变小,所以与其父亲进行比较 
	private void insertHeapify(node* n,int index)
	{
		while(distanceMap[index]<distanceMap[(index-1)/2])
		{
			swap(index,(index-1)/2);
			index = (index-1)/2;
		}
	}
	
	//弹出堆顶的时候,需要判断0位置上的点是否是最小的,然后决定其是否向下 
	private void heapify(int index,int size)
	{
		int left = 2*index+1;
		while(left<size)
		{
			int smallest = left+1<size&&distanceMap[nodes[left+1]]<distance[nodes[left]]?left+1:left;
			smallest = distanceMap[nodes[index]]<distanceMap[nodes[smallset]]?index:smallest;
			if(smallest==index)
			break;
			swap(index,smallest);
			index = smallest;
			left = index*2+1;
		}
	}
	
	//判断某个点是否进过堆 
	private bool isEnter(node* n)
	{
		return indexHeapMap.count(n);
	}
	
	//判断某个点是否现在还在堆中 
	private bool inHeap(node* n)
	{
		return isEnter(n)&&indexHeapMap[n]!=-1;
	}
	
	//在得到一个新的距离的时候,接下来的操作有三种:
	//1、之前没有源点到该点的距离,将其插入堆中
	//2、更新堆中已经有的距离
	//3、忽略,之前已经得到源点到该点的最短距离
	public void addOrUpdateOrIgnore(node* n,int distance)
	{
		//在堆中update
		if(inHeadp(n))
		{
			distanceMap[n] = min(distance,distanceMap[n]);
			//调整堆
			insertHepify(n,heapIndexMap[n]); 
		 } 
		 //如果之前就没有进过堆,add 
		 if(!isEntered(n))
		 {
		 	nodes[size] = n;
		 	distanceMap[n] = distance;
		 	heapIndexMap[n] = size;
		 	size++;
		 	insertHeapify(n,size);
		 }
		 //否则这个点已经进过堆了,ignore 
	 } 
	 
	 //返回堆顶的最小距离的点 
	 public node* pop()
	 {
	 	swap(0,size-1);
	 	//表示进堆并且已经出来 
	 	heapIndexMap[size-1] = -1;
	 	heapify(0,--size);
	 	return nodes[size];
	 }
	 
}; 

map<node*,int> Dijllstra2(node* head)
{
	map<node*,int> reault;
	NodeHeap nodeHeap = NodeHeap();
	nodeHeap.addOrUpdateOrIgnore(head,0);
	node* minNode;
	node* node_to = NULL;
	while(!nodeHeap.isEmpty())
	{
		//弹出堆中最小的
		//弹出的目的是用其来更新其余的点
		//这个点到源点的最短距离是确定了的 
		minNode = nodeHeap.pop();
		int distance = distanceMap[minNode];
		int len = minNode.edges.size();
		for(int i = 0;i<len;++i)
		{
			node_to = minNode.edges[i]->to;
			nodeHeap.addOrUpdateOrIgnore(node_to,distance+minNode.edges[i]->weight);
		}
		//该点已经使用过,其应当就是最短的距离
		//那么其就是最后的结果 
		result[minNode] = distance;
	}
	return result;
	}

今天是七夕呢,啊,王一博还是那么帅!!!
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值