复习3图的全家桶
图论还是来个全家桶吧,其实图论这种东西还是蛮好理解的
-1.什么是图
图(Graph)是表示物件与物件之间的关系的数学对象,是图论的基本研究对象。一个不带权图中若两点不相邻,邻接矩阵相应位置为0,对带权图(网),相应位置为∞。
有向图与无向图
如果给图的每条边规定一个方向,那么得到的图称为有向图。在有向图中,与一个节点相关联的边有出边和入边之分。相反,边没有方向的图称为无向图。
图的术语
阶(Order):
图G中顶集V的大小称作图G的阶。
子图(Sub-Graph):
当图G'=(V',E')其中V‘包含于V,E’包含于E,则G'称作图G=(V,E)的子图。每个图都是本身的子图。
生成子图(Spanning Sub-Graph):
指满足条件V(G') = V(G)的G的子图G'。
导出子图(Induced Subgraph):
以图G的顶点集V的非空子集V1为顶点集,以两端点均在V1中的全体边为边集的G的子图,称为V1导出的导出子图;以图G的边集E的非空子集E1为边集,以E1中边关联的顶点的全体为顶点集的G的子图,称为E1导出的导出子图。
度(Degree):
一个顶点的度是指与该顶点相关联的边的条数,顶点v的度记作d(v)。
入度(In-degree)和出度(Out-degree):
对于有向图来说,一个顶点的度可细分为入度和出度。一个顶点的入度是指与其关联的各边之中,以其为终点的边数;出度则是相对的概念,指以该顶点为起点的边数。
自环(Loop):
若一条边的两个顶点为同一顶点,则此边称作自环。
路径(Path):
从u到v的一条路径是指一个序列v0,e1,v1,e2,v2,...ek,vk,其中ei的顶点为vi及vi - 1,k称作路径的长度。如果它的起止顶点相同,该路径是“闭”的,反之,则称为“开”的。一条路径称为一简单路径(simple path),如果路径中除起始与终止顶点可以重合外,所有顶点两两不等。
行迹(Trace):
如果路径P(u,v)中的边各不相同,则该路径称为u到v的一条行迹。
轨道(Track):
如果路径P(u,v)中的顶点各不相同,则该路径称为u到v的一条轨道。
0. 图的存储
邻接矩阵
实际上就是一个二维数组,我们假设它是G,那么\(G[i][j]=k\)就表示\((i,j)\)之间有一条长为k的边,这里我们插入和查询的都是\(O(1)\)的,但是我们的空间复杂度达到\(O(n^2)\)的,所以实际上并不是特别实用
领接表
这里我们就上到我们比较高大上的领接表了,我们用它遍历就比较简单了
邻接表,存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。
对于下面这幅图
我们所建出来的邻接表就是这样的
刚刚开始这个我们还是不能理解这个神奇的东西,那么我们就可以先看下代码再来理解。对于建图中最重要的就是\(addedge\)函数了
我们就来看下\(addedge\)函数的具体的代码
void addedge(int x,int y){
nxt[++tot]=head[x];
head[x]=tot;
to[tot]=y;
}
代码很短这里我建了一条有向但是没有边权的边
ps:其实无向图就正着建一次,反着在建一次
head[x]:从点x出发的最后一条边的标号
nxt[i]:与标号i从同一点出发的下一条边
to[i]:标号i的边到的点
我们遍历的代码就很简单了
for(int i=haed[x];i;i=nxt[i]){
int v=to[i];
//do something
}
当然我们这里可能会遇到一些边是还有一个属性的,那就是边权,那么我们代码就是长这样的
void addedge(int x,int y,int z){
nxt[++tot]=head[x];
head[x]=tot;
to[tot]=y;
cost[tot]=z;
}
和上面同一理论cost[i]:标号i的边的权值
1.搜索
显然图论中许多算法都是基于搜索的啊,但是搜索的顺序可能有一点不同
深度优先搜索Depth First Search
这是所有算法中最最基础的吧,这里也就不多说了,也就是按深度来搜索,代码大多数时候就是递归实现的啊
void dfs(int x){
if(some resons)return;
//do someting
for(int i=head[x];i;i=nxt[i]){
int v=to[i];
if(!vis[v]){
vis[v]=1;
dfs(v);
}
//do someting
}
//do someting
}
广度优先搜索 Breadth First Search
这种算法其实就是一种以广度或者说宽度来决定优先顺序的算法,它往往是以队列为基础的一种算法
代码也就很简单,主要就是以队列为核心
void bfs(int s){
queue<int> q;
q.push(s);
vis[s]=1;
//do something
while(!q.empty()){
int x=q.front();
q.pop();
for(int i=head[x];i;i=nxt[i]){
int v=to[i];
//do something
if(!vis[v]){
//do something
vis[v]=1;
q.push(v);
}
}
}
}
2.最短路算法
松弛
这里我们将松弛操作单独来讲,它算是所有最短路算法的核心
我们可以看个很有趣的东西
令我们找到当前\(i,j\)之间的最短路为\(dis(i,j)\)
现在我们找到一个点\(k\)
如果满足
我们就更新\(dis(i,j)=dis(i,k)+dis(k,j)\)
Floyd
Floyd就是一种基于松弛的dp啊,时间复杂度\(O(n^3)\)
这里我就不写代码了
Bellman-Ford (SPFA)
我是真的不会Bellman-Ford啊只能讲讲它的队列优化SPFA,其实我是在\(NOIP2017\)场上突然明白了SPFA的,一定是我太弱了。直白的说,它就是BFS+松弛,代码也十分的好写
这是luogu3371 的代码,这就是一道模板题
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
int n,m,s,tot,nxt[500001],cost[500001],to[500001],head[100001],d[100001];
bool inque[100001];
void addedge(int x,int y,int z){
nxt[++tot]=head[x];
head[x]=tot;
to[tot]=y;
cost[tot]=z;
}
void spfa(int x){
memset(d,0x7f,sizeof(d));
queue<int> q;
q.push(x);
inque[x]=1;
d[x]=0;
while(!q.empty()){
int now=q.front();q.pop();
inque[now]=0;
for(int i=head[now];i;i=nxt[i]){
int u=to[i];
if(d[u]>d[now]+cost[i]){
d[u]=d[now]+cost[i];
if(!inque[u]){
q.push(u);
inque[u]=1;
}
}
}
}
}
int main(){
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++){
int x,y,z;scanf("%d%d%d",&x,&y,&z);
addedge(x,y,z);
}
spfa(s);
for(int i=1;i<=n;i++){
printf("%d ",d[i]==0x7f7f7f7f?0x7fffffff:d[i]);
}
return 0;
}
Dijkstra
这个算法我也不好说,直白点就是堆广搜
有时候SPFA会被卡,我们就要有Dijkstra,但是不能有负边
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
int n,m,s,tot,nxt[200001],head[100001],to[200001],d[100001],cost[200001];
struct node{
int x;int dis;
inline bool operator < (const node &b) const {
return dis>b.dis;
}
};
inline void addedge(int x,int y,int z){
nxt[++tot]=head[x];
head[x]=tot;
to[tot]=y;
cost[tot]=z;
}
void dijkstra(int xx){
memset(d,0x7f,sizeof(d));
d[xx]=0;
priority_queue<node> q;
q.push(node{xx,0});
while(!q.empty()){
int x=q.top().x;
int dis=q.top().dis;
q.pop();
if(dis>d[x])continue;
for(int i=head[x];i;i=nxt[i]){
int v=to[i];
if(d[v]>d[x]+cost[i]){
d[v]=d[x]+cost[i];
q.push(node{v,d[v]});
}
}
}
}
int main(){
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<=m;i++){
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
addedge(x,y,z);
}
dijkstra(s);
for(int i=1;i<=n;i++){
printf("%d ",d[i]);
}
return 0;
}
3.Tarjan算法
Tarjan算法有很多种这里我就讲下最常见的
割点
了解定义,算法浅显
// luogu-judger-enable-o2
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,m,ind,ans,tot,dfn[100001],low[100001],head[100001],nxt[200001],to[200001];
bool iscp[100001];
void addedge(int x,int y){
nxt[++tot]=head[x];
head[x]=tot;
to[tot]=y;
}
void find_cp(int u,int fa) {
int child=0;
dfn[u]=low[u]=++ind;
for(int i=head[u];i;i=nxt[i]) {
int v=to[i];
if(!dfn[v]) {
child++;
find_cp(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u])iscp[u]=1;
} else {
if(dfn[v]<dfn[u] and v!=fa) {
low[u]=min(low[u],dfn[v]);
}
}
}
if(fa<0 and child==1){
iscp[u]=0;
}
}
int main() {
scanf("%d%d",&n,&m);
for(int i=1; i<=m; i++) {
int x,y;
scanf("%d%d",&x,&y);
addedge(x,y);
addedge(y,x);
}
ind=0;
for(int i=1; i<=n; i++) {
if(!dfn[i]) {
find_cp(i,-1);
}
}
for(int i=1; i<=n; i++) {
if(iscp[i]) {
ans++;
}
}
printf("%d\n",ans);
for(int i=1; i<=n; i++) {
if(iscp[i]) {
printf("%d ",i);
}
}
return 0;
}