图论的整合
图
有若干个节点,有若干条边连接节点。(两个点之间不是必须相连)
比如:
有向图
可以理解为边上面有箭头的图,比如下面这张图:
在这张图中,点 111 可以通过这条有向边到达点 222,但是点 222 到达不了点 111。
有向图的边是有单向性的。
无向图
只要连了边,边两端的点都可以互相到达。
这张图是无向图,里面任一点都可以互相到达。
这就是一张连通图。
连通图
一张无向图,如果任意一个点都可以到达图上的所有点,那么这张无向图就是连通图。
如图1。
强连通图
一张有向图,如果任意两个点都可以互相到达,这就是一张强连通图。
比如上面的这张图就是不联通的,不是强连通图。
如果再加上一条边:
现在这就是一张强连通图了。
带权图
可以理解为图中的边被加上了一个权值,经过这条边就要付出对应权值的代价。
比如:
在这张图中,点 111 走到点 444 的总权值是 3+5+2=103+5+2=103+5+2=10。
图的存储
邻接矩阵
假设有一个 NNN 个点的图,我们可以开一个 N∗NN*NN∗N 的二维数组 aaa,aija_{ij}aij 表示点 iii 到点 jjj 有没有边。
如果是带权图,也可以用 aija_{ij}aij 表示点 iii 到点 jjj 边的权值。
例题1
题目描述
给出一个无向图,顶点数为 nnn,边数为 mmm。n≤1000,m≤10000n\le1000,m\le10000n≤1000,m≤10000
输入格式
第一行两个整数 n,mn,mn,m,接下来有 mmm 行,每行两个整数 u,vu,vu,v,表示点 uuu 到点 vvv 有一条边。
输出格式
由小到大依次输出每个点的邻接点,邻接点也按照由小到大的顺序输出。
首先 nnn 只有 100010001000,可以用邻接矩阵存图。
是无向图,所以 uuu 到 vvv 和 vvv 到 uuu 都有一条边,所以要双向存。
#include<bits/stdc++.h>
#define psp putchar(' ')
#define endl putchar('\n')
using namespace std;
const int M=1e3+5;
int read(){int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();return x*f;
}
void print(int x){if(x<0)putchar('-'),x=-x;if(x<10){putchar(x+'0');return;}print(x/10);putchar(x%10+'0');
}
int n,m;
int a[M][M];
signed main(){n=read(),m=read();for(int i=1;i<=m;i++){int u=read(),v=read();//双向存边a[u][v]=1;a[v][u]=1;}for(int i=1;i<=n;i++){for(int j=1;j<=n;j++){if(a[i][j])print(j),psp;//有边,输出。}endl;}
}
邻接表
假如一张图有 204820482048 个点,那么用邻接矩阵就要开 204822048^220482 的数组,那如果这张图只有 101010 条边,开这么大的空间就全浪费了。
这时候就要用到邻接表。
可以用动态数组来存点和边,减少空间的浪费。
vector<int>a[N];
例题2
题目描述
给出一个无向图,顶点数为 nnn,边数为 mmm。n≤1000,m≤10000n\le1000,m\le10000n≤1000,m≤10000
输入格式
第一行两个整数 n,mn,mn,m,接下来有 mmm 行,每行两个整数 u,vu,vu,v,表示点 uuu 到点 vvv 有一条边。
输出格式
第i行输出第点 iii 的所有邻接点,邻接点按照度数由小到大输出,如果度数相等,则按照编号有小到大输出。
按照度数排序,用邻接表更方便。
#include<bits/stdc++.h>
#define endl putchar('\n')
using namespace std;
const int N=1e6+5;
int read(){int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();return x*f;
}
void print(int x){if(x<0)putchar('-'),x=-x;if(x<10){putchar(x+'0');return;}print(x/10);putchar(x%10+'0');
}
int n,m;
vector<int>a[N];
int d[N];
bool cmp(int a,int b){//按照度数排序return d[a]<d[b]||(d[a]==d[b]&&a<b);
}
signed main(){n=read(),m=read();for(int i=1;i<=m;i++){int u=read(),v=read();//邻接表存图a[u].push_back(v);a[v].push_back(u);d[u]++,d[v]++;}for(int i=1;i<=n;i++){sort(a[i].begin(),a[i].end(),cmp);for(int j=0;j<a[i].size();j++)print(a[i][j]),psp;endl;}
}
例题3
题目描述
K(1≤K≤100)K(1\le K\le100)K(1≤K≤100) 只奶牛分散在 N(1≤N≤1000)N(1\le N\le1000)N(1≤N≤1000) 个牧场。现在她们要集中起来进餐。
牧场之间有 M(1≤M≤10000)M(1\le M\le10000)M(1≤M≤10000) 条有向路连接,而且不存在起点和终点相同的有向路。她们进餐的地点必须是所有奶牛都可到达的地方。
那么,有多少这样的牧场呢?
输入格式
第1行输入 K,N,MK,N,MK,N,M。
接下来 KKK 行,每行一个整数表示一只奶牛所在的牧场编号。
接下来 MMM 行,每行两个整数,表示一条有向路的起点和终点。
输出格式
所有奶牛都可到达的牧场个数。
由于 NNN 最大只有 100010001000,KKK 最大只有 100100100,所以可以考虑枚举每一头牛,然后记他们可以到达的点,最后的答案就有所有牛都能到达点的数量。
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
int read(){int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();return x*f;
}
void print(int x){if(x<0)putchar('-'),x=-x;if(x<10){putchar(x+'0');return;}print(x/10);putchar(x%10+'0');
}
int n,m,k;
int cow[N];
vector<int>a[N];
int vis[N];
int dis[N];//dis[i]表示点i有几头牛可以到达
signed main(){k=read(),n=read(),m=read();for(int i=1;i<=k;i++)cow[i]=read();while(m--){int u=read(),v=read();a[u].push_back(v);}for(int i=1;i<=k;i++){queue<int>q;for(int j=1;j<=n;j++)vis[j]=0;vis[cow[i]]=1;q.push(cow[i]);while(!q.empty()){int x=q.front();q.pop();dis[x]++;//记录for(int p=0;p<a[x].size();p++){int y=a[x][p];if(vis[y])continue;vis[y]=1;q.push(y);}}}int ans=0;for(int i=1;i<=n;i++)ans+=(dis[i]==k);print(ans);
}
例题4
P3144 [USACO16OPEN] Closing the Farm S
依次遍历每个仓库关闭,检查是否能遍历到除关闭仓库外的所有仓库。
#include<bits/stdc++.h>
#define endl putchar('\n')
using namespace std;
const int N=1e6+5;
int read(){int x=0,f=1;char c=getchar();while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();return x*f;
}
void putstr(string s){for(char v:s)putchar(v);
}
int n,m;
vector<int>a[N];
int cl[N];
int vis[N];
signed main(){n=read(),m=read();while(m--){int u=read(),v=read();a[u].push_back(v);a[v].push_back(u);}int st=1;for(int i=1;i<=n;i++){while(cl[st])st++;queue<int>q;q.push(st);for(int i=1;i<=n;i++)vis[i]=0;vis[st]=1;while(!q.empty()){int x=q.front();q.pop();for(int i=0;i<a[x].size();i++){int y=a[x][i];if(vis[y])continue;if(cl[y])continue;vis[y]=1;q.push(y);}}bool flag=1;for(int i=1;i<=n;i++){if(cl[i])continue;if(!vis[i])flag=0;}cl[read()]=1;if(flag)putstr("YES");else putstr("NO");endl;}
}
最短路
一张带权图,一个点 xxx 到另一个点 yyy 所经过边的最小权值和称为点 xxx 到点 yyy 的最短路。
Dijkstra
一种高效的求最短路的算法,缺点是不能求带负权的最短路。
它的主要用途是求一个点到图中其他点的最短路。
具体过程
最开始,除了起点外所有点没有被标记过,起点是最开始被选中的点。
开始模拟:找到被选中的点连接的最小边权的且没有被标记过的点 yyy,记录最短路,然后点 yyy 被标记,并成为下一个被选中的点,直到所有点都被标记。
例题5
P4779 【模板】单源最短路径(标准版)
具体代码见单源最短路径
SPFA
讲解+例题见带负权的最短路问题
见无向图 ↩︎