tarjan
缩点
本质上来说就是在有向边找强连通分量缩点。
提前说明下变量:
- \(dfn_i\):代表编号为 \(i\) 的点的 DFS 序序号。
- \(low_i\):代表编号为 \(i\) 的点所在的强连通分量中所有点中的最小dfn值。
- \(kase\):时间戳。
- \(id_i\):缩点之后重新给点编号。
- \(num_i\):每个强连通分量的点的数量。
- \(vis\):标记这个点是否在栈中。
图解:
例图

进入一个没有遍历过的点 \(x\),更新 dfn[x]=low[x]=++kase;。

遍历 \(x\) 的所有出边,从 \(x\) 到 \(to\),如果发现 \(to\) 点还没有被遍历过,dfs(j),之后更新 low[x]=min(low[to],low[x]);。

如果发现 \(to\) 点已经在栈中,更新low[x]=min(low[u],dfn[to]);。
\(4 \rightarrow 3\)

\(4 \rightarrow 2\)

当一个点 \(x\) 遍历完了所有的边后,检查 \(dfn_x\) 是不是等于 \(low_x\),如果是,则不停的弹出栈顶元素直到栈顶元素是自己,这个操作中所有弹出的点都和节点 \(x\) 同属于一个强连通分量,可以缩为一个点,分配一个 \(id\) 编号。
dfs 遍历完点 \(5,4,6,3,7\) 后

dfs 遍历完点 \(2\) 后

dfs 遍历完点 \(1\) 后

缩完点后

