模板汇总2.4_图论4
1.拓扑排序
①拓扑排序是什么
拓扑排序经常被用来解决有关于“顺序”的图论问题,其本质是对图的一种操作,可以判断一张有向图中是否有环。拓扑排序会将一张图$G$中所有顶点排成一个线性序列,在这个线性序列中,对于图中任意一条边$(u,v)∈E(G)$,使得$u$在这个线性序列中出现在$v$之前,这个线性序列叫做图的拓扑序列。
拓扑排序是针对有向图的算法。
②拓扑排序的原理
拓扑排序有两种实现方式:BFS和DFS,分别这样实现:
BFS:通过队列实现
①将所有入度为零的点入队
②取出队头的点$tn$,弹掉队头
③遍历并删去由$tn$连出的所有边,所谓删去是在度数意义上的,即是将这些边指向的点的入度减去$1$
④检查这些边指向的点,将其中入度为零的点入队
在算法结束后,检查所有的点的入度,如果全部为零说明图中不存在环
输出序列:每有一个点入队就输出这个点
DFS:通过递归实现
①遍历所有的点,如果一个点未被访问过,则从这个点开始搜索
②进入DFS函数:
(1)判断这个点是否被访问过,如果被访问过,回溯,否则将这个点标记为访问过
(2)遍历由这个点连出的所有边,如果发现连出的这些边所指向的点有已经被访问过的,说明有环,否则依次标记并从这些点继续搜索
(3)将这个点压入一个栈中
输出序列:不断输出栈顶元素并弹栈直到栈空
③拓扑排序的时间复杂度
时间复杂度:$O(n+m)$
④拓扑排序的具体实现
1 //BFS----topsort 2 #include<queue> 3 #include<cstdio> 4 using namespace std; 5 const int N=100005,M=500005; 6 int p[N],noww[M],goal[M],deg[N]; 7 int n,m,t1,t2; 8 bool cir; 9 queue<int> qs; 10 int main () 11 { 12 scanf("%d%d",&n,&m); 13 for(int i=1;i<=m;i++) 14 { 15 scanf("%d%d",&t1,&t2); 16 link(t1,t2),deg[t2]++; 17 } 18 for(int i=1;i<=n;i++) 19 if(!deg[i]) qs.push(i);//output the number 20 while(!qs.empty()) 21 { 22 int tn=qs.front();qs.pop(); 23 for(int i=p[tn];i;i=noww[i]) 24 if(!(--deg[goal[i]])) 25 qs.push(goal[i]);//output the number 26 } 27 for(int i=1;i<=n;i++) 28 if(deg[i]) cir=true; 29 return 0; 30 }
1 //DFS----topsort 2 #include<stack> 3 #include<cstdio> 4 using namespace std; 5 const int N=100005,M=500005; 6 int p[N],noww[M],goal[M]; 7 int n,m,t1,t2; 8 bool vis[N]; 9 bool cir; 10 stack<int> st; 11 void DFS(int nde) 12 { 13 if(vis[nde]) return; 14 vis[nde]=true; 15 for(int i=p[nde];i;i=noww[i]) 16 if(vis[goal[i]]) {cir=true;return ;} 17 else DFS(goal[i]); 18 st.push(nde); 19 } 20 int main () 21 { 22 scanf("%d%d",&n,&m); 23 for(int i=1;i<=m;i++) 24 scanf("%d%d",&t1,&t2),link(t1,t2); 25 for(int i=1;i<=n;i++) 26 if(!vis[i]) DFS(i); 27 //while(the stack is not empty) output the top of the stack,pop the stack 28 return 0; 29 }
2.2-sat
①什么是2-sat
2-sat,即2-satisfiablity,“双可行性”问题。所谓可行性问题(即satisfiablity)是这样的一类问题:给出若干集合,和一些从集合里取元素的规则,要你判断是否有可行解。而前面的“$2$”则表示一个集合里最多有$2$个元素,由此你就可以明白什么叫3-sat,什么叫4-sat等等。不过多说一句,$3-sat$已经被证明是$NPC$问题了。
②2-sat问题的求解原理
将每个变元拆成是/非两个点,对于每个型如若$a$则$b$的命题,由$a$向$b$连一条有向边,同时要对隐含条件:这个命题的逆否命题进行连边。所谓逆否命题就是字面意思,先逆再否;比如下面这个命题:
若 ydnhaha 模拟赛爆零了,则 i207M 就会 AK 模拟赛
其逆否命题为:
若 i207M 没有AK模拟赛,则 ydnhaha 模拟赛没有爆零
这些处理结束后,我们可以通过Tarjan尝试求强联通分量。若每个变元所拆出的两点都不在一个SCC里,则有可行解。可行解可以这样构造:将SCC缩点,对新图做反向的拓扑排序,同时黑白染色。但是更简单的方法是直接比较节点所在SCC编号的大小,我们用$i$表示一个变元为真,$i+n$表示一个变元为假,那么当$col[i]<col[i+n]$时变元$i$为真,否则为假,原理是这样也符合拓扑序(我不知道怎么证)
③求解2-sat问题的时间复杂度
时间复杂度:$O(n+m)$(就是Tarjan的复杂度)
④求解2-sat问题的具体方法
洛谷板子
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=2000005; 6 int dfn[N],low[N],ins[N],stk[N],col[N]; 7 int p[N],noww[2*N],goal[2*N]; 8 int n,m,c,cnt,tot,top,t1,t2,t3,t4; 9 void link(int f,int t) 10 { 11 noww[++cnt]=p[f]; 12 goal[cnt]=t,p[f]=cnt; 13 } 14 void Tarjan_SCC(int nde) 15 { 16 dfn[nde]=low[nde]=++tot; 17 stk[++top]=nde,ins[nde]=true; 18 for(int i=p[nde];i;i=noww[i]) 19 if(!dfn[goal[i]]) 20 Tarjan_SCC(goal[i]),low[nde]=min(low[nde],low[goal[i]]); 21 else if(ins[goal[i]]) 22 low[nde]=min(low[nde],dfn[goal[i]]); 23 if(dfn[nde]==low[nde]) 24 { 25 int tmp; c++; 26 do 27 { 28 tmp=stk[top--]; 29 ins[tmp]=false; 30 col[tmp]=c; 31 }while(tmp!=nde); 32 } 33 } 34 int main () 35 { 36 scanf("%d%d",&n,&m); 37 for(int i=1;i<=m;i++) 38 { 39 scanf("%d%d%d%d",&t1,&t2,&t3,&t4); 40 if(t2&&t4) link(t1+n,t3),link(t3+n,t1); 41 else if(t2) link(t1+n,t3+n),link(t3,t1); 42 else if(t4) link(t1,t3),link(t3+n,t1+n); 43 else link(t1,t3+n),link(t3,t1+n); 44 } 45 for(int i=1;i<=2*n;i++) 46 if(!dfn[i]) Tarjan_SCC(i); 47 for(int i=1;i<=n;i++) 48 if(col[i]==col[i+n]) 49 printf("IMPOSSIBLE"),exit(0); 50 printf("POSSIBLE\n"); 51 for(int i=1;i<=n;i++) 52 printf("%d ",col[i]<col[i+n]); 53 return 0; 54 }
3.LCA
①什么是LCA
LCA即树上两点最近公共祖先
②各种求LCA的方法
倍增求LCA
跳的过程用倍增优化
树剖求LCA
见树链剖分模板
RMQ LCA
搞出来欧拉序之后变成RMQ问题
③时间复杂度
倍增 预处理$O(n\log$ $n)$,单次查询$O(\log n)$
树剖 预处理$O(n)$,单次查询$O(\log n)$
RMQ LCA 预处理$O(n\log n)$,单次查询$O(1)$
④具体实现
倍增
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int N=500005,K=20; 6 int p[N],noww[2*N],goal[2*N]; 7 int dep[N],mul[N][K]; 8 int n,q,r,t1,t2,cnt; 9 void link(int f,int t) 10 { 11 noww[++cnt]=p[f]; 12 goal[cnt]=t,p[f]=cnt; 13 } 14 void DFS(int nde,int fth,int dth) 15 { 16 dep[nde]=dth,mul[nde][0]=fth; 17 for(int i=1;mul[nde][i-1];i++) 18 mul[nde][i]=mul[mul[nde][i-1]][i-1]; 19 for(int i=p[nde];i;i=noww[i]) 20 if(goal[i]!=fth) 21 DFS(goal[i],nde,dth+1); 22 } 23 int LCA(int x,int y) 24 { 25 if(dep[x]<dep[y]) swap(x,y); 26 for(int i=19;dep[x]!=dep[y];i--) 27 if(dep[mul[x][i]]>=dep[y]) x=mul[x][i]; 28 if(x==y) return x; 29 for(int i=19;~i;i--) 30 if(mul[x][i]!=mul[y][i]) 31 x=mul[x][i],y=mul[y][i]; 32 return mul[x][0]; 33 } 34 int main () 35 { 36 scanf("%d%d%d",&n,&q,&r); 37 for(int i=1;i<n;i++) 38 { 39 scanf("%d%d",&t1,&t2); 40 link(t1,t2),link(t2,t1); 41 } 42 DFS(r,0,0),dep[0]=-1; 43 while(q--) 44 { 45 scanf("%d%d",&t1,&t2); 46 printf("%d\n",LCA(t1,t2)); 47 } 48 return 0; 49 }
RMQ
注意欧拉序开两倍点数存
1 #include<cmath> 2 #include<cstdio> 3 #include<cstring> 4 #include<algorithm> 5 using namespace std; 6 const int N=5e5+50,M=1e6+60,K=20; 7 int p[N],noww[M],goal[M]; 8 int dfn[N],idf[N],fir[N],st[M][K]; 9 int n,m,r,t1,t2,cnt,dfo,app; 10 void Link(int f,int t) 11 { 12 noww[++cnt]=p[f]; 13 goal[cnt]=t,p[f]=cnt; 14 noww[++cnt]=p[t]; 15 goal[cnt]=f,p[t]=cnt; 16 } 17 void DFS(int nde,int fth) 18 { 19 idf[dfn[nde]=++dfo]=nde; 20 st[fir[nde]=++app][0]=dfo; 21 for(int i=p[nde];i;i=noww[i]) 22 if(goal[i]!=fth) 23 DFS(goal[i],nde),st[++app][0]=dfn[nde]; 24 } 25 int LCA(int x,int y) 26 { 27 x=fir[x],y=fir[y]; 28 if(x>y) swap(x,y); 29 int l2=log2(y-x+1); 30 return idf[min(st[x][l2],st[y-(1<<l2)+1][l2])]; 31 } 32 int main () 33 { 34 scanf("%d%d%d",&n,&m,&r); 35 for(int i=1;i<n;i++) 36 scanf("%d%d",&t1,&t2),Link(t1,t2); 37 DFS(r,0); 38 for(int i=1;i<=19;i++) 39 for(int j=1;j+(1<<i)-1<=app;j++) 40 st[j][i]=min(st[j][i-1],st[j+(1<<(i-1))][i-1]); 41 for(int i=1;i<=m;i++) 42 { 43 scanf("%d%d",&t1,&t2); 44 printf("%d\n",LCA(t1,t2)); 45 } 46 return 0; 47 } 48
树剖的见树剖板子

浙公网安备 33010602011771号