tarjan

缩点

本质上来说就是在有向边找强连通分量缩点。

提前说明下变量:

  1. \(dfn_i\):代表编号为 \(i\) 的点的 DFS 序序号。
  2. \(low_i\):代表编号为 \(i\) 的点所在的强连通分量中所有点中的最小dfn值。
  3. \(kase\):时间戳。
  4. \(id_i\):缩点之后重新给点编号。
  5. \(num_i\):每个强连通分量的点的数量。
  6. \(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;
}

】、

posted @ 2023-05-09 17:20  Symbolize  阅读(41)  评论(0)    收藏  举报