万里之行,始于足下。本博客总结暑期学习到的部分图论模板,以便于日后查询使用。图论好难啊QAQ本蒟蒻第一次尝试写博客,水平有限,难免存在疏漏不足,恳请诸位看官斧正。倘若我的文章可以帮到你,十分荣幸。感谢18级学长wzq19级学长lsw提供的灵感。
目录
1.搜索
搜索是遍历图的一种重要方法。
(1)BFS(广度优先搜索)
BFS即广度优先搜索,所谓广度优先。就是每次都尝试访问同一层的节点。 如果同一层都访问完了,再访问下一层。这样做的结果是,BFS 算法找到的路径是从起点开始的最短合法路径。换言之,这条路所包含的边数最小。在 BFS 结束时,每个节点都是通过从起点到该点的最短路径访问的。在这里需要使用queue,即队列存储数据。上伪代码:
BFS()
{
声明queue
标记初始状态start为检查过
将初始状态start压入queue
while(队列非空)
{
使用q.front()访问队首t
弹出当前队首
if(t满足目标状态)
{
结果找到,BFS结束
}
枚举t能转移到的所有状态next
if(next合法)
{
标记next为检查过
将next压入队列
}
}
搜索结束
}
(2)DFS(深度优先搜索)
DFS即深度优先搜索,就是说每次都尝试向更深的节点走,谓之“不撞南墙不回头”。DFS可以更加优秀的解决无向图的连通性问题,之后也可以拓展到有向图的tarjan算法,之后再可以处理割点、割边问题。上伪代码。
DFS()
{
if(n满足目标状态)
{
结果找到,DFS回溯或结束
}
如有必要,可以设置限制条件剪枝
枚举n能转移到的所有状态next:
if(next合法)
{
标记next为检查过
递归调用DFS(next)
}
视情况撤回next的标记进行回溯
}
int main()
{
声明初始状态start
标记start为检查过
DFS(start)
撤回start的标记
}
这里需要的注意的是,在搜索中需要设置一个vis数组记录访问,避免对数据的多次访问造成时间的浪费。
2.拓扑排序
这是一个经典问题也是第一个我们完整的学习的从如何抽象建图到解决问题的实例:大学课程中有:单变量微积分,线性代数,离散数学概述,概率论与统计学概述,语言基础,算法导论,机器学习等。当我们想要学习 算法导论 的时候,就必须先学会 离散数学概述 和 概率论与统计学概述,不然在课堂就会听的一脸懵逼。当然还有一个更加前的课程单变量微积分。所以我们需要将这些课程排序,而这样的排序又和我们之前学的排序不同,我们将之命名为拓扑排序,排序之后的序列称之为拓扑序。把每一门课都当作一个点vi,如果有两门课u和v满足必须先上u再上v,那么建立有向边(u,v),这样我们就建立了一张图。
如题:洛谷-UVA10305 给任务排序 Ordering Tasks
#include <bits/stdc++.h>
using namespace std;
int m,n,dev[105],vis[105];//两个数组存储每个点的入度和访问记录
vector<int> edge[105];//存储边
vector<int> ans;//存储答案
queue<int> q;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);//加快读写
while(cin>>n>>m)
{
if(n==0&&m==0)
{
break;
}//-----一下为拓扑排序重点
ans.clear();
fill(vis,vis+105,0);
fill(dev,dev+105,0);
for(int i=1;i<=n;i++)
{
edge[i].clear();
}//一波初始化可别忘了
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
edge[x].push_back(y);//存图
dev[y]++;//入度加一
}
for(int i=1;i<=n;i++)
{
if(!dev[i])
{
q.push(i);
}
}//先把入度为零,即不需要前提条件的事件揪出来扔队列
while(!q.empty())//是不是有BFS的感觉?
{
int p=q.front();
vis[p]=1;
q.pop();
ans.push_back(p);//存储答案
for(int i=0;i<edge[p].size();i++)
{
int t=edge[p][i];
dev[t]--;//遍历找到与入度为零点相连的点,入度减一
if(!dev[t]&&!vis[t])
{
q.push(t);
}//如果它度数也变零了而且没访问过就扔队列里
}
}//--------以上为拓扑排序重点
for(int i=0;i<ans.size();i++)//遍历vector容器输出答案
{
cout<<ans[i];
if(i<ans.size()-1)
{
cout<<' ';
}
}
cout<<endl;
}
return 0;
}
拓扑排序的思路就是不停的“干掉”入度为1的点,按照被“干掉”的先后顺序确定答案序列。
图 G 可以拓扑排序的等价条件,即图 G 有向无环图。这样的图被称为 DAG 。这种图天生适合 DP ,所以非常重要。
3.并查集
严格来说,并查集属于集合论,从名字上解释,并查集是对集合进行两种操作,合并与查询,下面会展示并查集比较基础的几个函数。
如题:洛谷-P1551 亲戚
#include <iostream>
using namespace std;
int n,m,p,f[10010];//f数组存储各节点的父节点
inline void init()
{
for(int i=1;i<=n;i++)
{
f[i]=i;
}
return ;
}//初始化,父节点设置为我自己
int findf(int x)
{
if(f[x]==x)
{
return x;
}
else
{
return f[x]=findf(f[x]);
}
}//查询,利用递归查询父节点
inline void mer(int x,int y)
{
f[findf(y)]=findf(x);
return ;
}//合并,将两个点父节点统一
//并查集比较基础几个的函数如上
int main()
{
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);//加速读写
cin>>n>>m>>p;
init();//初始化
for(int i=0;i<m;i++)
{
int x,y;
cin>>x>>y;
mer(x,y);//合并
}
for(int i=0;i<p;i++)
{
int x,y;
cin>>x>>y;
if(findf(x)==findf(y))
{
cout<<"Yes"<<endl;
}
else
{
cout<<"No"<<endl;;
}//看父节点是否相同就知道是否有亲缘关系了
}
return 0;
}
我们还可以尝试将“连在一起”的节点的父节点统一起来以实现压缩路径,提高查询效率。
int findf(int x)
{
return x==f[x]?x:(f[x]=findf(f[x]));
}
emmmm并查集还有按秩合并等高阶操作,这里我还尚未掌握,以后会持续更新的。
“山再高,往上攀,总能登顶;路再长,走下去,定能到达。”路漫漫其修远兮,吾将上下而求索,共勉!