C++ 图论(写到手软)

目录

一、图的各种定义

1.图的构成

2.树 

3.二分图 

二、图的存储

1.邻接矩阵

2.邻接表 

3.最后

三、各类算法(大汇总😃)

1.判断二分图

2.拓扑排序(用于DAG)

 3.最短路

(1)定义

(2)弗洛伊德(floyd:浓浓的DP气味;多源最短路)

前置定义……如下!

代码……如下! 

(3)迪杰斯拉(dijkstra) 

前置定义……如下!

普通代码……如下!

这就是极限吗,代码……如下!

(4)贝尔曼·福德( bellman-ford)

前置定义……如下!

(5)SPFA( bellman-ford的升级版)

时间复杂度……如下!

代码……如下! 

4.生成树 

(1)定义

(2)克鲁斯卡尔(kruskal:求最小生成树的算法;贪心的气息)

前置定义……如下

luogu模版题P3366,代码如下!

(3)新的概念,新的玩意 (次小生成树)

5.强联通分量

(1)前置定义

 (2)Tarjan求强联通分量

四、最后


一、图的各种定义

1.图的构成

  • 图由点和边组成的。
  • 有向图、有向边,是有方向的。
  • 是一点所关联的边数。
  • 出度有向图中,从一个点出去的边数;入度有向图中,从一个点进来的边数。
  • 自环,邪恶的东西,能寄掉一些算法,需要特殊处理,一条边两端连着同一个点。
  • 路径,从一个点走到另一个点(可含环);简单路径,就是不含环的路径。
  • 环,从一点出发在绕回这个点的路径
  • 注:有向无环图(DAG),若设动态规划状态为点,转移为为边,这一定是DAG。   

2.树 

,是一种无环、无向、连通的图,并且如果有x个点,那么就有x-1条边

  • 如果去掉连通那么就变成了森林。
  • 如果去掉无向那么就变成了DAG了,而有一种特殊的DAG——有向树(2023.10.14对错误内容进行了更改,可放心食用😀),有向树分为内向树,和外向树,边指向根节点,是内向树,边非指向根节点,就是外向树。
  • 如果去掉无环那就是一个普通的无向图,但是若只有一个环,那就是基环树(章鱼图)。但若每一条边都只在一个环里,那就是仙人掌图。

3.二分图 

就这样两边只有点中间只有边(竟然还***有自环)!一但有有奇数长度的环(奇环)就不是二分图了。

二、图的存储

1.邻接矩阵

int g[N][N];

随便建一个二维数组,g_{i,j}代表i点与j点之间有一条,边权为 g_{i,j},若没有边权有边则g_{i,j}=1

  • 内存消耗(空间复杂度):S(N^2)
  • 查询效率(时间复杂度):O(1)

详细讲解~点这!icon-default.png?t=N7T8https://blog.csdn.net/weixin_66767924/article/details/128594325

2.邻接表 

vector<pair<int,int> >edge[N];//第一种
vector<int> edge[N];//第二种

 edge[v][i].firstedge[v][i].second分别表示与v点相连的第i个点(第i条边)和与v点相连的第i条边的边权,若没有边权,就要用第二种了(上方代码)

  • 内存消耗(空间复杂度):S(n+m)
  • 查询效率(时间复杂度):O(n)

3.最后

总结一下,各有利弊,邻接矩阵空间多,速度快,邻接表空间少,速度相较下慢一点(2024.1.29作者前来吐槽了一句“6,我怎么只写了一半,后半段没写”,并默默地填充了后半段,现可放心食用😃)

三、各类算法(大汇总😃)

1.判断二分图

染色的方法~代码如下……

int col[maxn];
//col[i]==0 i点还没决定放哪边
//col[i]==1 i点放左边
//col[i]==2 i点放右边 

