目录
最短路基础算法
dijkstra 邻接矩阵版
初始化
for n-1次
int t=-1;
for 找出未标记最小点
for 松弛边
st标记
剪枝
void djs()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=0;i<n-1;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
if(!st[j]&&(t==-1||dist[t]>dist[j])) t=j;
for(int j=1;j<=n;j++)
dist[j]=min(dist[j],dist[t]+g[t][j]);
st[t]=1;
if(st[n]) return;
}
}
dijkstra 邻接表版
初始化
while 不空
弹出堆顶(top pop)
判断标记st
for 松弛边并入堆
剪枝
void djs()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
priority_queue<PII,vector<PII>,greater<PII>> heap;
heap.push({0,1});
while(heap.size())
{
int t=heap.top().y;
heap.pop();
if(st[t]) continue;
st[t]=1;
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
if(dist[j]>dist[t]+w[i])
{
dist[j]=dist[t]+w[i];
heap.push({dist[j],j});
}
}
if(st[n]) return;
}
}
spfa
初始化 循环队列
while 不空
弹出元素
标记不在队列中
for 边
更新dist 并将没在队列中点加入队列
void spfa()
{
memset(dist,0x3f,sizeof dist);
int hh=0,tt=0;
dist[1]=0;
st[1]=0;
q[tt++]=1;
while(hh!=tt)
{
int t=q[hh++];
if(hh==N) hh=0;
st[t]=false;
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
if(dist[j]>dist[t]+w[i])
{
dist[j]=dist[t]+w[i];
if(!st[j])
{
q[tt++]=j;
if(tt==N) tt=0;
st[j]=true;
}
}
}
}
}
bellman_ford
一般不用
特殊用法 可以求出一个点到其它点最多经过k条边的最短路
void bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i ++ )
{
memcpy(last, dist, sizeof dist);
for (int j = 0; j < m; j ++ )
{
auto e = edges[j];
dist[e.b] = min(dist[e.b], last[e.a] + e.c);
}
}
}
floyd
经过前k个点i到j的最短距离
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(e[i][j]>e[i][k]+e[k][j]) e[i][j]=e[i][k]+e[k][j];
单源最短路
拓展变形
最短路+dfs 例如当要求必须经过某几个点又不要求经过点的顺序
则可以用dfs枚举出经过这几个点顺序 在进行求解
最短路+二分 可以控制条件改变边的状态进行二分的check()
最短路+拓扑排序 可以先将图缩点为拓扑图 在在缩点内做最短路。
按拓扑序扫描点时当 扫描到一个点时该点不会在被更新
最短路+dp 当一个图中依赖关系有环时 则可以考虑使用最短路算法求解。
需要注意的是 能否用 djs 要看是否每个点出队时是否还会被更新。spfa一般没限制
拆点 当一个点的状态不仅有坐标信息时可以增加状态信息 类比 dp 进行状态转移建边
最短路方案数 可以利用dp的方式记录 需要注意的是 djs 点出堆顺序满足拓扑序 但spfa不满足
次短路
typedef long long LL;
const int N = 100010, M = 300010, INF = 0x3f3f3f3f;
int n, m;
struct Edge
{
int a, b, w;
bool used;
bool operator< (const Edge &t) const
{
return w < t.w;
}
}edge[M];
int p[N];
int h[N], e[M], w[M], ne[M], idx;
int depth[N], fa[N][17], d1[N][17], d2[N][17];
int q[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
LL kruskal()
{
for (int i = 1; i <= n; i ++ ) p[i] = i;
sort(edge, edge + m);
LL res = 0;
for (int i = 0; i < m; i ++ )
{
int a = find(edge[i].a), b = find(edge[i].b), w = edge[i].w;
if (a != b)
{
p[a] = b;
res += w;
edge[i].used = true;
}
}
return res;
}
void build()
{
memset(h, -1, sizeof h);
for (int i = 0; i < m; i ++ )
if (edge[i].used)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
add(a, b, w), add(b, a, w);
}
}
void bfs()
{
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[1] = 1;
q[0] = 1;
int hh = 0, tt = 0;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (depth[j] > depth[t] + 1)
{
depth[j] = depth[t] + 1;
q[ ++ tt] = j;
fa[j][0] = t;
d1[j][0] = w[i], d2[j][0] = -INF;
for (int k = 1; k <= 16; k ++ )
{
int anc = fa[j][k - 1];
fa[j][k] = fa[anc][k - 1];
int distance[4] = {d1[j][k - 1], d2[j][k - 1], d1[anc][k - 1], d2[anc][k - 1]};
d1[j][k] = d2[j][k] = -INF;
for (int u = 0; u < 4; u ++ )
{
int d = distance[u];
if (d > d1[j][k]) d2[j][k] = d1[j][k], d1[j][k] = d;
else if (d != d1[j][k] && d > d2[j][k]) d2[j][k] = d;
}
}
}
}
}
}
int lca(int a, int b, int w)
{
static int distance[N * 2];
int cnt = 0;
if (depth[a] < depth[b]) swap(a, b);
for (int k = 16; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
a = fa[a][k];
}
if (a != b)
{
for (int k = 16; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
distance[cnt ++ ] = d1[a][k];
distance[cnt ++ ] = d2[a][k];
distance[cnt ++ ] = d1[b][k];
distance[cnt ++ ] = d2[b][k];
a = fa[a][k], b = fa[b][k];
}
distance[cnt ++ ] = d1[a][0];
distance[cnt ++ ] = d1[b][0];
}
int dist1 = -INF, dist2 = -INF;
for (int i = 0; i < cnt; i ++ )
{
int d = distance[i];
if (d > dist1) dist2 = dist1, dist1 = d;
else if (d != dist1 && d > dist2) dist2 = d;
}
if (w > dist1) return w - dist1;
if (w > dist2) return w - dist2;
return INF;
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edge[i] = {a, b, c};
}
LL sum = kruskal();
build();
bfs();
LL res = 1e18;
for (int i = 0; i < m; i ++ )
if (!edge[i].used)
{
int a = edge[i].a, b = edge[i].b, w = edge[i].w;
res = min(res, sum + lca(a, b, w));
}
printf("%lld\n", res);
return 0;
}
Floyd算法扩展
传递闭包
求传递闭包就是可以 求出所有点的传递关系
例如 a>b b>c 则a>c
可以利用 floyd算法求解 传递闭包 但g[i][i]=1时一般代表矛盾
memset(g, 0, sizeof g);
g[i][j]=1 //将有关系的点名为一
for(int i=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(g[i][k]&g[k][j]) g[i][j]=1;
}
找最小环
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 109,INF=0x3f3f3f3f;
int n,m;
int g[N][N],d[N][N];
int path[N];
int cnt;
int pos[N][N];
void getp(int x,int y)
{
if(pos[x][y]==0) return ;
int k=pos[x][y];
getp(x,k);
path[cnt++]=k;
getp(k,y);
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g);
int a,b,c;
for(int i=0;i<m;i++)
{
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c);
g[b][a]=c;
}
memcpy(d,g,sizeof g);
int ans=INF;
for(int k=1;k<=n;k++)
{
for(int i=1;i<k;i++)
{
for(int j=i+1;j<k;j++)
{
if((LL)d[i][j]+g[i][k]+g[k][j]<ans)
{
ans=d[i][j]+g[i][k]+g[k][j];
cnt=0;
path[cnt++]=i;
getp(i,j);
path[cnt++]=j;
path[cnt++]=k;
}
}
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
if(d[i][k]+d[k][j]<d[i][j])
{
d[i][j]=d[i][k]+d[k][j];
pos[i][j] = k;
}
}
}
}
if(ans==INF) cout<<"No solution.";
else
{
for(int i=0;i<cnt;i++) cout<<path[i]<<' ';
}
return 0;
}
恰好经过k条边最短路
利用倍增的思想和flody结合进行求解
#include<bits/stdc++.h>
using namespace std;
const int N=1009,M=109;
int idx;
unordered_map<int,int> mmap;
int n,T,s,e;
int g[N][N],t[N][N];
int getx(int x)
{
if(mmap.count(x)) return mmap[x];
else
{
mmap[x]=++idx;
return idx;
}
}
void mul(int c[][N],int a[][N],int b[][N])
{
memset(t,0x3f,sizeof t);
for(int k=1;k<=idx;k++)
for(int i=1;i<=idx;i++)
for(int j=1;j<=idx;j++)
if(a[i][k]+b[k][j]<t[i][j]) t[i][j]=a[i][k]+b[k][j];
memcpy(c,t,sizeof t);
}
int quick(int k)
{
int ret[N][N];
memset(ret,0x3f,sizeof ret);
for(int i=0;i<=idx;i++) ret[i][i]=0;
while (k)
{
if (k & 1) mul(ret,ret,g);
mul(g,g,g);
k >>= 1;
}
return ret[getx(s)][getx(e)];
}
int main()
{
cin>>n>>T>>s>>e;
memset(g,0x3f,sizeof g);
int a,b,c;
for(int i=0;i<T;i++)
{
cin>>c>>a>>b;
a=getx(a);
b=getx(b);
g[a][b]=min(g[a][b],c);
g[b][a]=g[a][b];
}
cout<<quick(n)<<endl;;
return 0;
}
最小生成树
在解题时 如果越到要求有必选边 则提前合并即可。并且在克鲁斯卡尔算法中
会优先使用短边,即用最少边和连接最多点。
求一棵树的完全图且改图的最小生成树为原图 可以根据克鲁斯卡尔算法的
合并顺序进行求解即可
int prim()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
int ret=0;
for(int i=0;i<n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
{
if(!st[j]&&(t==-1||dist[t]>dist[j])) t=j;
}
if(dist[t]==INF) return INF;
st[t]=true;
ret+=dist[t];
for(int j=1;j<=n;j++)
{
dist[j]=min(dist[j],g[t][j]);
}
}
return ret;
}
//并查集
int find(int x)
{
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int kruskal()
{
sort(edges,edges+m);
for(int i=1;i<=n;i++) p[i]=i;
int ret=0,cnt=0;
for(int i=0;i<m;i++)
{
int a=edges[i].a,b=edges[i].b,w=edges[i].w;
a=find(a),b=find(b);
if(a!=b)
{
p[a]=b;
ret+=w;
cnt++;
}
}
if(cnt<n-1) return INF;
return ret;
}
spfa求负环
在求解负环的题中 可以尝试将spfa中的循环队列该为站,一般情况下可以加速求解速度
但是在求解最短路的题中切勿使用
int h[N], e[M], w[M], ne[M], idx;
int q[N], dist[N],cnt[N];
bool st[N];
int n,m,W;
void add(int a, int b, int c) // 添加一条边a->b,边权为c
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool spfa() // 求1号点到n号点的最短路距离
{
int hh = 0, tt = -1;
memset(dist, 0, sizeof dist);
memset(cnt,0,sizeof cnt);
memset(st, 0, sizeof st);
for(int i=1;i<=n;i++)
{
q[++tt] = i;
st[i] = true;
}
while (hh<=tt)
{
int t = q[tt--];
// if (hh == N) hh = 0;
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];
cnt[j]=cnt[t]+1;
if(cnt[j]>=n) return true;
if (!st[j])
{
q[++tt] = j;
//if (tt == N) tt = 0;
st[j] = true;
}
}
}
}
return false;
}
差分约束
应用:解不等式组
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1e5+9,M=3e5+9;
int h[N],e[M],w[M],ne[M],idx;
int n,k;
int q[N];
int dist[N];
bool st[N];
int cnt[N];
void add(int a,int b,int c)
{
e[idx]=b,w[idx]=c,ne[idx]=h[a],h[a]=idx++;
}
LL spfa()
{
memset(dist,-0x3f,sizeof dist);
dist[0]=0;
int hh=0,tt=0;
q[tt++]=0;
st[0]=true;
while(hh!=tt)
{
int t=q[--tt];
// if(hh==N) hh=0;
st[t]=false;
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
if(dist[j]<dist[t]+w[i])
{
dist[j]=dist[t]+w[i];
cnt[j]=cnt[t]+1;
if(cnt[j]>n) return -1;
if(!st[j])
{
q[tt++]=j;
// if(tt==N) tt=0;
st[j]=true;
}
}
}
}
LL ret=0;
for(int i=1;i<=n;i++)
{
ret+=dist[i];
}
return ret;
}
int main()
{
// cin>>n>>k;
scanf("%d%d",&n,&k);
int a,b,c;
memset(h,-1,sizeof h);
//构建不同边
for(int i=0;i<k;i++)
{
// cin>>c>>a>>b;
scanf("%d%d%d",&c,&a,&b);
if(c==1)
{
add(a,b,0),add(b,a,0);
}else if(c==2)
{
add(a,b,1);
}else if(c==3)
{
add(b,a,0);
}else if(c==4)
{
add(b,a,1);
}else if(c==5)
{
add(a,b,0);
}
}
//构建绝对值边
for(int i=1;i<=n;i++)
{
add(0,i,1);
}
cout<<spfa()<<endl;
return 0;
}
最近公共祖先(LCA)
void bfs(int root) // 预处理倍增数组
{
memset(depth, 0x3f, sizeof depth);
depth[0] = 0, depth[root] = 1; // depth存储节点所在层数 0为无效哨兵
int hh = 0, tt = 0;
q[tt++] = root;
while (hh != tt)
{
int t = q[hh ++ ];
for (int i = h[t]; ~i; i = ne[i])
{
int j = e[i];
if (depth[j] > depth[t] + 1)
{
depth[j] = depth[t] + 1;
q[tt++] = j;
fa[j][0] = t; // j的第二次幂个父节点
for (int k = 1; k <= 15; k ++ )
fa[j][k] = fa[fa[j][k - 1]][k - 1];
}
}
}
}
int lca(int a, int b) // 返回a和b的最近公共祖先
{
if (depth[a] < depth[b]) swap(a, b);
for (int k = 15; k >= 0; k -- )
if (depth[fa[a][k]] >= depth[b])
a = fa[a][k];
if (a == b) return a;
for (int k = 15; k >= 0; k -- )
if (fa[a][k] != fa[b][k])
{
a = fa[a][k];
b = fa[b][k];
}
return fa[a][0];
}
int root = 0;
memset(h, -1, sizeof h);
for (int i = 0; i < n; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
if (b == -1) root = a;
else add(a, b), add(b, a);
}
bfs(root);
//离线 线性做法
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
typedef pair<int, int> PII;
const int N = 10010, M = N * 2;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int dist[N];
int p[N];
int res[M];
int st[N];
vector<PII> query[N]; // first存查询的另外一个点,second存查询编号
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u, int fa)
{
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (j == fa) continue;
dist[j] = dist[u] + w[i];
dfs(j, u);
}
}
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
void tarjan(int u)
{
st[u] = 1;
for (int i = h[u]; ~i; i = ne[i])
{
int j = e[i];
if (!st[j])
{
tarjan(j);
p[j] = u;
}
}
for (auto item : query[u])
{
int y = item.first, id = item.second;
if (st[y] == 2)
{
int anc = find(y);
res[id] = dist[u] + dist[y] - dist[anc] * 2;
}
}
st[u] = 2;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for (int i = 0; i < n - 1; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
for (int i = 0; i < m; i ++ )
{
int a, b;
scanf("%d%d", &a, &b);
if (a != b)
{
query[a].push_back({b, i});
query[b].push_back({a, i});
}
}
for (int i = 1; i <= n; i ++ ) p[i] = i;
dfs(1, -1);
tarjan(1);
for (int i = 0; i < m; i ++ ) printf("%d\n", res[i]);
return 0;
}
有向图的强连通分量(SCC)
还可以 求最大半连通子图 先缩点在dp求最长路即可
求最长路/差分约束(边权非负)先缩点在建图,建图中看缩点内是否边权均为0如果不满足代表无界 ,最后按拓扑序dp最长路即可
1. 加时间戳;
2. 放入栈中,做好标记;
3. 遍历邻点
1)如果没遍历过,tarjan一遍,用low[j]更新最小值low
2) 如果在栈中,用dfn[j]更新最小值low
4.找到最高点
1)scc个数++
2)do-while循环:
从栈中取出每个元素;标志为出栈;
对元素做好属于哪个scc;该scc中点的数量++
int dfn[N], low[N], timestamp;
int stk[N], top;
bool in_stk[N];
int id[N], scc_cnt, scc_size[N];
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
stk[ ++ top] = u, in_stk[u] = true;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!dfn[j])
{
tarjan(j);
low[u] = min(low[u], low[j]);
}
else if (in_stk[j]) low[u] = min(low[u], dfn[j]);
}
if (dfn[u] == low[u])
{
++ scc_cnt;
int y;
do {
y = stk[top -- ];
in_stk[y] = false;
id[y] = scc_cnt;
scc_size[scc_cnt] ++ ;
} while (y != u);
}
}
建图缩点
for 点
if(!dfn[]) tarjan();
拓扑序为逆序
for(int i=scc_cnt;i>0;i--)
无向图的边连通分量(e-DCC)
无向图的点连通分量(v-DCC)
二分图
二分图,即可以将一个 无向图 划分为两个集合,是的集合的点没有边相连
应用:
1、棋盘二分图,给棋盘最多放多少1*2骨牌 日子行
//二分图判定
int color[N];
bool dfs(int u,int c)
{
color[u]=c;
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(!color[j])
{
if(!dfs(j,3-c)) return 0;
}else if(color[j]==c) return 0;
}
return 1;
}
bool flag=1;
for(int i=1;i<=n;i++)
{
if(!color[i])
{
if(!dfs(i,1))
{
flag=false;
break;
}
}
}
if(flag) cout<<"Yes";
else cout<<"No";
//二分图的最大匹配
int match[N];
bool st[N];
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
int ret=0;
for(int i=1;i<=n1;i++)
{
memset(st,false,sizeof st);
if(find(i)) ret++;
}
欧拉路径/回路
应用:一笔画问题
普通版本
时间复杂度:最坏情况下
o
(
m
2
)
o(m^2)
o(m2)
void dfs(int u)
{
for(int i=h[u];~i;i=ne[i])
{
if(st[i]) continue;
st[i]=true;
if(t==1) st[i^1]=true;//无向图时需将方向标也标记
dfs(e[i]);
ans[cnt++]=?? //记录答案 注意第一个点的初始化
}
}
//输出路径
for(int i=cnt-1;i>=0;i--) cout<<ans[i]<<' ';
环路优化
时间复杂度:
O
(
m
)
O(m)
O(m)
void dfs(int u)
{
for(int i=h[u];~i;i=h[u])
{
if(st[i])
{
h[u]=ne[i];
continue;
}
h[u]=ne[i];
st[i]=true;
if(t==1) st[i^1]=true;//无向图时需将方向标也标记
dfs(e[i]);
ans[cnt++]=?? //记录答案
}
}
//输出路径
for(int i=cnt-1;i>=0;i--) cout<<ans[i]<<' ';
拓扑排序
满足拓扑排序的充要条件是改图为有向无环图(DAG)。
应用:
1、可以与dp结合,用拓扑序进行dp求解
2、最长路(有限制,具体看最长路几种求解)
时间复杂度: O ( n + m ) O(n+m) O(n+m)
int n;
int q[N],d[N];
int h[N], e[M], ne[M], idx;
void topsort()
{
int hh = 0, tt = -1;
// d[i] 存储点i的入度
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[ ++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++ ];
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (-- d[j] == 0)
q[ ++ tt] = j;
}
}
}
//输出拓扑序
for(int i=0;i<n;i++) cout<<q[i]<<' ';
其它tips
最长路
最短路算法不仅可以求最短路 也可以求最长路
只需要更改 判断条件即可 dist[j]=max(dist[j],dist[[t]+w);
虚拟源点
可以在图中建立一些虚拟的点解决一些 多起点问题。
路径权重
最短路算法不仅可以处理路径的加减 还可以应用到乘除
当权值 >=1 时 可以用 djs 当权值 >0 只能用 spfa
初始化一般为 1 不为 0
建图
建图时可以考虑 边点交换
单词建图时可以考虑将每个字母作为一个点(作为边)
虚拟源点
起点和终点是否可以交换
注意重边是否影响结果
邻接矩阵是否需要初始化自己到自己为0