基础
前置知识
- 强连通:对于有向图中两点 i i i, j j j,若存在 i i i 到 j j j 和 j j j 到 i i i 的路径,则称 i i i, j j j 强连通。
- 强连通:对于有向图中两点 i i i, j j j,若存在 i i i 到 j j j 或 j j j 到 i i i 的路径,则称 i i i, j j j 弱连通。
- 强连通图:任意两点均强连通的图。
- 弱连通图:任意两点均弱连通的图。
- 强连通分量(scc):有向图的极大强连通子图,即再向子图中加点构不成强连通图。
- DFS 树:对图进行 DFS 之后删去所有未经过的边产生的树。
- 树边:在 DFS 树上的边
- 返祖边:对于 u u u 到 v v v 的边,如果 v v v 出栈的时候 u u u 还没有出栈,那么从 v v v 到 u u u 的边是一条返祖边
- 横叉边
- DFS 序(dfn):点被 DFS 的顺序。
Tarjan
一些定义:
low[u]
: u u u 通过树边和至多一条连向当前强连通分量内部的非树边(即返祖边)能访问到的 dfn 的最小值dfn[u]
:DFS 序g[u]
存储所有 u u u 能到达的点sccnum[i]
: i i i在哪个编号的强连通分量中low[u]=dfn[u]
:这个点和它的子树构成强连通分量low[u]<dfn[u]
:这个点和它的父亲构成强连通分量sccsize[i]
:编号为 i i i 的强连通分量中点的数量- s s s 相当于一个栈,按照被 DFS 的顺序往里面扔东西,起点 1 1 1在栈底
如果当前在
u
u
u,遍历
u
∼
v
u\sim v
u∼v。如果
v
v
v 没有被访问过,那么
v
v
v 是树边,low[u]=min(low[u],low[v])
;如果
v
v
v 被访问过,那么判断
v
v
v 是否已经属于一个 scc,如果不属于,那么
u
u
u 和
v
v
v 一定属于同一个 scc,low[u]=min(low[u],dfn[v])
。
代码如下:
void dfs(int u)
{
dfn[u]=low[u]=++dfscnt;//dfscnt为dfs的序号计数器
s.push[u];
for(int v:g[u])//对于g[u]中的每一个元素v
{
if(!dfn[v])
dfs(v),low[u]=min(low[u],low[v]);
else if(!sccnum[v])
low(u)=min(low[u],dfn[v]);
}
if(low[u]==dfn[u])
{
scccnt++;//scccnt为第几个scc
while(1)
{
int x=S.top;S.pop();
sccnum[x]=scccnt;
sccsize[scccnt]++;
if(x==u) break;
}
}
}
割点
Tarjan 算法,可以解决求图中割点的问题。
什么是割点?在无向连通图中,删去一个点和这个点连接的所有边,图就不再连通,那么这个点就是图的割点。
连通图指对于图中任意两点,都有一条路径使两点相通
对于 Tarjan 求割点,我们先选定一个根节点,然后开始 DFS 遍历整个图。对于计算每个节点的子树数量,如果它有超过两棵子树,那么一定是割点,因为断掉这个点它的子树一定不连通。对于非根节点,我们利用 Tarjan 的思想,维护 low
和 dfn
两个标记,对于边
(
u
,
v
)
(u,v)
(u,v),如果 low[v]>=dfn[u]
,则
u
u
u 是割点(因为
v
v
v 到不了
u
u
u 以上的点,所以割掉
u
u
u 之后
v
v
v 会单出来)。
时间复杂度为 O ( n + m ) O(n+m) O(n+m)。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int head[maxn],to[maxn],nxt[maxn],cnt,dfscnt,low[maxn],dfn[maxn];
bool cut[20005];
void add(int x,int y)
{
to[++cnt]=y;
nxt[cnt]=head[x];
head[x]=cnt;
}
void dfs(int u,int fa)
{
low[u]=dfn[u]=++dfscnt;
int c=0;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(!dfn[v])
{
dfs(v,u);
low[u]=min(low[u],low[v]);//树边
if(low[v]>=dfn[u]&&u!=fa) cut[u]=1;
if(u==fa) c++;
}
else low[u]=min(low[u],dfn[v]);//其他
}
if (c>=2&&u==fa) cut[u]=1;
}
int main()
{
int n,m,ans=0;cin>>n>>m;
for(int i=1;i<=m;i++) {int y,x;cin>>x>>y,add(x,y),add(y,x);}
for(int i=1;i<=n;i++) if(!dfn[i]) dfs(i,i);
for(int i=1;i<=n;i++) if(cut[i]) ans++;
cout<<ans<<endl;
for(int i=1;i<=n;i++) if(cut[i]) cout<<i<<' ';
return 0;
}
缩点
强连通分量缩点正常 Tarjan 操作即可。
要求经过点权和最大的路径,在缩点的基础上建新图。又因为 Tarjan 之后建的图满足逆拓扑序,直接拓扑序 DP 即可。
证明:对于两个 SCC S 1 , S 2 S_1,S_2 S1,S2,若 S 1 S_1 S1 可以到达 S 2 S_2 S2,则 S 1 S_1 S1 比 S 2 S_2 S2 后出栈。根据出栈顺序定下 SCC 序号,序号满足逆拓扑序。
#include <bits/stdc++.h>
using namespace std;
const int maxn=1e4+5,maxm=1e5+5;
int head1[maxn],head2[maxn],a[maxn],low[maxn],scc[maxn]/*sizeof scc*/,dfscnt,dfn[maxn],f[maxn],s[maxn],top,scccnt,cnt1,cnt2,sccnum[maxn];
struct edge{int to,nxt;}e1[maxm],e2[maxm];
bool vis[maxn];
void add1(int x,int y){e1[++cnt1]=(edge){y,head1[x]},head1[x]=cnt1;}
void add2(int x,int y){e2[++cnt2]=(edge){y,head2[x]},head2[x]=cnt2;}
void tarjan(int x)
{
low[x]=dfn[x]=++dfscnt,s[++top]=x,vis[x]=1;
for(int i=head1[x];i;i=e1[i].nxt)
{
if(!dfn[e1[i].to]) tarjan(e1[i].to),low[x]=min(low[x],low[e1[i].to]);
else if(vis[e1[i].to]) low[x]=min(low[x],dfn[e1[i].to]);
}
if(dfn[x]==low[x])
{
sccnum[x]=++scccnt;
while(s[top]!=x) sccnum[s[top]]=scccnt,vis[s[top--]]=0;
vis[x]=0,top--;
}
}
int main()
{
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1,u,v;i<=m;i++) cin>>u>>v,add1(u,v);
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++)
{
scc[sccnum[i]]+=a[i];
for(int j=head1[i];j;j=e1[j].nxt) if(sccnum[i]!=sccnum[e1[j].to]) add2(sccnum[i],sccnum[e1[j].to]);
}
int ans=-1;
for(int ii=scccnt;ii;ii--)
{
f[ii]+=scc[ii],ans=max(ans,f[ii]);
for(int i=head2[ii];i;i=e2[i].nxt) f[e2[i].to]=max(f[e2[i].to],f[ii]);
}
cout<<ans;
return 0;
}
2-SAT
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int maxn=2*1e6+5;
int head[maxn],to[maxn],nxt[maxn],cnt;
int dfn[maxn],sta[maxn]/*栈*/,low[maxn],dfncnt,vis[maxn],top/*记录栈中元素编号*/,sccnum/*记录强连通分量编号*/,scc[maxn]/*编号为i的点所在的强连通分量编号*/;
void add(int x,int y)
{
to[++cnt]=y;
nxt[cnt]=head[x];
head[x]=cnt;
}
void tarjan(int u)
{
low[u]=dfn[u]=++dfncnt;
sta[++top]=u;vis[u]=1;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(!dfn[v]) tarjan(v)/*继续往下搜*/,low[u]=min(low[u],low[v]);
else if(vis[v]) low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
++sccnum;
while(sta[top]!=u)//把它搜索树里的都弹完
scc[sta[top]]=sccnum,vis[sta[top]]=0,top--;//数组模拟弹栈
//处理它自己
scc[sta[top]]=sccnum;
vis[sta[top]]=0;
top--;
}
}
int main()
{
int n,m;cin>>n>>m;
for(int i=1;i<=m;i++)
{
int ii,a,jj,b;cin>>ii>>a>>jj>>b;
//实现建两个点,对于一个点k,k+n是true点,k是false点
add(ii+!a*n,jj+b*n);
add(jj+!b*n,ii+a*n);
}
for(int i=1;i<=2*n;i++) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++)
if(scc[i]==scc[i+n]) puts("IMPOSSIBLE"),exit(0);
puts("POSSIBLE");
for(int i=1;i<=n;i++)
{
if(scc[i]>scc[i+n]) cout<<1<<' ';
else cout<<0<<' ';
}
return 0;
}
边双
首先要知道什么是边双连通分量,如果对于一个连通图,删去其中的任何一条边,都不会影响图的连通性,都不会破坏图的连通性,则该子图是一个边双连通分量。我们先用 Tarjan 求出图中的所有割边(桥),所谓割边,就是对于一个连通图,如果删去其中的一条边,连通块会增多即图的连通性会被破坏,那么这条边就是割边。显然对于一个边双来说,里面是不能有割边的,所以我们只需要预处理出所有割边然后删掉一个连通块中的割边就可以得到一个边双啦!注意为了标记桥我们需要让链前的 cnt
从
1
1
1 开始,这样标记桥的时候我们只需要 bridge[i]=bridge[i^1]=1
即可。
Tarjan 求桥的时间复杂度为线性的 O ( n ) O(n) O(n)。
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int maxn=4e6+5;
int head[maxn],to[maxn],nxt[maxn],cnt=1,low[maxn],dfn[maxn],scc[maxn],dfscnt,scccnt;//cnt从1开始,标记桥的时候成对标记
bool bridge[maxn],vis[maxn];
vector<int> ans[maxn];
void add(int x,int y)
{
to[++cnt]=y;
nxt[cnt]=head[x];
head[x]=cnt;
}
void tarjan(int u,int fa)
{
low[u]=dfn[u]=++dfscnt;
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
if(!dfn[v])
{
tarjan(v,i);
low[u]=min(low[u],low[v]);
if(low[v]>dfn[u]) bridge[i]=bridge[i^1]=1;
}
else if(i!=(fa^1)) low[u]=min(low[u],dfn[v]);
}
}
void dfs(int x)
{
scc[x]=scccnt;
if(x) ans[scccnt].push_back(x);
for(int i=head[x];i;i=nxt[i])
{
if(scc[to[i]]||bridge[i]) continue;//如果是桥或该店属于其他连通块停止搜索
dfs(to[i]);
}
}
signed main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
int n,m;cin>>n>>m;
for(int i=1;i<=m;i++)
{
int u,v;cin>>u>>v;
add(u,v);add(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i]) tarjan(i,0);
for(int i=1;i<=n;i++)
if(!scc[i]) ++scccnt,dfs(i);
cout<<scccnt<<endl;
for(int i=1;i<=scccnt;i++)
{
cout<<ans[i].size()<<' ';
for(int j=0;j<ans[i].size();j++) cout<<ans[i][j]<<' ';
cout<<endl;
}
return 0;
}