int main()
{
	cin >>n >> m;
	for (int i=1;i<=m;i++)
	{
		int s,e;
		cin >> s >> e;
		add_edge(s,e);
		add_edge(e,s);
	}
	bool able=true;//能够。
	for (int i=1;i<=n;i++)
		if (col[i] == 0)
		{
			col[i] = 1;
			queue<int> q;//还需要更新周围点放哪边的那些点 
			q.push(i); 
			while (q.size())
			{
				int now=q.front();
				q.pop();
				for (int i=0;i<g[now].size();i++)
				{
					int j=g[now][i];//是一条从now -> j的边
					if (col[j] == 0) //j点还没有放
					{
						col[j] = 3-col[now];
						q.push(j);
					}
					else if (col[now] == col[j]) able=false;
				}
			} 
		}
	if (able) cout << "yes\n";
	else cout << "no\n";
		
	return 0;
}

2.拓扑排序(用于DAG)

以下图片的拓扑排序是1、3、2、4、5,首先1、3入度为0加入队列并删除,2、4入度变为0加入队列并删除,5入度变为0加入队列并删除。

但是删除过于繁琐、费时,我们可以假删。

luoguB3644模版题代码如下……

#include<bits/stdc++.h>
using namespace std;
#define ll long long
vector<ll> edge[105];
bool vis[105];
ll n,in[105];
void add_edge(ll v,ll u){
	edge[v].push_back(u);
	in[u]++; 
}
queue<ll> Q;
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		ll v;
		while(1){
			cin>>v;
			if(v!=0)add_edge(i,v);
			else break;
		}	
	}
	for(int i=1;i<=n;i++){
		if(in[i]==0){
			cout<<i<<' ';
			Q.push(i);
		}//若入度为0直接加入,为后面bfs做基础。
	}
	while(Q.size()){
		int v=Q.front();
		Q.pop();
		for(int i=0;i<edge[v].size();i++){
			in[edge[v][i]]--;
			if(in[edge[v][i]]==0){
				cout<<edge[v][i]<<' ';
				Q.push(edge[v][i]);
			}
		}
	}//bfs,启动!
}

 3.最短路

(1)定义

从某点走到另一个点的最短路径就是最短路,现在我们有一个问题,怎么算最短路的长度

  • 单源最短路:从一点出发,到所有点的最短路。
  • 多源最短路:从多点出发,到其他点的最短路。

我们设dist_{i,j}为i到j的最短路长度。而现在我们要介绍一个等式三角等式,dist_{i,j}+dist_{j,k} \geq dist_{i,k} 。

(2)弗洛伊德(floyd:浓浓的DP气味;多源最短路)
前置定义……如下!
  • 需要用邻接接矩阵。
  • f_{i,j,k}表示从 j 走到 k 经过的点的编号小于等于 i 的最短路。
代码……如下! 
int main()
{
	cin >> n >> m;
	memset(f,0x3f,sizeof(f));
	for (int i=1;i<=n;i++)
		f[0][i][i] = 0;
	for (int i=1;i<=m;i++)
	{
		int s,e,d;
		cin >> s >> e >> d;
		f[0][s][e] = min(f[0][s][e],d);
	}
	
	for (int i=1;i<=n;i++)
		for (int j=1;j<=n;j++)
			for (int k=1;k<=n;k++)
				f[i][j][k] = min(f[i-1][j][k], f[i-1][j][i] + f[i-1][i][k]);
				
	for (int i=1;i<=n;i++)
		for (int j=1;j<=n;j++)
			cout << f[n][i][j] << "\n";
	
}

但是我们还可以优化空间,将f_{i,j,k}变为f_{j,k}(移步下方链接)。 

详细讲解~点这!icon-default.png?t=N7T8https://blog.csdn.net/weixin_66767924/article/details/128578498

(3)迪杰斯拉(dijkstra) 
前置定义……如下!

松弛操作:通过一个点,将周围所有点最短路变短。

  • dist_i到i点的最短路长度。
  • done_i代表i的的最短路是否求出来了 。
  • g邻接表。
普通代码……如下!

时间复杂度:O(n^2+m)

void dijkstra(int s)//计算s到其他所有点的最短路 
{
	memset(dist,0x3f,sizeof(dist));
	dist[s] = 0;
	for (int i=1;i<=n;i++){
		//找还没有求出最短路的dist值最小的那个点 
		int p=0;
		for (int j=1;j<=n;j++)//O(n^2)
			if (!done[j] && (p==0 || dist[j] < dist[p])) p = j;
		done[p] = true;
		//更新其他点的最短路。 
		for (int j=0;j<g[p].size();j++)//O(m)
		{
			int q=g[p][j].first;
			int d=g[p][j].second;//这是一条从 p->q 长度为d的边。 
			if (dist[q] > dist[p] + d) dist[q] = dist[p] + d;
		} 
	}
}
这就是极限吗,代码……如下!

