基本图论
1. 图的一些基本概念
- 图:简单来说,是由一些顶点用边连接的一个图。(例如地图上的各个点都是用边连起来的)
- 顶点:图中的数据元素。
- 边:顶点之间的逻辑关系,表示为 。
2. 有向边和无向边、有向图和无向图
- 有向边:
从字面理解,就是边有方向。例如 表示 到 有一条边,但只能从 走到 ,不能从 走到 。 - 无向边:
同理,就是边没有方向,例如 之间有一条无向边,那么 可以走到 , 也可以走到 。 - 有向图:
只用有向边的图叫有向图。 - 无向图:
只用无向边的图叫无向图。
3. 完全图、稀疏图、稠密图
- 完全图
字面就看得出来,完全图就是每个点跟其他所有点都有边连着。
完全图有一个性质: 个点的完全图有 条边 - 稀疏图和稠密图
稀疏图和稠密图没有标准的定义,你可以理解为稀疏图边较少,稠密图边较多。
4. 例题:可莉的难题
这题很多人以为要存图,实际上是玩你的。
断开第一个点需要炸掉 条边
断开第二个点需要炸掉 条边
最后这道题的答案为 。
5. 权值、度
- 权值
权值分为点权和边权,很简单,点权代表点 代表了一个 xxx,边权同理。 - 度
无向图顶点的边数叫度,有向图顶点的边数叫出度和入度。
6. 邻接矩阵
- 建立
用一个二维数组 表示。
表示 有一条有向边。 - 输入
例如:有一个 个点、 条边的无向图,每次给定输入 ,表示 有一条无向边。请将它存下来。
很容易写出代码:
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
a[x][y]=1;
}
但这样写是错误的,因为你只存了 ,而 也应该是有一条边的。(一条无向边相当于两条有向边)。所以正确代码应是:
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
a[x][y]=1;
a[y][x]=1;
}
7. 邻接表
- 建立
前置芝士:vector
邻接矩阵对于稀疏图还是太浪费空间了,我们一般用vector为基础的邻接表。
建立的方式很简单:vector<int> a[1001]; - 输入
a[x]表示点 能到的所有边。
那么输入代码:
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y;
cin>>x>>y;
a[x].push_back(y);
a[y].push_back(x);
}
a[x][y] 表示点 到点 a[x][y] 有一条边。
8. 邻接矩阵和邻接表的优点和缺点
用了一个表来表示:
| 图的类型 | 优点 | 缺点 |
|---|---|---|
| 邻接矩阵 | 好写、查询时间为 | 浪费空间 |
| 邻接表 | 节省空间 | 理解较难、查询时间为 |
9. 例题:图的存储、图的存储与出边的顺序
10. 遍历
图上遍历分两种:DFS、BFS,这里都说一下:
- DFS
前置芝士:DFS
图上DFS并没有什么特殊的:就是将所有节点可以直接到达的点枚举。注意一下边界即可。
接下类给出模板:
void dfs(int x)
{
if(x到达边界) return;
for(int i=0;i<a[x].size();i++)
{
...
dfs(a[x][i]);//遍历
...
}
}
- BFS
前置芝士:BFS
图上BFS和BFS差不多,先将一些点压入队列,然后每次弹出,将它的相邻节点全部压入队列。
接下来给出模板:
for(int i=1;i<=n;i++) if(a[i]符合要求) q.push(i);
while(!q.empty())
{
int u=q.front();q.pop();
if(u到达边界) continue;
for(int i=0;i<a[u].size();i++)
{
...
q.push(u);
...
}
}
11. 图的一些其他术语
- 环
分为自环和环。
自环:自己到自己有一条边
环:一条只有第一个和最后一个顶点重复的非空路径 - 重边
可以理解为两个点之间不只有一条直接路径,这些重复的直接路径为(一组)重边。 - 简单图
不含环与重边的图。 - 反图
可理解为取反所有边方向。(例如本来是 ,改成 )
12. 有向无环图
有向无环图(Directed Acyclic Graph),简称DAG
定义:是一张有向图,并且没有环的一张图。
对一张有向无环图,求最短路算法为 。先拓扑排序,再将方程改为 即可。
13. 练习
14. Floyd-多源最短路算法
- 多源最短路模板
题目传送门
Floyd运用的是DP的思想。假设有两个点 ,我们只有两种走法:
1.直接从 点走向 点。
那么可以得到方程f[x][y]=a[x][y]。
2.先从 点走向 点,再从 点走向 点。
那么可以得到方程f[x][y]=f[x][z]+f[z][y]。
最后取最小值便可,核心代码实现:时间复杂度
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
f[i][j]=min(f[i][j],f[i][k]+f[k][j]);
- 传递闭包
题目传送门
传递闭包是Floyd一个重要的应用。
和Floyd一样,每两个点要不直接可以通过,要不通过其他点联通。
所以状态转移方程变为f[x][y]=f[x][y]|(f[x][k]&f[k][y])
15. Dijkstra-单源最短路算法
- 松弛
后面的算法需要松弛操作。松弛操作就是更新为点 到点 的最短路和点 到 ,再从 到 的最短路。即 。 - 暴力
题目传送门
这题Floyd会炸。考虑用别的算法。
建立两个数组,,分别表示点 到点 的最短路,是否经过点 。
一开始将所有到点 的最短路设为 ,当然,从 点到 点的最短路为 。
接着,每次将没有经过的(即 的i)中选一个 最小的点,将他和他所有能直接到达的点进行松弛操作。
重复上列操作,直到遍历完所有点。
时间复杂度 ,核心代码如下:
for(int i=1;i<=n;i++)
{
int u=-1,minn=dis[0];
for(int v=1;v<=n;v++)
if(!vis[v]&&dis[v]<minn) minn=dis[v],u=v;
if(u==-1) break;
else vis[u]=1;
for(int v=0;v<a[u].size();v++)
{
node z=a[u][v];
dis[z.to]=min(dis[z.to],dis[u]+z.v);
}
}
- 优先队列优化
题目传送门
我们发现,找最小的 的这个操作,可以使用优先队列。
开始现将所有 插入优先队列,每次取队头,进行松弛。
但是我们会发现一个问题,可能队列里同时出现两个相同的数据。
但没有办法,只能插入两个相同的。
由于最多队列里会有 个数据,所以时间复杂度为
dis[1]=0;
q.push((node){1,0});
while(!q.empty())
{
node u=q.top();
q.pop();
int x=u.x,v=u.v;
if(vis[x]) continue;
vis[x]=1;
for(int i=0;i<a[x].size();i++)
{
node z=a[x][i];
if(dis[z.to]>dis[u]+z.v) q.push((node){z.to,dis[u]+z.v});
}
}
16. 拓扑排序
拓扑排序例题:B3644
这题一看就是DAG(有向无环图)
我们知道,必定要先列出祖先。祖先的入度一定为 ,不然就还会有祖先先输出。
那么新建一个队列,每一次将入度为 的点压入队列。
每弹出一个点,就将他所有可出去的儿子的入度 ,或者说删除这个点。还要将他压入队列。
队列为空时结束。
时间复杂度:由于顶多经过每个点一次,每条边一次,所以时间复杂度为 。
核心代码:
for(int i=1;i<=n;i++) if(r[i]==0) q.push(i);
while(!q.empty())
{
int u=q.front();q.pop();
for(int i=0;i<a[u].size();i++)
{
int v=a[u][i];
r[v]--;
if(r[v]==0) q.push(v);
}
ans[++cnt]=u;
}
17. 最小生成树
- 定义
一个数的最小生成树是边权和最小的生成树。 - Kruskal
为了让生成树最小,考虑每次选择最短的边。
但是,如果两个点已经连通,再连就没有意义了,故需要一个并查集来维护(你不要告诉我你不会并查集)。代码如下:
int Kruskal(int m,int n,node a[])
{
int sum=0,flag=0,number=0;
sort(a+1,a+1+m,cmp);
for(int i=1;i<=m;i++)
{
int fau=find(a[i].u),fav=find(a[i].v);
if(fau==fav) continue;
fa[fau]=fav;
sum+=a[i].c;
number++;
if(number==n-1)
{
flag=1;
break;
}
}
if(flag) return sum;
else return -1;
}
注意要对并查集初始化。

浙公网安备 33010602011771号