图论算法整理与模板总结(一)
这篇博客主要是acwing算法基础课的学习结果,同时作为图论的复习。
具体链接见: link
DFS与BFS
暂时不整理。
拓扑排序
在图论中,拓扑排序(Topological Sorting)是一个有向无环图(DAG, Directed Acyclic Graph)的所有顶点的线性序列。且该序列必须满足下面两个条件:
(1)每个顶点出现且只出现一次。
(2)若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
有向无环图(DAG)才有拓扑排序,非DAG图没有拓扑排序一说。
具体思路:
(1)建图
(2)统计所有的顶点的入度。
(3)构造队列,将所有入度为0的顶点入队,删除入度为0的顶点,更新队列和答案数组。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
vector<vector<int>> map;
const int N=1e5+10;
int degree[N];
int ans[N];
int n,m;
bool Toposort()
{
queue<int> q;
int cnt=0;
for(int i=1;i<=n;i++)
if(degree[i]==0)
{
q.push(i);
//cnt++;
}
//int t=0; //总共输出了多少个;
while(q.size())
{
int u=q.front();
q.pop();
ans[cnt++]=u;
for(int i=0;i<map[u].size();i++)
{
int t=map[u][i]; //u->t;
degree[t]--;
if(degree[t]==0)
{
q.push(t);
}
}
}
//cout<<cnt<<endl;
return cnt==n;
}
int main()
{
cin>>n>>m;
map.resize(n+1);
for(int i=0;i<m;i++)
{
int a,b;
cin>>a>>b;
map[a].push_back(b);
degree[b]++; //a->b;
}
if(Toposort())
{
for(int i=0;i<n;i++)
cout<<ans[i]<<' ';
}
else
puts("-1");
return 0;
}
最短路
正权图(Dijkstra)
朴素版Dij:
这里的模板注意:
不需要在一开始更新dis,在循环的过程中第一次一定选择第一个顶点并进行距离的更新。
复杂度为O(n^2),n为图中的顶点数;
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int N=510;
int map[N][N];
int dis[N];
bool vis[N];
int n,m;
int dij()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0;
for(int i=0;i<n-1;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
if(!vis[j]&&(t==-1||dis[j]<dis[t]))
t=j;
vis[t]=true;
for(int j=1;j<=n;j++)
dis[j]=min(dis[j],dis[t]+map[t][j]);
}
if(dis[n]==0x3f3f3f3f) return -1; //这里要特别注意?
else return dis[n];
}
int main()
{
cin>>n>>m;
memset(map,0x3f,sizeof map);
while(m--)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
map[a][b]=min(map[a][b],c);
}
int t=dij();
cout<<t<<endl;
return 0;
}
堆优化版Dij:
优化了找到距离源点最近的那个点的步骤,直接用堆存储。
注意堆里可能会有冗余的元素,需要判断,即一个顶点可能在堆里多次出现,只利用第一次出现进行更新。
#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<vector>
#include<map>
#include<cstring>
using namespace std;
const int N=1e5+10;
typedef pair<int,int> PII;
vector<vector<PII>> g;
int n,m;
int dis[N];
bool vis[N];
int dij()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0;
priority_queue<PII,vector<PII>,greater<PII>> heap;
heap.push({0,1}); //距离放在前面,编号放在第二个;
while(heap.size())
{
auto t=heap.top();
heap.pop();
int u=t.second,d=t.first;
if(vis[u]) continue; //已经用u更新过了;(可能会有冗余)
vis[u]=true;
for(int i=0;i<g[u].size();i++)
{
int v=g[u][i].first;
int w=g[u][i].second;
if(dis[v]>w+dis[u])
{
dis[v]=w+dis[u];
heap.push({dis[v],v});
}
}
}
if(dis[n]==0x3f3f3f3f) return -1;
return dis[n];
}
int main()
{
cin>>n>>m;
g.resize(n+1);
int a,b,c;
for(int i=0;i<m;i++)
{
cin>>a>>b>>c;
g[a].push_back({b,c});
}
int k=dij();
cout<<k<<endl;
return 0;
}
有边数限制的最短路
Bellman-ford 算法:
这是存储所有边,并利用边的信息求最短路的一种方法;
边权可以为负,复杂度恒为O(mn)
不能出现负环。
题目有边数限制的要求,则一定只能用bellman_ford.
寻找有k条边的最短路;
#include<cstdio>
#include<iostream>
#include<cstring>
using namespace std;
const int N=510,M=10010;
struct Edge{
int a,b,c;
}edges[M];
int n,m,k;
int dis[N];
int last[N]; //存储上一次更新后的结果;
void bellman_ford()
{
memset(dis,0x3f,sizeof dis);
dis[1]=0;
for(int i=0;i<k;i++) //最多k条边, 每循环一次添加一条边
{
memcpy(last,dis,sizeof dis);
for(int j=0;j< m;j++)
{
auto e=edges[j];
dis[e.b]=min(dis[e.b],last[e.a]+e.c); //这里很关键,本次的结果用上一次存储的来更新;
}
}
}
int main()
{
cin>>n>>m>>k;
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
edges[i]={a,b,c};
}
bellman_ford();
if(dis[n]>0x3f3f3f3f / 2) cout<<"impossible"<<endl;
else cout<<dis[n]<<endl;
return 0;
}
spfa
spfa是求最短路的另一种方法,它的优点是允许有负权值出现,且复杂度的上限为O(mn);
它是bellman-ford的队列优化形式(仅有被松弛后的边继续参与后续的计算):
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<queue>
using namespace std;
const int N=1e5+10;
typedef pair<int,int> PII;
vector<vector<PII>> g;
int dis[N];
bool vis[N];
int n,m;
int spfa()
{
memset(dis,0x3f,sizeof dis);
queue<int> q;
dis[1]=0;
q.push(1);
vis[1]=true;
while(q.size())
{
int t=q.front();
q.pop();
vis[t]=false;
for(int i=0;i<g[t].size();i++)
{
int u=g[t][i].first;
if(dis[u]>dis[t]+g[t][i].second)
{
dis[u]=dis[t]+g[t][i].second;
if(!vis[u])
{
q.push(u);
vis[u]=true;
}
}
}
}
return dis[n];
}
int main()
{
cin>>n>>m;
g.resize(n+1);
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
}
int k=spfa();
if(k==0x3f3f3f3f) cout<<"impossible"<<endl;
else
cout<<k<<endl;
return 0;
}
spfa还可以用来判断图中是否出现负环:
核心思想是判断当前最短路径边数是否超过n。如果超过一定存在负环。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<vector>
#include<map>
#include<queue>
using namespace std;
int n,m;
vector<vector<pair<int,int>>> g;
int cnt[2010],dis[2010];
bool vis[2010];
bool spfa()
{
//注意,由于是寻找负环,所以这里并不需要进行正无穷的初始化
//默认为0就可以
queue<int> q;
for(int i=1;i<=n;i++) //先把所有顶点都放入队列中;
{
q.push(i);
vis[i]=true;
}
while(q.size())
{
int t=q.front();
q.pop();
vis[t]=false;
for(int i=0;i<g[t].size();i++)
{
int j=g[t][i].first;
if(dis[j]>dis[t]+g[t][i].second)
{
dis[j]=dis[t]+g[t][i].second;
cnt[j]=cnt[t]+1;
if(cnt[j]>=n) return true; //代表最短路径有n+1条边,肯定出现环;
if(!vis[j])
{
q.push(j);
vis[j]=true;
}
}
}
}
return false;
}
int main()
{
cin>>n>>m;
g.resize(n+1);
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
g[a].push_back({b,c});
}
if(spfa()) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
return 0;
}