时间复杂度(STL堆,以下代码):O((n+m)log(n+m))

时间复杂度(手写堆,非以下代码):O((n+m)log n)

void dijkstra(int s)//计算s到其他所有点的最短路 。 
{
	memset(dist,0x3f,sizeof(dist));
	dist[s] = 0;
	priority_queue<pair<int,int> > heap;//first 最短路的相反数(转换小根堆,压入转相反数,压出转回去,你细品), second 点的编号。 
	for (int i=1;i<=n;i++)
		heap.push(make_pair(-dist[i],i));
	for (int i=1;i<=n;i++)
	{
		while (done[heap.top().second])
			heap.pop();
		//找还没有求出最短路的dist值最小的那个点。 
		int p = heap.top().second;
		heap.pop();
		done[p] = true;
		//更新其他点的最短路。 
		for (int j=0;j<g[p].size();j++)
		{
			int q=g[p][j].first;
			int d=g[p][j].second;//这是一条从 p->q 长度为d的边 。 
			if (dist[q] > dist[p] + d){
				dist[q] = dist[p] + d;
				heap.push(make_pair(-dist[q],q));
			}//松弛。 
		} 
	}
}
(4)贝尔曼·福德( bellman-ford)
前置定义……如下!
  • 时间复杂度:O(nm)
  • 无数次松弛的成果!太伟大了!
cin>>n>>m;//n点m边。
for(int i=1;i<=m;i++)
	cin >>s[i]>>e[i]>>d[i];
memset(dist,0x3f,sizeof(dist));
dist[1]=0;//只能从1开始。
//dist[i]表示从1到i的最短路。
for (int i=1;i<n;i++)
	for (int j=1;j<=m;j++)
		dist[e[j]]=min(dist[e[j]],dist[s[j]]+d[j]);
//第i条边是从s[i]->e[i]长度为d[i]。 
(5)SPFA( bellman-ford的升级版)
时间复杂度……如下!
  • 最坏:O(nm)
  • 平均:O(km)k \leq 20)。
代码……如下! 
int dist[maxn];//dist[i]到i点的最短路长度 
bool inq[maxn];//inq[i]代表i点是否在队列中 
void spfa(int s)//计算s到其他所有点的最短路 
{
	memset(dist,0x3f,sizeof(dist));
	dist[s]=0;
	queue<int> q;//用来存储可能改变其他点最短路的点 
	q.push(s);
	inq[s] = true;
	//最坏O(nm)
	//平均O(km) k<20 
	while (q.size() != 0)//队列不为空
	{
		int a = q.front();
		q.pop();
		inq[a] = false;
		for (int i=0;i<g[a].size();i++)
		{
			int b = g[a][i].first;
			int c = g[a][i].second;//一条从 a->b 长度为c的边
			if (dist[b] > dist[a] + c)
			{
				dist[b] = dist[a] + c;
				if (!inq[b])
				{
					inq[b] = true;
					q.push(b);
				}
			} 
		}
	} 
}

4.生成树 

(1)定义

一张图只保留一些边,变成一颗树,这棵树就是生成树最小生成树是生成树中边权之和最小的。只有在图连通的时候才有生成树。

(2)克鲁斯卡尔(kruskal:求最小生成树的算法;贪心的气息)
前置定义……如下

我们将一个图每条边按权值排序,然后枚举排完序的序列,在空白图上进行加边,枚举时若加入某条边会形成环那就不加入了,否则加。

那怎么判段有环呢?我们可以用并查集,每加入一条边时,判断左右端点是否根节点一样,若一样,说明加边后会产生环,否则可以加。  

而这个加边不用,真正的加,我们求的是最小生成树,用一个sum记录边权和就可以了。

