最短路
图论最基础的想必就是最短路啦
最短路能解决的问题:
最短路?
话是这么说没错啦,不过一些题目隐藏的比较深,需要转化一下才能看出最短路的模型:经典例题差分约束系统
对于差分约束我要说两句了
简述
给出若干形如
xi<=xj+w
x
i
<=
x
j
+
w
的限制条件,建图连边
j−(w)−>i
j
−
(
w
)
−
>
i
上面的三角不等式符合最短路的形式,所以我们用bellman跑一遍最短路,得到每个点的
dis
d
i
s
即为一组可行解(如果存在负环则说明原题无解)
细节一
前辈表示:最短路得到的
dis
d
i
s
是最大可行解
这是怎么回事?
观察一下不等式:
xi<=xj+w
x
i
<=
x
j
+
w
我们如果选择这条边松弛(说明这条边的限制最严格),那么
xi=xj+w
x
i
=
x
j
+
w
然而存在情况:
xi<xj+w
x
i
<
x
j
+
w
,
所以我们最短路得到的答案是符合限制条件下的上限
同理,如果我们遇到这样的限制条件:
xi>=xj+w
x
i
>=
x
j
+
w
连边
j−(w)−>i
j
−
(
w
)
−
>
i
,跑最长路
我们像上文一样分析一下,就会发现:最长路得到最小可行解
所以我们需要根据题设选择把约束条件化为最短路还是最长路
细节二
在细节一中,我们提到了求解可行解
注意初始化(very very important)
最长路:
−INF
−
I
N
F
最短路:
INF
I
N
F
判断负环:
0
0
我们在跑Bellman的时候,多半需要建立一个虚拟节点,连向所有结点
而虚拟结点就按照前文的方式初始化
经典例题:bfs(倒水问题)
经典例题:dfs(欧拉路径输出)
经典例题:dijkstra(逆向思维)
经典例题:线段树优化建图+分层图最短路
经典例题:dijkstra+floyed+dp(Mario)
经典例题:Bellman+二分(平均权值最小的回路)
经典例题:差分约束+二分(加减边权,使边权最小值非负且尽量大)
经典例题:差分约束
经典例题:差分约束(看似两种未知量+乘除变加减)
经典例题:差分约束+tarjan+floyed
下面就是堆优dijkstra和Bellman的代码啦
dijkstra有一个致命的缺陷:不能对付有负边的图
const int N=10010;
int n,m,st[N],tot=0,dis[N];
bool vis[N];
struct node{
int y,v,nxt;
}way[N<<1];
struct heapnode{
int u,d;
heapnode(int uu=0,int dd=0) {
u=uu; d=dd;
}
bool operator <(const heapnode &a) const {
return d>a.d;
}
};
void Dijkstra(int s,int t) {
priority_queue<heapnode> q;
memset(dis,0x33,sizeof(dis));
dis[s]=0;
q.push(heapnode(s,0));
while (!q.empty()) {
heapnode now=q.top(); q.pop();
int u=now.u,d=now.d;
if (vis[u]) continue;
vis[u]=1;
for (int i=st[u];i;i=way[i].nxt)
if (dis[way[i].y]>d+way[i].v) {
dis[way[i].y]=d+way[i].v;
q.push(heapnode(way[i].y,dis[way[i].y]));
//每次更新都扔进堆里
}
}
}
Bellman
int cnt[N],dis[N];
bool in[N];
int Bellman() {
queue<int> q;
for (int i=1;i<=n;i++) { //虚拟源点
dis[i]=0; //判断负环
q.push(i);
in[i]=1; cnt[i]=0;
}
while (!q.empty()) {
int now=q.front(); q.pop();
in[now]=0;
for (int i=st[now];i;i=way[i].nxt)
if (dis[way[i].y]>dis[now]+way[i].v) {
dis[way[i].y]=dis[now]+way[i].v;
if (!in[way[i].y]) {
in[way[i].y]=1;
if (++cnt[way[i].y]>n) return 0; //存在负环
q.push(way[i].y);
}
}
}
return 1;
}
最短路延伸:次短路
次短路有两种解法:
跑一边最短路,将最短路中的边扔到一个集合中
每次删除集合中的一条边(边权设为INF),再次跑一边最短路,得到大于的最短路的最短路径即为次短路对于每一个结点记录,本别表示最短路和次短路
每次松弛的时候,分别转移一下两者即可(此种方法可以进行最短路和次短路计数)给出解法二的代码
int dis[N][2],cnt[N][2]; bool vis[N][2]; void solve(int s,int t) { for (int i=1;i<=n;i++) { dis[i][0]=dis[i][1]=INF; vis[i][0]=vis[i][1]=0; cnt[i][0]=cnt[i][1]=0; } dis[s][0]=0; //最短路 cnt[s][0]=1; for (int T=1;T<2*n;T++) //n^2的dijksta 当然也可以用nlogn的方法 { int p=0,q=0,mn=INF; for (int i=1;i<=n;i++) if (!vis[i][0]&&dis[i][0]<mn) { mn=dis[i][0]; p=i; q=0; } else if (!vis[i][1]&&dis[i][1]<mn) { mn=dis[i][1]; p=i; q=1; } if (mn==INF) break; vis[p][q]=0; for (int i=st[p];i;i=way[i].nxt) { int w=dis[p][q]+way[i].v; int y=way[i].y; if (w<dis[y][0]) { dis[y][1]=dis[y][0]; //不要忘了转移次短路 cnt[y][1]=cnt[y][0]; dis[y][0]=w; cnt[y][0]=cnt[p][q]; } else if (w==dis[y][0]) { cnt[y][0]+=cnt[p][q]; } else if (w<dis[y][1]) { dis[y][1]=w; cnt[y][1]=cnt[p][q]; } else if (w==dis[y][0]) { cnt[y][1]+=cnt[p][q]; } } } }
割点+桥+双连通分量
连通分量真的不是很会
对于这一部分,应该还是比较重要的
给出相关定义吧(三个概念都是在无向图的基础上):割点
如果将连通图G中的某个点及和这个点相关的边删除后,将使连通分量数量增加,那么这个点就称为图G的割点
【性质一】
如果深度优先搜索树的根节点至少有两个以上的子节点,则根节点是割点。显然去掉根节点后将得到以子节点为根结点的森林
【性质二】
在深度优先搜索树中,v存在一个子节点不能通过后向边到达v的祖先节点,则节点v是割点
也就是说从v的子节点开始没有一条边能够回到v的祖先节点,那么当去掉v时将会使得v的子孙节点与v的祖先节点之间失去联系,必定会使得图不再连通桥
如果将连通图G中的某条边删除后,将使连通分量数量增加,那么这条边就称为图G的割点
双连通分量
对于一个连通图,如果任意两点至少存在两条“点不重复”的路径,则说这个图是点-双连通的(一般简称双连通)
这就相当于任意两条边都在同一个简单环中,即内部无割点
类似的,如果任意两个点至少存在“边不重复”的路径,我们说这个图是边-双连通的
即所有边都不是桥其实这三个东西的求法大同小异:维护low和dfn
注意一下这句话:if (dfn[y]<dfn[now]&&y!=fa)
在求 BCC B C C 时,我们维护一个栈,里面放我们已经走过的边
(因为点双不能重复经过点,实际上就是不能重复经过边)
找到一个割顶后,弹栈,栈中边的两个端点属于一个 BCC B C C割顶+桥
const int N=100010; int low[N],dfn[N],clo=0; bool bridge[N],iscut[N]; void dfs(int now,int fa) { dfn[now]=low[now]=++clo; int ch=0; for (int i=st[now];i;i=way[i].y) { int y=way[i].y; if (!dfn[y]) { ch++; //子结点 dfs(y,now); low[now]=min(low[now],low[y]); if (low[y]>=dfn[now]) //存在即合理 iscut[now]=1; //割点 if (low[y]>dfn[now]) //该边为桥 bridge[i]=1; } else if (dfn[y]<dfn[now]&&y!=fa) { //dfn[y]<dfn[now] low[now]=min(low[now],dfn[y]); } } if (fa<0&&ch==1) iscut[now]=0; //性质一验证 }
BCC
struct node{ int x,y,nxt; }way[N<<1]; int dfn[N],low[N],iscut[N],belong[N],clo,bcc_cnt; vector bcc[N]; stack<node> S; void dfs(int now,int fa) { dfn[now]=low[now]=++clo; int ch=0; for (int i=st[now];i;i=way[i].nxt) { node e=way[i]; int y=way[i].y; if (!dfn[y]) { S.push(e); //边入栈 ch++; dfs(y,now); low[now]=min(low[now],low[y]); if (low[y]>=dfn[now]) { iscut[now]=1; bcc_cnt++; bcc[bcc_cnt].clear(); for (;;) { node x=S.top(); S.pop(); if (belong[x.x]!=bcc_cnt) { bcc[bcc_cnt].push_back(x.x); belong[x.x]=bcc_cnt; } if (belong[x.y]!=bcc_cnt) { bcc[bcc_cnt].push_back(x.y); belong[x.y]=bcc_cnt; } if (x.x==now&&x.y==y) break; } } } else if (dfn[y]<dfn[now]&&y!=fa) { S.push(e); //边入栈 low[now]=min(low[now],dfn[y]); } } if (fa<0&&ch==0) iscut[now]=0; } void find_bcc() { //调用结束后S保证为空 memset(dfn,0,sizeof(dfn)); memset(iscut,0,sizeof(iscut)); memset(belong,0,sizeof(belong)); clo=bcc_cnt=0; for (int i=1;i<=n;i++) //forest if (!dfn[i]) dfs(i,-1); }
强连通分量
强连通分量的应用:2_SAT
经典例题:tarjan+dp
经典例题:tarjan+拓扑+概率期望+gauss注意代码中一定要判断是否在栈内
int S[N],top=0,dfn[N],low[N],clo=0,belong[N],cnt=0; bool in[N]; void tarjan(int now) { dfn[now]=low[now]=++clo; S[++top]=now; in[now]=1; //入栈 for (int i=st[now];i;i=way[i].nxt) if (!dfn[way[i].y]) { tarjan(way[i].y); low[now]=min(low[now],low[way[i].y]); } else if (in[way[i].y]) { low[now]=min(low[now,dfn[way[i].y]); } if (low[now]==dfs[now]) { cnt++; int x=-1; while (x!=now) { x=S[top--]; belong[x]=cnt; in[x]=0; //出栈 } } } void solve() { memset(dfn,0,sizeof(dfn)); for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i); }
拓扑
可以解决有阶梯性的问题(2_SAT解得构造)
拓扑有两种写法:依赖队列,依赖栈
依赖队列就相当于bfs
依赖栈就相当于dfs
都比较好写,视情况选择int S[N],top,ans[N],cnt; void TOP() { top=0; cnt=0; for (int i=1;i<=n;i++) if (!du[i]) S[++top]=i; while (top) { int now=S[top--]; ans[++cnt]=now; for (int i=st[now];i;i=way[i].nxt) { du[way[i].y]--; if (!du[way[i].y]) S[++top]=way[i].y; } } }
生成树
当图变成了一棵数(纠结的生成树)
喜闻乐见最小生成树有两种写法,一般我使用的时Kruskal算法
但是面对稠密图(边数过多),我们需要最小生成树解法二:Prim算法Kruskal
const int N=100010; int fa[N],n,m,dep[N]; struct node{ int x,y,v; }e[N]; int cmp(const node &a,const node &b) { return a.v<b.v; } int find(int x) { //路径压缩 if (fa[x]!=x) fa[x]=find(fa[x]); return fa[x]; } void unionn(int f1,int f2) { //按秩合并 if (dep[f1]<dep[f1]) swap(f1,f2); //f2->f1 fa[f2]=f1; dep[f1]=max(dep[f1],dep[f2]+1); } void Kruskal() { int ans=0; //权值和 for (int i=1;i<=n;i++) fa[i]=i,dep[i]=1; sort(e+1,e+1+m,cmp); int cnt=0; for (int i=1;i<=m;i++) { int x=e[i].x; int y=e[i].y; int f1=find(x); int f2=find(y); if (f1==f2) continue; unionn(f1,f2); cnt++; ans+=e[i].v; if (cnt==n-1) break; } }
Prim
(实质上就是dijkstra)
v v 数组记录与每个点相连的最短边int dis[N][N],v[N]; bool vis[N]; void Prim() { vis[1]=1; v[1]=0; int cur=1,ans=0; for (int i=2;i<=n;i++) vis[i]=0,v[i]=INF; for (int T=1;T<n;T++) { int k=0,mn=INF; for (int i=1;i<=n;i++) if (!vis[i]) { if (v[i]>d[cur][i]) v[i]=d[cur][i]; if (v[i]<mn) mn=v[i],k=i; } vis[k]=1; cur=k; ans+=mn; } }
最小生成树是最小网络的一种,求解联通所有点且不增加其他点的最小网络
这里简单提一下最小生成树的兄弟:斯坦纳树
斯坦纳树求解联通部分点且可以增加其他点的最小网络什么?你想求解图的所有生成树数量?
矩阵树定理讲解
首先构造度数矩阵和邻接矩阵 A A ,得到矩阵
方便起见,我们去掉的第n行和第n列,得到一个新矩阵
用高斯消元(取模情况下要使用辗转相除的高斯消元)得到新矩阵的上三角形
对角线乘积的绝对值就是生成树数量
KM算法
求解完全二分图的最大完美匹配
划重点:完美匹配,最大
如果我们图两部的大小不相等,也不是完全图
那么我们就可以加点加边使其变成完全图const int N=305; int W[N][N],n; int Lx[N],Ly[N],belong[N],slack[N]; bool L[N],R[N]; int match(int i) { L[i]=1; for (int j=1;j<=n;j++) if (!R[j]) { int v=Lx[i]+Ly[j]-W[i][j]; if (!v) { R[j]=1; if (!belong[j]||match(belong[j])) { belong[j]=i; return 1; } } else slack[j]=min(slack[j],v); } return 0; } int KM() { memset(belong,0,sizeof(belong)); //Y部在X部的匹配元素 for (int i=1;i<=n;i++) { //顶标 Ly[i]=0; Lx[i]=W[i][1]; for (int j=2;j<=n;j++) Lx[i]=max(Lx[i],W[i][j]); } for (int i=1;i<=n;i++) { for (int j=1;j<=n;j++) slack[j]=INF; //(德尔塔)顶标 while (1) { memset(L,0,sizeof(L)); memset(R,0,sizeof(R)); if (match(i)) break; int a=INF; for (int j=1;j<=n;j++) if (!R[j]) a=min(a,slack[j]); //寻找新顶标,新顶标只与没有参与匹配的Y点有关 for (int j=1;j<=n;j++) if (L[j]) Lx[j]-=a; for (int j=1;j<=n;j++) if (R[j]) Ly[j]+=a; else slack[j]-=a; } } int ans=0; for (int i=1;i<=n;i++) ans+=W[belong[i]][i]; return ans; }
匈牙利算法
图上的文章再续(二分图)
blog上说的已经很详细了
需要注意的一点:
匈牙利算法(包括ta的特殊情况:KM算法)都已一个一个点以此匹配每次 match m a t c h 之前, L L 和数组都要清零
给出匈牙利算法如何计算最小顶点覆盖
const int N=101; int n,m; struct node{ int y,nxt; }way[N<<1]; int st[N],tot=0,ans[N],cnt=0; int cx[N],cy[N]; bool R[N],L[N]; int match(int x) { L[x]=1; for (int i=st[x];i;i=way[i].nxt) { int y=way[i].y; if (!R[y]) { R[y]=1; if (cy[y]==-1||match(cy[y])) { cy[y]=x; cx[x]=y; return 1; } } } return 0; } void method() { //最小顶点覆盖 memset(L,0,sizeof(L)); memset(R,0,sizeof(R)); for (int i=1;i<=n;i++) if (cx[i]==-1) //寻找X部未匹配点 match(i); for (int i=1;i<=n;i++) if (!L[i]) ans[++cnt]=i; //X部仍未匹配的点 for (int i=1;i<=m;i++) if (R[i]) ans[++cnt]=i+n; //Y部匹配点 } int XYL() { int ans=0; memset(cx,-1,sizeof(cx)); memset(cy,-1,sizeof(cy)); for (int i=1;i<=n;i++) if (cx[i]==-1) { memset(L,0,sizeof(L)); memset(R,0,sizeof(R)); ans+=match(i); } return ans; }
其他
对偶图
平面图的最小割转换成对偶图的最短路
化面为点,规定一个方向,每条边相邻的面连边f[n]=2C(n,2)−∑i=1n−1C(n−1,i−1)f[i]∗2C(n−i,2) f [ n ] = 2 C ( n , 2 ) − ∑ i = 1 n − 1 C ( n − 1 , i − 1 ) f [ i ] ∗ 2 C ( n − i , 2 )f[n]=∑i=1n−1f[i]∗f[n−i]∗C(n−2,i−1)∗(2i−1) f [ n ] = ∑ i = 1 n − 1 f [ i ] ∗ f [ n − i ] ∗ C ( n − 2 , i − 1 ) ∗ ( 2 i − 1 )