最后跑 top 排序就不多说了。
AC Code of Luogu P3387【模板】缩点
#include<bits/stdc++.h>
#define pii pair<int,int>
#define x first
#define y second
#define rep1(i,l,r) for(int i=l;i<=r;i++)
#define rep2(i,l,r) for(int i=l;i>=r;i--)
const int N=1e5+10;
using namespace std;
int n,m,a[N],h1[N],e1[N],ne1[N],h2[N],e2[N],ne2[N],w[N],dfn[N],idx,kase,low[N],stk[N],top,cnt,id[N],dist[N],dis[N];
bool vis[N];
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return f*x;
}
void add1(int x,int y)
{
e1[idx]=y;
ne1[idx]=h1[x];
h1[x]=idx++;
}
void add2(int x,int y)
{
e2[idx]=y;
ne2[idx]=h2[x];
h2[x]=idx++;
}
void tarjan(int x)
{
dfn[x]=low[x]=++kase;//dfn记录时间戳,low记录追溯值,是一个强连通分量中最小的时间戳的值
vis[x]=1;
stk[++top]=x;//存放同一个强连通分量的点
for(int i=h1[x];~i;i=ne1[i])
{
int to=e1[i];
if(!dfn[to])
{
tarjan(to);
low[x]=min(low[x],low[to]);
}
else if(vis[to]) low[x]=min(low[x],low[to]);
}
if(low[x]==dfn[x])
{
++cnt;//记录强连通分量的个数
while(1)
{
int k=stk[top--];
id[k]=cnt;//记录当前点属于哪一个强连通分量
vis[k]=0;
if(k==x) break;
}
}
return;
}
int Top()
{
queue<int> q;
rep1(i,1,cnt)
{
if(!dist[i])
{
dis[i]=w[i];
q.push(i);
}
}
while(!q.empty())
{
int t=q.front();
q.pop();
for(int i=h2[t];~i;i=ne2[i])
{
int to=e2[i];
dis[to]=max(dis[to],dis[t]+w[to]);
--dist[to];
if(!dist[to]) q.push(to);
}
}
int ans=0;
rep1(i,1,cnt) ans=max(ans,dis[i]);
return ans;
}
signed main()
{
memset(h1,-1,sizeof h1);
memset(h2,-1,sizeof h2);
n=read();
m=read();
rep1(i,1,n) a[i]=read();
rep1(i,1,m)
{
int u=read();
int v=read();
add1(u,v);
}
rep1(i,1,n) if(!dfn[i]) tarjan(i);//进行强连通块的划分
//建立连通块之间的边
rep1(i,1,n)
{
for(int j=h1[i];~j;j=ne1[j])
{
int to=e1[j];
if(id[i]!=id[to])
{
add2(id[i],id[to]);
++dist[id[to]];
}
}
w[id[i]]+=a[i];
}
cout<<Top()<<endl;//拓扑排序
return 0;
}
割点
首先选定一个初始节点,从该节点 dfs 遍历整个图。
对于初始节点,计算其子树数量,如果不止一颗子树,就是割点。因为如果去掉这个点,这两棵子树就不能互相到达。
对于非根节点,判断是不是割点就有些麻烦了。维护 \(dfn\) 和 \(low\),\(dfn_u\) 表示顶点 \(u\) 访问时间排名,\(low_u\) 表示顶点 \(u\) 及其子树中的点,通过非父子边,能够回溯到的最小的 \(dfn\) 的值。对于边 \(u \leftrightarrow v\),如果 \(low_v \geq dfn_u\),此时u就是割点。
\(low\) 的维护:
最开始 \(low_u = dfn_u\)
有一条边 \(u \leftrightarrow v\),如果 \(v\) 未访问过,继续遍历,遍历完之后,\(low_u = \min (low_u,low_v)\);
如果v访问过则一定有 \(dfn_v < dfn_u,low_u = \min (low_u,dfn_v)\)。
AC Code of Luogu P3388 【模板】割点(割顶)
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define x first
#define y second
#define rep1(i,l,r) for(int i=l;i<=r;i++)
#define rep2(i,l,r) for(int i=l;i>=r;i--)
const int N=2e5+10;
const int inf=0x3f3f3f3f3f3f3f3f;
using namespace std;
int n,m,h[N],e[N],ne[N],idx,dfn[N],low[N],s[N],ans,kase;
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return f*x;
}
void add(int x,int y)
{
e[idx]=y;
ne[idx]=h[x];
h[x]=idx++;
}
void tarjan(int x,int fa)
{
dfn[x]=low[x]=++kase;
int son=0;
for(int i=h[x];~i;i=ne[i])
{
int to=e[i];
if(!dfn[to])
{
tarjan(to,fa);
low[x]=min(low[x],low[to]);
if(low[to]>=dfn[x]&&x!=fa) s[x]=1;
if(x==fa) ++son;
}
low[x]=min(low[x],dfn[to]);
}
if(son>=2&&x==fa) s[x]=1;
return;
}
signed main()
{
memset(h,-1,sizeof h);
n=read();
m=read();
rep1(i,1,m)
{
int u=read();
int v=read();
add(u,v);
add(v,u);
}
rep1(i,1,n) if(!dfn[i]) tarjan(i,i);//tarjan
rep1(i,1,n) if(s[i]) ++ans;//割点个数
cout<<ans<<endl;
rep1(i,1,n) if(s[i]) cout<<i<<' ';
putchar('\n');
return 0;
}
双联通
点双联通
定义
点双联通图满足去掉任意一个点及其所连的边以后仍然联通的图。
实现
我们可以发现点双联通图与没有割点形成充要条件,即点双联通图一定无割点,无割点的图一定是点双联通图。所以他的判定就出来了,如果要寻找一个图中的点双联通子图,可以采用缩点使用栈,只是 pop 到 \(to\) 就行了,因为 \(x\) 若为割点,则也有可能在别的子图中。
Code
AC Code of Luogu P8435 【模板】点双连通分量
/*
Luogu name: Symbolize
Luogu uid: 672793
*/
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define x first
#define y second
#define rep1(i,l,r) for(register int i=l;i<=r;++i)
#define rep2(i,l,r) for(register int i=l;i>=r;--i)
#define rep3(i,x,y,z) for(register int i=x[y];~i;i=z[i])
#define rep4(i,x) for(auto i:x)
#define debug() puts("----------")
const int N=4e6+10;
const int inf=0x3f3f3f3f3f3f3f3f;
using namespace std;
int n,m,h[N],e[N],ne[N],idx,dfn[N],low[N],kase,s[N],top,id;
vector<int> ans[N];
int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return f*x;
}
void add(int x,int y)
{
e[idx]=y;
ne[idx]=h[x];
h[x]=idx++;
}
void tarjan(int x,int fa)
{
int cnt=0;
dfn[x]=low[x]=++kase;
s[++top]=x;
rep3(i,h,x,ne)
{
int to=e[i];
if(!dfn[to])
{
++cnt;
tarjan(to,x);
low[x]=min(low[x],low[to]);
if(low[to]>=dfn[x])
{
++id;
while(s[top+1]!=to) ans[id].push_back(s[top--]);//pop到to
ans[id].push_back(x);//单独加入x
}
}
else if(to!=fa) low[x]=min(low[x],dfn[to]);
}
if(!fa&&!cnt) ans[++id].push_back(x);
return;
}
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
memset(h,-1,sizeof h);
n=read();
m=read();
rep1(i,1,m)
{
int u=read();
int v=read();
add(u,v);
add(v,u);
}
rep1(i,1,n)
{
if(!dfn[i])
{
top=0;
tarjan(i,0);
}
}
cout<<id<<endl;
rep1(i,1,id)
{
cout<<ans[i].size()<<' ';
rep4(j,ans[i]) cout<<j<<' ';
putchar('\n');
}
return 0;
}
边双联通
定义
边双联通图满足去掉任意一条边后仍联通的图。
实现
我们可以发现在无向图中,对于一条桥(割边)的判定是 \(dfn_x<low_{to}\),意思就是说如果经过一条边后在不经过其返祖边的前提下无法会去。
Code
AC Code of Luogu P8436 【模板】边双连通分量
/*
Luogu name: Symbolize
Luogu uid: 672793
*/
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define x first
#define y second
#define rep1(i,l,r) for(register int i=l;i<=r;++i)
#define rep2(i,l,r) for(register int i=l;i>=r;--i)
#define rep3(i,x,y,z) for(register int i=x[y];~i;i=z[i])
#define rep4(i,x) for(auto i:x)
#define debug() puts("----------")
const int N=4e6+10;
const int inf=0x3f3f3f3f3f3f3f3f;
using namespace std;
int n,m,h[N],e[N],ne[N],idx,dfn[N],low[N],kase,cnt,st[N],vis[N],id;
vector<int> ans[N];
int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return f*x;
}
void add(int x,int y)
{
e[idx]=y;
ne[idx]=h[x];
h[x]=idx++;
return;
}
void tarjan(int x,int from)
{
dfn[x]=low[x]=++kase;
rep3(i,h,x,ne)
{
int to=e[i];
if(!dfn[to])
{
tarjan(to,i);
low[x]=min(low[x],low[to]);
if(dfn[x]<low[to]) st[i]=st[i^1]=1;
}
else if(i!=(from^1)/*判断是否为返祖边*/) low[x]=min(low[x],dfn[to]);
}
return;
}
void dfs(int x)
{
ans[id].push_back(x);
vis[x]=1;
rep3(i,h,x,ne)
{
int to=e[i];
if(vis[to]||st[i]) continue;
dfs(to);
}
return;
}
signed main()
{
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
memset(h,-1,sizeof h);
n=read();
m=read();
rep1(i,1,m)
{
int u=read();
int v=read();
add(u,v);
add(v,u);
}
rep1(i,1,n) if(!dfn[i]) tarjan(i,-1);
rep1(i,1,n)
{
if(vis[i]) continue;
++id;
dfs(i);
}
cout<<id<<endl;
rep1(i,1,id)
{
cout<<ans[i].size()<<' ';
rep4(j,ans[i]) cout<<j<<' ';
putchar('\n');
}
return 0;
}
2-SAT
SAT
可满足性问题(Satisfiability)
我们有若干个 bool 变量需要对其赋值以满足限定的要求。
例如:\((a \vee \neg b \vee c) \wedge (a \vee b \vee c) \wedge( \neg a \vee \neg b \vee \neg c)\)
SAT 为 NPC(NP完全)问题,只能暴力求解。
2-SAT
讲解
就是有两个 bool 变量需要对其赋值以满足限定的要求。
这个问题就不一定使用暴力了。
我们将每一个点拆为 \(x\) 与 \(\neg x\)。
由此我们就得到了以下表格
| 逻辑运算 | 建图 |
|---|---|
| \(a \vee b\) | \(\neg a \rightarrow b \wedge \neg b \rightarrow a\) |
| \(a \vee \neg b\) | $\neg a \rightarrow \neg b \wedge b \rightarrow a $ |
| \(\neg b \vee a\) | \(a \rightarrow b \wedge \neg b \rightarrow \neg a\) |
| \(\neg a \vee \neg b\) | \(a \rightarrow \neg b \wedge b \rightarrow \neg a\) |
| 所以我们就可以得到 |
\((a \vee \neg b) \wedge (a \vee b) \wedge( \neg a \vee \neg b)\)的图:

