【小结】tarjan
题单:
Tarjan大礼包
“超级点”就是极大联通子图。缩完之后变权值,DAG 上 DP。
SCC 的编号的倒叙就是拓扑序(注意不唯一)。
因为是按点的遍历顺序缩的。
所以有时可以直接倒叙 DP,不需要队列模拟。
然后 tarjan 的原理就是:
\(low_x\) \(x\) 能到达的最小时间戳。
\(dfn_x\) \(x\) 自己的时间戳。
记录时间戳。然后进入一个环的就是 \(low_x\)=\(dfn_x\)。
若 \(dfn_x\) = \(low_x\),则此点为入口。
若 \(dfn_x\) = \(0\),那么没走过,直接\(low_x=\min(low_x,low_{to})\)。
若 \(to\) 在栈里,则 \(low_x=\min(low_x,dfn_{to})\)。
因为他是环的一部分,可以直接 \(dfn_{to}\),也可以 \(low_{to}\)。因为只要大小关系。
tarjan 本质上也是搜索树 + 返祖边,然后去判断。
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10,M=1e5+10;
int n,m,i,v[N],w[N],x[M],y[M],ans;
int idx,st[N],tp,low[N],dfn[N];
int last[N],c,cnt,id[N],dp[N],din[N];
bool in[N];
queue<int> q;
struct Node{
int to,next;
}a[M];
void add(int u,int v)
{
a[++c]={v,last[u]};
last[u]=c;
}
void tarjan(int x)
{
low[x]=dfn[x]=++idx;
st[++tp]=x;
in[x]=true;
int i,to;
for (i=last[x];i;i=a[i].next)
{
to=a[i].to;
if (!dfn[to])
{
tarjan(to);
low[x]=min(low[x],low[to]);
}
else if (in[to]) low[x]=min(low[x],dfn[to]);
}
if (low[x]==dfn[x])
{
cnt++;
int t;
do
{
t=st[tp];
tp--;
id[t]=cnt;
w[cnt]+=v[t];
in[t]=false;
}while (t!=x);
}
}
void topo()
{
int i,j,x,to;
for (i=cnt;i>=1;i--)
{
dp[i]=max(dp[i],w[i]);
for (j=last[i];j;j=a[j].next)
{
to=a[j].to;
dp[to]=max(dp[to],dp[i]+w[to]);
}
ans=max(ans,dp[i]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for (i=1;i<=n;i++) scanf("%d",&v[i]);
for (i=1;i<=m;i++)
{
scanf("%d%d",&x[i],&y[i]);
add(x[i],y[i]);
}
for (i=1;i<=n;i++)
{
if (!dfn[i]) tarjan(i);
}
for (i=1;i<=n;i++) last[i]=0;
c=0;
for (i=1;i<=m;i++)
{
if (id[x[i]]!=id[y[i]]) add(id[x[i]],id[y[i]]),din[id[y[i]]]++;
}
topo();
printf("%d",ans);
return 0;
}
然后就是双连通分量。
边双联通分量:删任意一条边还能联通(不包含桥)。
桥:一条边,删除后增加联通快。
tarjan。
\(dfn_x>low_{to}\),则此边隔开了两点,故此边为桥。
一般应用场景:缩点成为树。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10,M=2e6+10;
int n,m,i,x,y;
int last[N],c,ID[N],cnt;
int dfn[N],low[N],idx;
bool cut[M];
vector<int> G[N];
struct Node{
int to,next,id;
}a[M<<1];
void add(int u,int v,int w)
{
a[++c]={v,last[u],w};
last[u]=c;
}
void tarjan(int x,int fa)
{
int i,to,id;
dfn[x]=low[x]=++idx;
for (i=last[x];i;i=a[i].next)
{
to=a[i].to;
id=a[i].id;
if (!dfn[to])
{
tarjan(to,id);
low[x]=min(low[x],low[to]);
if (low[to]>dfn[x]) cut[id]=true;
}
else if (id!=fa) low[x]=min(low[x],dfn[to]);
}
}
void dfs(int x)
{
int i,to,id;
ID[x]=cnt;
for (i=last[x];i;i=a[i].next)
{
to=a[i].to;
id=a[i].id;
if (ID[to]||cut[id]) continue;
dfs(to);
}
}
int main()
{
scanf("%d%d",&n,&m);
for (i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
add(x,y,i);
add(y,x,i);
}
for (i=1;i<=n;i++)
{
if (!dfn[i]) tarjan(i,0);
}
for (i=1;i<=n;i++)
{
if (!ID[i])
{
cnt++;
dfs(i);
}
}
for (i=1;i<=n;i++) G[ID[i]].push_back(i);
printf("%d\n",cnt);
for (i=1;i<=cnt;i++)
{
printf("%d ",G[i].size());
for (int x:G[i]) printf("%d ",x);
printf("\n");
}
return 0;
}
点双联通分量:
就是不含割点的极大子图。
性质:
-
除了两点一线,点双都是边双。
-
一个割点一定在 2 个以上(含 2 个)点双里。
-
没有割点 <-> 图联通。
因为删除一个点就是删除多条边。
割点:删除此点后,联通快数量增加。
求割点:tarjan。当 \(dfn_x \leq low_{to}\),此点拦住了 \(to\)。
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+10,M=1e5+10;
int n,m,i,x,y,ans[N],k;
int last[N],c;
int dfn[N],low[N],idx;
bool cut[N];
struct Node{
int to,next;
}a[M<<1];
void add(int u,int v)
{
a[++c]={v,last[u]};
last[u]=c;
}
void tarjan(int root,int x)
{
int i,to,cnt=0;
dfn[x]=low[x]=++idx;
for (i=last[x];i;i=a[i].next)
{
to=a[i].to;
if (!dfn[to])
{
cnt++;
tarjan(root,to);
low[x]=min(low[x],low[to]);
if (low[to]>=dfn[x])
{
if (x!=root||cnt>=2) cut[x]=true;
}
}
else low[x]=min(low[x],dfn[to]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for (i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
for (i=1;i<=n;i++)
{
if (!dfn[i]) tarjan(i,i);
}
for (i=1;i<=n;i++)
{
if (cut[i]) ans[++k]=i;
}
printf("%d\n",k);
for (i=1;i<=k;i++) printf("%d ",ans[i]);
return 0;
}
还要考虑两点一线的情况。
当 \(x=root\) 且 \(son=1\) 时,此点为两点一线,排除掉即可。
然后其他都差不多。就是记录栈 \(st[]\),然后到一个\(类\)割点的时候,就可以弹栈。
易错点:
-
不能弹到 \(x\),这样会多弹子树。
-
不需要特判,因为根节点也满足。
-
弹完要把\(类\)割点 \(x\) 放入。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5+10,M=2e6+10;
int n,m,i,x,y;
int last[N],c;
int dfn[N],low[N],idx,cnt;
stack<int> st;
vector<int> G[N];
struct Node{
int to,next;
}a[M<<1];
void add(int u,int v)
{
a[++c]={v,last[u]};
last[u]=c;
}
void tarjan(int x)
{
if (!last[x])
{
G[++cnt].push_back(x);
return;
}
dfn[x]=low[x]=++idx;
st.push(x);
int i,to;
for (i=last[x];i;i=a[i].next)
{
to=a[i].to;
if (!dfn[to])
{
tarjan(to);
low[x]=min(low[x],low[to]);
if (low[to]>=dfn[x])
{
int tp;
cnt++;
while (!st.empty())
{
tp=st.top();
st.pop();
G[cnt].push_back(tp);
if (tp==to) break;
}
G[cnt].push_back(x);
}
}
else low[x]=min(low[x],dfn[to]);
}
}
int main()
{
scanf("%d%d",&n,&m);
for (i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
if (x==y) continue;
add(x,y);
add(y,x);
}
for (i=1;i<=n;i++)
{
if (!dfn[i]) tarjan(i);
}
printf("%d\n",cnt);
for (i=1;i<=cnt;i++)
{
printf("%d ",G[i].size());
for (int x:G[i]) printf("%d ",x);
printf("\n");
}
return 0;
}
2-SAT
就是一种经典 tarjan。
模型就是给定 \(n\) 个 bool 变量(两种选择),要满足 \(m\) 个条件:\(a_i=x\) 或 \(a_j=y\)。
另 \(i\) 为 \(a_i\) 为 0 的点 ,\(i+n\) 为 \(a_i\) 为 1 的点。
那么问题就转化成了 \(a_i!=x\)->\(a_j=y\),\(a_j!=y\)->\(a_i=x\)。
那么照着这个直接建模就行。
注意最后的构造:为什么从拓扑后往前?为什么一定有解?
因为 \(i\) 和 \(i+n\) 如果不在一条路径上,那么就没关系。
在一条路径上,那么如果在同一个强连通分量里,那么无解。否则我选路径上较后面的点,一定成立。
#include<bits/stdc++.h>
using namespace std;
/*
另 i 表示 i 为 true,i+n 表示 i 为 false。
u->v u 推出 v。
*/
const int N=3e6+10,M=3e6+10;
int n,m,x,y,aa,bb,i;
int low[N],dfn[N],idx,cnt,id[N];
int last[N],c;
int st[N],tp;
bool in[N];
struct Node{
int to,next;
}a[M<<1];
void add(int u,int v)
{
a[++c]={v,last[u]};
last[u]=c;
}
void tarjan(int x)
{
low[x]=dfn[x]=++idx;
st[++tp]=x;
in[x]=true;
int i,to;
for (i=last[x];i;i=a[i].next)
{
to=a[i].to;
if (!dfn[to])
{
tarjan(to);
low[x]=min(low[x],low[to]);
}
else if (in[to]) low[x]=min(low[x],dfn[to]);
}
if (low[x]==dfn[x])
{
cnt++;
int t;
do
{
t=st[tp];
tp--;
id[t]=cnt;
in[t]=false;
}while (t!=x);
}
}
int main()
{
scanf("%d%d",&n,&m);
for (i=1;i<=m;i++)
{
scanf("%d%d%d%d",&x,&aa,&y,&bb);
if (!aa&&!bb) add(x+n,y),add(y+n,x);
else if (!aa&&bb) add(x+n,y+n),add(y,x);
else if (aa&&!bb) add(x,y),add(y+n,x+n);
else add(x,y+n),add(y,x+n);
}
for (i=1;i<=2*n;i++)
{
if (!dfn[i]) tarjan(i);
}
for (i=1;i<=n;i++)
{
if (id[i]==id[i+n])
{
printf("IMPOSSIBLE\n");
return 0;
}
}
printf("POSSIBLE\n");
for (i=1;i<=n;i++) printf("%d ",id[i]>id[i+n]);
return 0;
}

浙公网安备 33010602011771号