图论小结(超详细)
最短路
单源最短路
权值皆为正数
朴素Dijstra
以下是我对单源最短路径有了的认知,迪杰斯特拉算法就是我们常用的最短路算法,该算法本质是在贪心的基础上进行的图论延申。
单源最短路径即为从一个点到其他点的最短路。该算法主要通过以下几步来完成。
1:初始化最短路数组,dis[1]=0,dis[i]=+1000000000;
2:循环n次,每次找出所有点中距离起点最近的那个点,将该点标记后,用该点更新其他的点的最短路距离。
以下即为迪杰斯特拉算法的朴素算法代码模板。
#include <iostream>
#include <algorithm>
#include<vector>
#include<map>
#include<string>
#include<cmath>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<set>
#include<unordered_map>
#include<queue>
#include<climits>
#include<stack>
using namespace std;
const int maxn= 510;
const int INF=INT_MAX;
int g[maxn][maxn];
int d[maxn];
int n,m;
bool vis[maxn]= {false};
int dijkstra(int s)
{
memset(d, 0x3f,sizeof d);
d[s]=0;
for(int i=0; i<n; i++)
{
int u=-1;
for(int j=1; j<=n; j++)
if(vis[j]==false &&(u==-1|| d[j]<d[u]))
u=j;
vis[u]=true;
for(int v=1; v<=n; v++)
d[v]=min(d[v],g[u][v]+d[u]);
}
if(d[n]!=0x3f3f3f3f)
return d[n];
else return -1;
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g);
for(int i=0; i<m; i++)
{
int x,y,z;
cin>>x>>y>>z;
g[x][y]=min(g[x][y],z);
}
printf("%d\n",dijkstra(1));
return 0;
}
//用该算法最大可以解决100*100的路径问题;若是数据量增大到1e5则需要使用数据结构进行优化。
堆优化版Dijstra
#include<stdio.h>
#include<iostream>
#include<vector>
#include<cstring>
#include<queue>
using namespace std;
const int N=1e6+6;
#define PII pair<int ,int>
int n, m;
int h[N], e[N], ne[N], w[N], idx;
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
int Dijkstra()
{
memset(dist, 0x3f,sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII> > heap;
//先把1号点加入队列
heap.push({0, 1});
while(heap.size())
{
//直接取出对头min
auto t = heap.top();
heap.pop();
int ver = t.second, dt = t.first;
//如果已经是确定了最短路的点就跳过
if(st[ver])
continue;
st[ver] = 1;
//开始更新以对头t为起点的边
for(int i = h[ver]; i!=-1; i = ne[i])
{
int j = e[i];
if(dist[j] > dist[ver] + w[i])
{
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f)
return -1;
return dist[n];
}
int main()
{
cin >> n >> m;
memset(h,-1,sizeof h);
while(m --)
{
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
cout << Dijkstra() << endl;
return 0;
}
#include<iostream>
#include<vector>
#include<queue>
using namespace std;
const int MAX_V=10005;
const int INF=0x3f3f3f;
struct edge
{
int to;//边的终点
int cost;//权值
edge(int t,int c)
{
to=t;
cost=c;
}
};
typedef pair<int,int> P;//first是该点入队时的最短距离,second是顶点编号
int V;//顶点数
int E;//边数
vector<edge> G[MAX_V];
int d[MAX_V];
void dijkstra(int s)
{
priority_queue<P,vector<P>,greater<P> > que;//优先队列里存的都是最短距离已经确认的顶点
fill(d,d+V+1,INF);
d[s]=0;
que.push(P(0,s));//起点,起点到起点的最短距离是确定的(0)
while(!que.empty())
{
P p=que.top();
que.pop();
int v=p.second;
if(d[v]<p.first)
continue;//表示该点入队不只一次(即之前有多个点都可以到达它),那么d[v]也可能更新了不只一次,
for(int i=0; i<G[v].size(); i++) //扫描其所有相邻的顶点,并更新他们的最短距离d[i]
{
edge e=G[v][i];
if(d[e.to]>d[v]+e.cost)
{
d[e.to]=d[v]+e.cost;//d[v]是已经确定的S
que.push(P(d[e.to],e.to));//更新后最短距离已经确认的点入队,优先队列里自动维护,队头是最小的那个
}
}
}
}
int main()
{
int start,to,cost;
scanf("%d%d",&V,&E);
for(int i=0; i<E; i++)
{
scanf("%d%d%d",&start,&to,&cost);
G[start].push_back(edge(to,cost));
}
dijkstra(1);
for(int i=1; i<=V; i++)
printf("%d ",d[i]);
return 0;
}
存在负权值边
Bellman-Ford
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int dist[N],backup[N];
int k,n,m;
struct edge
{
int a;
int b;
int w;
} edge[N];
int bellman_ford()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=1; i<=k; i++)
{
memcpy(backup,dist,sizeof dist);//将dist内的数据存储到backup
for(int j=1; j<=m; j++)
{
int a=edge[j].a,b=edge[j].b,w=edge[j].w;
dist[b]=min(dist[b],backup[a]+w);
}
}
return dist[n];
}
int main()
{
cin>>n>>m>>k;
for(int i=1; i<=m; i++)
{
int a,b,c;
cin>>a>>b>>c;
edge[i].a=a,edge[i].b=b,edge[i].w=c;
}
int t=bellman_ford();
if(t>=0x3f3f3f3f/2)puts("impossible");
else cout<<t<<endl;
}
SPFA
#include <iostream>
#include <string.h>
#include <queue>
using namespace std;
const int N=100010;
int n,m;
int e[N],ne[N],w[N],h[N],idx;
int dist[N];
bool st[N];
void add(int a,int b,int c){
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
int spfa()
{
queue<int> q;
memset(dist,0x3f,sizeof dist);
dist[1]=0;
q.push(1);
st[1]=true;
while(q.size()){
int t=q.front();
q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(dist[j]>dist[t]+w[i]){
dist[j]=dist[t]+w[i];
if(!st[j]){
q.push(j);
st[j]=true;
}
}
}
}
return dist[n];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--){
int a,b,w;
cin>>a>>b>>w;
add(a,b,w);
}
if(spfa()>0x3f3f3f3f/2) cout<<"impossible";
else cout<<dist[n];
return 0;
}
多源最短路
Floyd
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef pair<int, int> PII;
const int inf = 0x3f3f3f3f;
const int N = 55, M = 1600;
PII p[M]; // 保存边插入的顺序
int n, m, k, T, f[N][N], init[N][N];
void floyed()
{
for (int v = 1; v <= n; ++v)
{
f[v][v] = 0;
for (int i = 1; i <= n; ++i)
{
for (int j = 1; j <= n; ++j) f[i][j] = min(f[i][j], f[i][v] + f[v][j]);
}
}
}
int main()
{
cin >> T;
while (T--)
{
memset(init, inf, sizeof init);
cin >> n >> m >> k;
for (int i = 1; i <= m; ++i)
{
int a, b, c;
cin >> a >> b >> c;
p[i] = {a, b};
init[a][b] = c;
init[b][a] = c;
}
memcpy(f, init, sizeof f); // 回到初始状态
floyed();
printf("%d\n", f[1][n]);
memcpy(f, init, sizeof f);
for (int i = 1; i <= k; ++i)
{
// 删除第x次插入得边,注意从a->b, b->a都要删除,删除就是更新为inf
int x;
cin >> x;
f[p[x].first][p[x].second] = inf;
f[p[x].second][p[x].first] = inf;
}
floyed(); // 再次佛洛依德[添加链接描述](https://editor.csdn.net/md/?articleId=121609566)
if (f[1][n] > inf / 2) puts("-1");
else printf("%d\n", f[1][n]);
}
return 0;
}
最小生成树
Kruskal
此方法可以称为加边法。即将边的权值大小排好序之后按照从小到大的顺序依次将n-1条边加入到生成树中
Prim
对应于上述的加边法,这个算法可以称之为加点法。 每次迭代选择代价最小的边对应的点,加入到最小生成树中。算法从某一个顶点s开始,逐渐长大覆盖整个连通网的所有顶点。先遍历一遍当前点的所有边,找出最小边,将该边的另一个顶点加入到生成树上,并继续遍历该点的所有边,每次将边的权值放入辅助数组中,最后在数组中求出最小便=边的顶点,重复上述步骤,直到最小生成树有n-1条边或者n个顶点为止。
点此连接直达最小生成树板块(prim&&kruskal)
最近公共祖先(LCA)
朴素算法
1,首先将数据存储在邻接表里,先将数据按照并查集存储,然后将叶子节点的深度全部神深搜出来,存储到深度数组中。
2,然后就是具体做法:
先判断两个数是否在一棵树的同一层上,若不是先调整到同一层上。然后将两个数据在并查集内,同时向上搜寻,直至fath[a]==fath[b];
倍增法(Tarjan)
这个算法应该是最容易想到的,因为它本质上属于暴力算法的优化版本,只是把暴力算法的一次只跳一步变为了一次跳 2^k 步。
拓扑排序
对一个有向无环图 ( Directed Acyclic Graph 简称 DAG ) G 进行拓扑排序,是将 G
中所有顶点排成一个线性序列,使得图中任意一对顶点 u 和 v ,若边 < u , v > ∈ E ( G ),则 u 在线性序列中出现在 v之前。通常,这样的线性序列称为满足拓扑次序 ( Topological Order )
的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。(若图中存在环路则不可能排出一个线性次序)
总结起来有三个要点:
1.有向无环图;
2.序列里的每一个点只能出现一次;
3.任何一对 u 和 v ,u 总在 v 之前(这里的两个字母分别表示的是一条线段的两个端点,u 表示起点,v 表示终点)
许多实际应用都需要有向无环图来指明事件的优先次序,例如可以应用来求解食物链总条数。