我们可以发现,在同一强联通分量之中,知道一个值,可以顺势推出另外的所有数的值。
再进一步我们发现,只要有 \(x\) 与 \(\neg x\) 在同一强联通分量则一定无解。
由此,我们给每个强连通分量一个 top 序,若 \(id_x < id_{\neg x}\) 则 \(x=1\),否则为 \(0\)。
跑个 tarjan 就好了!
Code
AC Code of Luogu P4782 【模板】2-SAT 问题
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define x first
#define y second
#define rep1(i,l,r) for(int i=l;i<=r;i++)
#define rep2(i,l,r) for(int i=l;i>=r;i--)
const int N=4e6+10;
using namespace std;
int n,m,h[N],e[N],ne[N],idx,kase,dfn[N],low[N],id[N],stk[N],cnt,top;
bool vis[N];
inline int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return f*x;
}
void add(int x,int y)
{
e[idx]=y;
ne[idx]=h[x];
h[x]=idx++;
return;
}
void tarjan(int x)
{
dfn[x]=low[x]=++kase;
stk[++top]=x;
vis[x]=1;
for(int i=h[x];~i;i=ne[i])
{
int to=e[i];
if(!dfn[to])
{
tarjan(to);
low[x]=min(low[x],low[to]);
}
else if(vis[to]) low[x]=min(low[x],dfn[to]);
}
if(dfn[x]==low[x])
{
++cnt;
while(1)
{
int k=stk[top--];
id[k]=cnt;
vis[k]=0;
if(k==x) break;
}
}
return;
}
signed main()
{
memset(h,-1,sizeof h);
n=read();
m=read();
rep1(i,1,m)
{
int a=read();
int x=read();
int b=read();
int y=read();
if(!x)
{
if(!y)//x[a]==0||x[b]==0
{
add(a+n,b);
add(b+n,a);
}
else//x[a]==0||x[b]==1
{
add(a+n,b+n);
add(b,a);
}
}
else
{
if(!y)//x[a]==1||x[b]==0
{
add(a,b);
add(b+n,a+n);
}
else//x[a]==1||x[b]==1
{
add(a,b+n);
add(b,a+n);
}
}
}
rep1(i,1,(n<<1)) if(!dfn[i]) tarjan(i);//判断强联通分量
rep1(i,1,n)
{
if(id[i]==id[i+n])//在同一强联通分量则无解
{
puts("IMPOSSIBLE");
return 0;
}
}
puts("POSSIBLE");
rep1(i,1,n)//方案
{
if(id[i]>id[i+n]) cout<<1<<' ';
else cout<<0<<' ';
}
return 0;
}
】、

浙公网安备 33010602011771号