基本图论
1. 图的一些基本概念
- 图:简单来说,是由一些顶点用边连接的一个图。(例如地图上的各个点都是用边连起来的)
- 顶点:图中的数据元素。
- 边:顶点之间的逻辑关系,表示为
(v_i,v_j) 。
2. 有向边和无向边、有向图和无向图
- 有向边:
从字面理解,就是边有方向。例如a\to b 表示a 到b 有一条边,但只能从a 走到b ,不能从b 走到a 。 - 无向边:
同理,就是边没有方向,例如(a,b) 之间有一条无向边,那么a 可以走到b ,b 也可以走到a 。 - 有向图:
只用有向边的图叫有向图。 - 无向图:
只用无向边的图叫无向图。
3. 完全图、稀疏图、稠密图
- 完全图
字面就看得出来,完全图就是每个点跟其他所有点都有边连着。
完全图有一个性质:n 个点的完全图有(n-1)+(n-2)+\cdots+3+2+1=\dfrac{(n-1+1)\times(n-1)}{2}\approx n^2 条边 - 稀疏图和稠密图
稀疏图和稠密图没有标准的定义,你可以理解为稀疏图边较少,稠密图边较多。
4. 例题:可莉的难题
这题很多人以为要存图,实际上是玩你的。
断开第一个点需要炸掉
断开第二个点需要炸掉
最后这道题的答案为
5. 权值、度
- 权值
权值分为点权和边权,很简单,点权代表点i 代表了一个 xxx,边权同理。 - 度
无向图顶点的边数叫度,有向图顶点的边数叫出度和入度。
6. 邻接矩阵
- 建立
用一个二维数组a_{i,j} 表示。 - 输入
例如:有一个n 个点、m 条边的无向图,每次给定输入i,j ,表示i\to j 有一条无向边。请将它存下来。
很容易写出代码:cin>>n>>m; for(int i=1;i<=m;i++) { int x,y; cin>>x>>y; a[x][y]=1; }但这样写是错误的,因为你只存了
a_{i,j} ,而j\to i 也应该是有一条边的。(一条无向边相当于两条有向边)。所以正确代码应是: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]表示点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]表示点x 到点a[x][y]有一条边。
8. 邻接矩阵和邻接表的优点和缺点
用了一个表来表示:
| 图的类型 | 优点 | 缺点 |
|---|---|---|
| 邻接矩阵 | 好写、查询时间为 |
浪费空间 |
| 邻接表 | 节省空间 | 理解较难、查询时间为 |
9. 例题:图的存储、图的存储与出边的顺序
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-单源最短路算法
- 松弛
后面的算法需要松弛操作。松弛操作就是更新为点s 到点i 的最短路和点s 到j ,再从j 到i 的最短路。即dis_i=\min(dis_i,dis_j+e_{i,j}) 。 - 暴力
题目传送门
这题Floyd会炸。考虑用别的算法。
建立两个数组,dis[i],vis[i] ,分别表示点s 到点i 的最短路,是否经过点i 。
一开始将所有到点s 的最短路设为\infty ,当然,从s 点到s 点的最短路为0 。
接着,每次将没有经过的(即vis[i]=0 的i)中选一个dis[i] 最小的点,将他和他所有能直接到达的点进行松弛操作。
重复上列操作,直到遍历完所有点。
时间复杂度O(n^2+m) ,核心代码如下: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 的这个操作,可以使用优先队列。
开始现将所有s 插入优先队列,每次取队头,进行松弛。
但是我们会发现一个问题,可能队列里同时出现两个相同的数据。
但没有办法,只能插入两个相同的。
由于最多队列里会有m 个数据,所以时间复杂度为O(m\log m) 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; }注意要对并查集初始化。