Tarjan
重学图论
前置芝士
强连通:对于点x,y,存在一条x到y,和y到x的路径
强连通图:图中每两个点都强联通
强联通分量:极大强联通子图
都是定义在有向图中的
应用:缩点
我们对一张图进行DFS,可以得出一颗搜索树
对于搜索树上的每一条边,我们都可以归为四类:
-
树枝边(x,z):x是y的父节点
-
前向边(x,y):x是y的祖先节点
-
后向边(x,y):就是前向边反过来
-
横叉边(y,a):连向其他树枝的边(需要注意的是,(y,b)并不是一条横叉边,因为b节点是第一次被访问到,因此就成了树枝边)
那么如何判断一个点是否处于强联通分量中呢?
有这两种情况:
- 它可以回到某一个祖先节点(存在一条后向边,指向祖先节点)
- 它存在一条横叉边,该横叉边走到了祖先节点
感性理解一下
于是,就有了:
Tarjan算法求强联通分量(SCC)
引入概念:时间戳
就是按照DFS序,给每个节点一个编号
这样的话我们得到了一些性质:
- 树枝边/前向边的x小于y
- 后箱变x大于y
- 横叉边x大于y
我们对于每个点,定义两个时间戳:
dfn[u]表示遍历到u的时间
low[u]表示从u开始走,所能遍历到的最小时间戳
u是其所在强联通分量的最高点 ⟺ \Longleftrightarrow ⟺ dfs[u]=low[u]
就是从u开始走的话,无论如何也走不到u前面的点
证明懒得找了…
感性理解
板子:
void tarjan(int u){
dfn[u]=low[u]=++timestamp;
stk[++top]=u,in_stk[u]=true;
for(int i=hed[u];i;i=nxt[i]){
int j=ver[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(in_stk[j]) low[u]=min(low[u],dfn[j]);
}
if(dfn[u]==low[u]){
int y;
do{
y=stk[top--];
in_stk[y]=false;
id[y]=scc_cnt;
}while(y!=u);
}
return;
}
时间复杂度O(n+m);
值得注意的是,代码中栈存的点都不是强联通分量的顶点
当前还没有遍历完的强联通分量的所有点
缩点
for(int i=1;i<=n;i++)
for(int i=hed[u];i;i=nxt[i])
if(i和j不在同一SCC中)
add(i,j);//加边
缩完点后,我们就得到了一个DAG
很多人会在缩点后进行一边拓扑排序
但是这可能没啥用,因为tar完之后连通分量标号递减的顺序一定是拓扑序
首先我们进行缩点,因为在DAG上操作更舒服
一个比较显然的结论是:如果图中存在两个 出度为零的点,那么这两头牛一定互不欢迎
同理只存在一个出度为零的点,那么这个点受所有其他牛的欢迎
注意,这个点使我们缩点后的点,因此该点集中的点的个数就是答案
不过我们没有必要重新建出一个DAG其实
复杂度 O ( n ) O(n) O(n)
代码:
/*************************************************************************
> File Name: p2341受欢迎的牛.cpp
> Author: typedef
> Mail: 1815979752@qq.com
> Created Time: 2020/12/16 22:15:12
************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=10010,M=50010;
int n,m;
int hed[N],ver[M],nxt[M];
int tot=0;
int dfn[N],low[N],timestamp;
int stk[N],top;
bool in_stk[N];
int id[N],scc_cnt,size[N];
int dout[N];
void add(int x,int y){
ver[++tot]=y;
nxt[tot]=hed[x];
hed[x]=tot;
return;
}
void tarjan(int u){
dfn[u]=low[u]=++timestamp;
stk[++top]=u,in_stk[u]=true;
for(int i=hed[u];i;i=nxt[i]){
int j=ver[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(in_stk[j]) low[u]=min(low[u],dfn[j]);
}
if(dfn[u]==low[u]){
++scc_cnt;
int y;
do{
y=stk[top--];
in_stk[y]=false;
id[y]=scc_cnt;
size[scc_cnt]++;
}while(y!=u);
}
return;
}
int main(){
scanf("%d%d",&n,&m);
while(m--){
int a,b;
scanf("%d%d",&a,&b);
add(a,b);
}
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=n;i++)
for(int j=hed[i];j;j=nxt[j]){
int k=ver[j];
int a=id[i],b=id[k];
if(a!=b) dout[a]++;
}
int zeros=0,sum=0;
for(int i=1;i<=scc_cnt;i++){
if(!dout[i]){
zeros++;
sum+=size[i];
if(zeros>1){
sum=0;
break;
}
}
}
printf("%d\n",sum);
return 0;
}
首先缩点,把它艹成有向无环图
这样我们变得到了 p 个起点, q 个终点
因此我们至少给这 p 个点发信息,才能传到所有点,这便是第一问
那么第二问,我们只需要加 Max{p,q} 条边了
为什么呢?
我们不妨设 p 小于等于 q
边界情况: p 中有一个点
那么我们只需加 q 条边
若 p 大于1,那么 q,p 大于等于 2
我们至少可以找到两组点
p1 能走到 q1
p2 能走到 q2
反证法:如果找不到这样的两组点
那么必然意味着所有的出发点p都能走到同一个终点
由于终点至少有两个,那么另一个终点就走不回来了
与假设矛盾
因此我们仅需把q1连到p2
这样按照我们的定义,p和q的个数都将会减一
因此我们只需要连 |P| -1次边
然后再连|Q|-(|P|-1)次;
总的次数就是 Q 次
/*************************************************************************
> File Name: p2746校园网.cpp
> Author: typedef
> Mail: 1815979752@qq.com
> Created Time: 2020/12/17 21:38:26
************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=110,M=N*N;
int n;
int hed[N],ver[M],nxt[M];
int tot=0;
int dfn[N],low[N],timestamp;
int stk[N],top;
bool in_stk[N];
int id[N],scc_cnt,size[N];
int dout[N],din[N];
void add(int x,int y){
ver[++tot]=y;
nxt[tot]=hed[x];
hed[x]=tot;
return;
}
void tarjan(int u){
dfn[u]=low[u]=++timestamp;
stk[++top]=u,in_stk[u]=true;
for(int i=hed[u];i;i=nxt[i]){
int j=ver[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(in_stk[j]) low[u]=min(low[u],dfn[j]);
}
if(dfn[u]==low[u]){
++scc_cnt;
int y;
do{
y=stk[top--];
in_stk[y]=false;
id[y]=scc_cnt;
}while(y!=u);
}
return;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
int t;
while(cin>>t,t) add(i,t);
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;i++){
for(int j=hed[i];j;j=nxt[j]){
int k=ver[j];
int a=id[i],b=id[k];
if(a!=b){
dout[a]++;
din[b]++;
}
}
}
int a=0,b=0;
for(int i=1;i<=scc_cnt;i++){
if(!din[i]) a++;
if(!dout[i]) b++;
}
printf("%d\n",a);
if(scc_cnt==1) puts("0");
else printf("%d\n",max(a,b));
system("pause");
return 0;
}
注意特判QwQ
前置定义:
-
半联通:所谓半联通就是值可以满足从u走到v或从v走到u其中之一
-
导出子图:我们先从这张图中选出一些点,再加上相关的边(就是如果一条边连接的两个点属于这些点,那这条边就是相关的)
-
如果导出的子图是半联通的,那他就是半联通子图
-
最大半连通子图就是节点数最多的半联通子图
我们要做的是求出最大半联通子图的节点数量,以及不同最大半联通子图的个数
强联通分量显然是半联通的
所以我们先求出所有的强联通分量
在缩点,艹成拓扑图
我们要做的就是选一条最长(节点数量最大)链(不能分叉)
这条链就是所求
第二问呢?
我们就用递推好了
f[i]
表示以第i个点为终点的最长链的节点数
如何求?
我们枚举i的前驱
搞一个g(i)
,记录方案数
如果能够更新f(i)=f(j)+s(i),g(i)=g(j);
如果恰好相等,我们就把方案数相加
有点像背包求方案数
总结:
- tarjan
- 缩点,建图,给边判重
- 按拓扑序递推
优化常数:哈希表判重
/*************************************************************************
> File Name: p2272最大半联通子图.cpp
> Author: typedef
> Mail: 1815979752@qq.com
> Created Time: 2020/12/18 20:11:13
************************************************************************/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+7,M=2e6+7;
int n,m,mod;
int hed[N],heds[N],ver[M],nxt[M],tot;
int dfn[N],low[N],stk[N];
int timestamp,top;
bool in_stk[N];
int id[N],scc_cnt,size[N];
int f[N],g[N];
void add(int h[],int x,int y){
ver[++tot]=y;
nxt[tot]=h[x];
h[x]=tot;
return;
}
void tarjan(int u){
dfn[u]=low[u]=++timestamp;
stk[++top]=u,in_stk[u]=true;
for(int i=hed[u];i;i=nxt[i]){
int j=ver[i];
if(!dfn[j]){
tarjan(j);
low[u]=min(low[u],low[j]);
}
else if(in_stk[j]) low[u]=min(low[u],dfn[j]);
}
if(dfn[u]==low[u]){
++scc_cnt;
int y;
do{
y=stk[top--];
in_stk[y]=false;
id[y]=scc_cnt;
size[scc_cnt]++;
}while(y!=u);
}
return;
}
int main(){
scanf("%d%d%d",&n,&m,&mod);
while(m--){
int a,b;
scanf("%d%d",&a,&b);
add(hed,a,b);
}
for(int i=1;i<=n;i++) if(!dfn[i]) tarjan(i);
unordered_set<ll> S;
for(int i=1;i<=n;i++)
for(int j=hed[i];i;i=nxt[j]){
int k=ver[j];
int a=id[i],b=id[k];
ll hash=a*1000000ll+b;
if(a!=b&&!S.count(hash)){
add(heds,a,b);
S.insert(hash);
}
}
for(int i=scc_cnt;i>=1;i--){
if(!f[i]) {
f[i]=size[i];
g[i]=1;
}
for(int j=heds[i];j;j=nxt[j]){
int k=ver[j];
if(f[k]<f[i]+size[k]){
f[k]=f[i]+size[k];
g[k]=g[i];
}
else if(f[k]==f[i]+size[k]) g[k]=(g[k]+g[i])%mod;
}
}
int maxf=0,sum=0;
for(int i=1;i<=scc_cnt;i++)
if(f[i]>maxf){
maxf=f[i];
sum=g[i];
}
else if(f[i]==maxf) sum=(sum+g[i])%mod;
printf("%d\n%d\n",maxf,sum);
return 0;
}
大概就是这样…