图论模板总结
单源最短路问题
单源最短路有三种算法,分别是朴素的dijistra算法,堆优化的dijidtra算法,和spfa算法
前面两种算法不能解决负权边
spfa和floyd可以求最长路
朴素版dijistra算法 复杂度:n*n
***输入一个有向图,以及起点be
***输出起点到每个点的距离dist[n]
***N和M代表数据范围,点数和边数
const int N=;M=;
int ne[M],to[M],h[N],w[M];
int cnt;
int vis[N];
int dist[N];
void add(int a,int b,int c){
w[cnt]=c;to[cnt]=b;ne[cnt]=h[a];h[a]=cnt++;
}
void dijistra(int be){
memset(dist,0x3f,sizeof(dist));
memset(vis,0,sizeof(vis));
dist[be]=1;
vis[be]=1;
int start=be;
for( int i=1;i<n;i++){
for( int j=h[start];j!=-1;j=ne[j]){
int t=to[j];
dist[t]=min(dist[start]+w[j],dist[t]);
}
int minn=0x7fffffff;
for( int j=1;j<=n;j++){
if(dist[j]<minn&&vis[j]==0){
minn=dist[j];
start=j;
}
}
vis[start]=1;
}
}
堆优化版dijistra 复杂度 nlogm
int dist[N];
queue<int>q;
int vis[N];
struct node{
int pos,val;
bool operator <(const node &a ) const{
return a.val<val;
}
};
priority_queue<node>qu;
void dijistra(int be){
while(!qu.empty()) qu.pop();
memset(dist,0x3f,sizeof(dist));
memset(vis,0,sizeof(vis));
qu.push((node){be,0});
dist[be]=0;
while(!qu.empty()){
node now=qu.top();
qu.pop();
if(vis[now.pos])continue;
vis[now.pos]=1;
for( int i=h[now.pos];i!=-1;i=ne[i]){
int j=to[i];
if(dist[j]>dist[now.pos]+w[i]){
dist[j]=dist[now.pos]+w[i];
qu.push ( (node){j,dist[j]} );
}
}
}
}
spfa 复杂度 m不稳定最坏nm
queue<int>q;
void add( int a,int b,int c){
w[cnt]=c;to[cnt]=b;ne[cnt]=h[a];h[a]=cnt++;
}
void spfa(int be){
q.push(be);
while(!q.empty()){
int now=q.front();
vis[now]=0;
q.pop();
for( int i=h[now];i!=-1;i=ne[i]){
int j=to[i];
if(dist[j]>dist[now]+w[i]){
dist[j]=dist[now]+w[i];
if(vis[j]==0){
vis[j]=1;
q.push(j);
}
}
}
}
}
多元最短路问题(待补充)
floyd算法用于求解多元最短路
算法复杂度为nnn 数据范围是200
***输入一个dist矩阵,代表每两个点之间的距离,没有路的两点距离是正无穷
***输出一个dist矩阵,代表两个点之间的距离
memset(dist,0x3f,sizeof(dist));
for()输入距离
for( int i=1;i<=n;i++){//循环枚举所有顶点
for( int j=1;j<=n;j++){
for( int k=1;k<=n;k++)
dist[j][k]=min(dist[j][k],dist[j][i]+dist[i][k]);
}
}
最小生成树
kruskal加边法,基于并查集
算法复杂度 mlogm
sort(a+1,a+1+m,cmp);//将所有边排序
long long ans=0;
for(int i=1;i<=m;i++){
if(findd(a[i].begin)!=findd(a[i].end)){//如果两个根不同,
就是不同的集合
ans+=a[i].dis;
merge(a[i].begin,a[i].end);//将两个集合合并
}
}
prim加边法
算法复杂度 nn
ll prim(){//模板res返回最小生成树边权和
int start=1;
vis[1]=1;
dist[1]=0;这里的dist代表最短的边,和dijidtera的dist含义不同
ll res=0;
for( int i=1;i<n;i++){
for(int j=h[start];j!=-1;j=ne[j]){
int t=to[j];
dist[t]=min(w[j],dist[t]);//更新所有点到点集的最小值
}
int minn=0x7fffffff;
for( int j=1;j<=n;j++){//找出离点集最近的点作为start
if(maxx>dist[j]&&vis[j]==0){
start=j;
minn=dist[j];
}
}
res+=dist[start];
}
return res;
}
spfa求负环
统计到某个点的最短路所经过点的个数,如果经过n个点,则说明存在负环。
(这里写第一种)
统计每个点的入队次数,如果某个点入队大于等于n次,则说明有负环
如果超时,可以统计所有点的入队次数,如果大于2n或3n一般存在负环
int spfa(){
while(!q.empty()) q.pop();
memset(dist,0,sizeof(dist));//简化
memset(rem,0,sizeof(rem));
for( int i=1;i<=n;i++){//简化
q.push(i);
vis[i]=1;
}
while(!q.empty()){
int now=q.front();
q.pop();
vis[now]=0;
for( int i=h[now];i!=-1;i=ne[i]){
int j=to[i];
if(dist[j]>dist[now]+w[i]){
dist[j]=dist[now]+w[i];
rem[j]=rem[now]+1;//注意步数统计要加在这里
if(rem[j]>=n) return 1;
if(!vis[j]){
vis[j]=1;
q.push(j);
}
}
}
}
return 0;
}
有向图强连通分量
kosaraju算法,先对正图dfs再对反图dfs
int h[N][2],to[M][2],ne[M][2];
int cnt1,cnt0;
int add(int a,int b,int c){//1代表正向建图,0代表反向建图
if(c==1){
to[cnt1][c]=b;ne[cnt1][c]=h[a][c];h[a][c]=cnt1++;
}
else {
to[cnt0][c]=b;ne[cnt0][c]=h[a][c];h[a][c]=cnt0++;
}
}
int vis[N];
stack<int>rem;
void dfs1(int u){
vis[u]=1;
for( int i=h[u][1];i!=-1;i=ne[i][1]){
int j=to[i][1];
if(vis[j]==0) dfs1(j);
}
rem.push(u);
}
void dfs2(int u){
vis[u]=0;
for( int i=h[u][0];i!=-1;i=ne[i][0]){
int j=to[i][0];
if(vis[j]==1) {
f[j]=f[u];
dfs2(j);
这里可以维护强连通分量内点的某些性质例如最大值
}
}
return;
}
下面是重新建图的函数,注意初始化head
注意初始化并查集
for( int i=1;i<=m;i++){
if(f[u[i]!=f[v[i]]])add(f[u[i]] ,f[v[i]],1);
}
无向图的双连通分量
差分约束
差分约束使用spfa求解
差分约束是求解不等式方程组
如果存在负环就无解
如果求两个变量最大值,将不等号变为<=,建图求最短路
如果求两个变量最小值,将不等号变为>=,建图求最长路
最近公共祖先(LCA)
树链剖分算法:
树链剖分用于维护树上任意两点到其公共祖先的某些性质
int d[N],f[N],size[N],son[N],top[N];
int dfs1( int fa,int rt){
d[rt]=d[fa]+1;f[rt]=fa;
size[rt]=1;son[rt]=0;//这两句不要忘
for( int i=h[rt];i!=-1;i=ne[i]){
int j=to[i];
if(j==fa) continue;
else dfs1(rt,j);
size[rt]+=size[j];
if(size[son[rt]]<size[j])
son[rt]=j;
}
}
void dfs2( int rt,int fa){//先序遍历dfs
if(son[fa]==rt) top[rt]=top[fa];
else top[rt]=rt;
for( int i=h[rt];i!=-1;i=ne[i]){
int j=to[i];
if(j==fa) continue;
else dfs2(j,rt);
}
}
int lca(int x,int y){
while(top[x]!=top[y]){
if(d[top[x]]<d[top[y]]) y=f[top[y]];
else x=f[top[x]];
//f[top]较大的点向上寻找
}
return d[x]<d[y]?x:y;//返回深度较小的点
}
倍增算法:
默认根节点是1,如果不是1,需要改正
int h[N], ne[M], to[M];
int cnt, root;
int fa[N][17];
void add(int a, int b)
{
to[cnt] = b,ne[cnt] = h[a],h[a] = cnt++;
}
queue<int> q;
int depth[N];
void bfs()
{
memset(depth,-1,sizeof(depth));
depth[root] = 1;
int now = root;
q.push(now);
while (!q.empty())
{
now = q.front();
q.pop();
for (int i = h[now]; i != -1; i = ne[i])
{
int j = to[i];
if(depth[j]!=-1) continue;
depth[j] = depth[now] + 1;
q.push(j);
fa[j][0] = now;
for (int x = 1; x <= 16; x++)
{
fa[j][x] = fa[fa[j][x - 1]][x - 1];
}
}
}
}
int lca(int x, int y)
{
if (depth[x] < depth[y])
swap(x, y);
for (int i = 16; i >= 0; i--)
{
if (depth[fa[x][i]] >= depth[y])
{
x=fa[x][i];
}
}
if(x==y) return y;
for( int i=16;i>=0;i--){
if(fa[x][i]!=fa[y][i]){
x=fa[x][i],y=fa[y][i];
}
}
return fa[x][0];
}
二分图
二分图的最小顶点覆盖=二分图的最大匹配
二分图的最小边、路径覆盖=v-二分图最大匹配
最大独立点集(任意两点在图中无对应边)=v-二分图最大匹配
时间复杂度为 ve
int l[N],r[N],vis[N];
int match[N];//注意下标是右边的数,储存左边
int dfs(int u){
for( int i=h[u];i!=-1;i++){
int j=to[i];
if(!vis[j]){
vis[j]=1;
if(match[j]==0||dfs(match[j])){
match[j]=u;
return true ;
}
}
}
return false;
}
int hungary(){
int res=0;//返回最大匹配数
memset(h,-1,sizeof(h));
memset(match,0,sizeof(match));
for( int i=1;i<=n;i++){
memset(vis,0,sizeof(vis));
if(dfs(i)) res++;
}
return res;
}
欧拉回路
欧拉回路指存在一条路可以经过所有边并回到原点
判断欧拉回路和欧拉路的方法的方法:
1.首先使用dfs或者并查集判断图的连通性
2.无向图:如果图中的点全部是偶点,则存在欧拉回路,任意一点都可以作为起点和终点。如果只有两个奇点,则存在欧拉回路,其中一个奇点是起点,另一个值终点。
3.有向图:把一个点上的出度记为1,入度记为-1,这个点上所有入度和出度相加就是这个点的度数。一个有向图存在欧拉回路,当且仅当该图所有的点度数为0。如果只有一个度数为1的点,一个度数为-1的点,其他所有点的度数为0,那么存在欧拉路径,其中度数为1的点为起点,度数为0的点为终点。
拓扑排序
拓扑排序主要解决已知每两个元素之间的关系,求所有元素的排序
int n;
queue<int> ans;
void top_sort(){
for( int i=1;i<=n;i++){
/*
int count=0;
for( int i=1;i<=n;i++){
if(indeg[i]==0) count++;
}
if(count == 0) return ; 说明图中存在环路
if(count > 1 ) return ;说明图中有两个点权重相同
*/
for( int j=1;j<=n;j++){
if(indeg[j]==0) {
ans.push(j);
break;
}
}
}
}
2-SAT问题
分层图
分层图主要解决对原图进行至多k次变化的问题,将图建为k+1层
注意空间开k+1倍
int main(){
int n;
cin>>n;
for( int i=1;i<=m;i++){
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);add(b,a,c);//第0层
for( int j=1;j<=k;j++){//建第1~k层
add(a+j*n,b+j*n, c );
add(b+j*n,a+j*n, c );
add(a+j*n-n,b+j*n,___c);
add(b+j*n-n,a+j*n,___c);
//下层的图有一条通向上层的路径,但是上层没有通向下层的路径
}
}
}
割点与割边
判断割边只需要将low[j]>=num[rt]改为low[j]>num[rt]。
如果不等式成立,那么( rt , j ) 就是割边
iscut标记割点,ans表示割点数量
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define N 20010
#define M 200100
int h[N],to[M],ne[M],cnt;
int low[N],num[N];//分别代表连接的最小点和时间戳
void add( int a,int b){
to[cnt]=b;ne[cnt]=h[a];h[a]=cnt++;
}
int ans=0;
int iscut[N];
int dfs( int rt,int fa){
low[rt]=num[rt]=cnt++;
int child=0;//搜索树的孩子数,不是原图的孩子数
for( int i=h[rt];i!=-1;i=ne[i]){
int j=to[i];
if(j==fa) continue;
if(num[j]==0){//该点是子节点
child++;
low[rt]=min(dfs(j,rt),low[rt]);
if(low[j]>=num[rt]&&iscut[rt]==0&&fa!=0){
iscut[rt]=1;
ans++;
}
}
else if(num[j]<num[rt]){//该点是某个祖先节点
low[rt]=min(low[rt],num[j]);
}
}
if(child>=2&&fa==0){//特判根节点
iscut[rt]=1;
ans++;
}
return low[rt];
}
int main(){
int n,m;
cin>>n>>m;
memset(h,-1,sizeof(h));
memset(low,0x3f,sizeof(low));
for( int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
add(a,b);add(b,a);
}
cnt=1;
for( int i=1;i<=n;i++){//防止不连通
if( num[i]==0)
dfs(i,0);
}
cout<<ans<<endl;
for( int i=1;i<=n;i++){
if(iscut[i]!=0) {
printf("%d ",i);
}
}
}
图论的常用技巧
- 创建虚拟原点
- 将点权移动到边权