luogu模版题P3366,代码如下!
#include<bits/stdc++.h>
using namespace std;
int to[5005],n,m;
int go(int p){
	if (to[p] == p) return p;
	else return to[p]=go(to[p]);
}
struct edge{
	int s,e,d;
}ed[200005];
bool cmp(edge a,edge b){
	return a.d<b.d;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++)
		cin>>ed[i].s>>ed[i].e>>ed[i].d;
	sort(ed+1,ed+m+1,cmp);
	for(int i=1;i<=n;i++)
		to[i]=i;
	int ans=0;
	for(int i=1;i<=m;i++){
		int p1=ed[i].s,p2=ed[i].e,d=ed[i].d;
		if (go(p1)!=go(p2)){
			ans+=d;
			to[go(p1)]=go(p2);
		}
	}
	for(int i=1;i<n;i++){
		if(go(i)!=go(i+1)){
			cout<<"orz";
			return 0;
		}
	}
	cout<<ans<<endl;
}



(3)新的概念,新的玩意 (次小生成树)

次小生成树,第二小的生成树,字面意思。加入一条新边,删掉一条旧边。

5.强联通分量

强联通分量,一个有向图的子图,其中任意两点都可以互相到达,要尽可能大。那怎么求强联通分量数量呢?强联通分量本质就是环,让我们在下文中解决这个问题。

(1)前置定义

我们把它搞成一个形似树的东西

树边:不可言传,只可意会! 

回边(形成环):从一个点指向自己祖先的边。 

横叉边(扩大环):既不是树边,也不是回边。

 (2)Tarjan求强联通分量

luogu模版题B3609代码如下!

#include<bits/stdc++.h>
using namespace std; 
#define ll long long
vector<ll> edge[100005]; 
ll n,m,s;
void add_edge(ll s,ll e){
	edge[s].push_back(e);
} 
int num;//当前dfs了几个点。
int dfn[10005];//dfn[i] 代表i点是第几个被dfs到的点。
int low[10005];
//low[i] 代表从i点出发 走 树边、回边、能扩大环的横叉边 能走到的所有点中dfn最小的是多少。
stack<int> sta;//用来存储 所有被 dfs 过 但还没有求出 强连通分量的点。
bool instack[10005];//instack[i] 代表i点是否在栈里面。
int cnt;//当前总共有多少个强连通分量。
int belong[10005];//belong[i]代表i点属于第几个强连通分量
void dfs(int i){
	num++,dfn[i]=low[i]=num;
	sta.push(i);
	instack[i]=1;
	for(int k=0;k<edge[i].size();k++){
		ll j=edge[i][k];
		if(!dfn[j]){//树边
			dfs(j);
			low[i]=min(low[i],low[j]); 
		}else{//回边横叉边
			if(instack[j])low[i]=min(low[i],dfn[j]);
		}
	}
	if(dfn[i]==low[i]){//i是它所属的强连通分量的最上面的点 
		cnt++;//新增了一个强连通分量
		while(sta.top()!=i){//栈顶不等于当前点
			belong[sta.top()]=cnt;//i和sta.top()一定属于同一个强连通分量
			instack[sta.top()]=0;
			sta.pop();
		}
		sta.pop();
		instack[i]=0;
		belong[i]=cnt;
	}
} 
vector<ll> Belong[10005];//Belong[i]是一个vector用来记录第i个强连通分量里面的所有点。
bool vis[10005];//vis[i]代表第i个强联通分量是否已经输出过了。 
int main(){
	cin>>n>>m;
	for(int i=1;i<=m;i++){
		int p1,p2;
		cin>>p1>>p2;
		add_edge(p1,p2);
	}
	for(int i=1;i<=n;i++){
		if(!dfn[i])dfs(i);
		Belong[belong[i]].push_back(i);
	}
	cout<<cnt<<'\n'//有几个强连通分量 ;
	for(int i=1;i<=cnt;++i){
		sort(Belong[i].begin(),Belong[i].end());
	}
	for(int i=1;i<=n;++i){
		if(vis[belong[i]])continue;
		vis[belong[i]]=1;
		for(int j=0;j<Belong[belong[i]].size();++j){
			cout<<Belong[belong[i]][j]<<" ";
		}cout<<'\n';
	}
}

四、最后

  • 终于!写完了。
  • 每个算法都各有所长。
  • 6
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值