【Luogu P3387】缩点模板(强连通分量Tarjan&拓扑排序)
Luogu P3387
强连通分量的定义如下:
有向图强连通分量:在有向图G中,如果两个顶点vi,vj间(vi>vj)有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称两个顶点强连通(strongly connected)。如果有向图G的每两个顶点都强连通,称G是一个强连通图。有向图的极大强连通子图,称为强连通分量(strongly connected components)。
来源于百度百科
我本人的理解:有向图内的一个不能再拓展得更大的强连通子图叫做这个有向图的一个强连通分量(也可以说是一个环)
注意:单独的一个孤立的点也会是一个强连通分量
求出强连通分量以后有什么用呢?
很显然,我们可以把整个强连通分量作为单独的一个点,权值按照题目要求取(在这题就是取所有点权的总和),这样就可以让这一个有向有环图转化成一个有向无环图。
Tarjan算法
Tarjan算法(在这里指Tarjan对于强连通分量提出的算法)就是用于求出一个有向图内的所有强连通分量的有效算法。
基本思想就是利用DFS往下搜索,标记顺序,如果找到返回祖先的一条边,则说明会构成一个环(强连通分量)。
这里要引入几个Tarjan算法必备的数组
数组名 | 作用 |
---|---|
dfn[i] | 用于记录节点i的dfs序 |
stk[i] | 一个栈,用于记录当前搜索的这一条链上的节点 |
low[i] | 用于记录节点i能访问到的节点中最小的dfs序 ,也就是最上层的祖先 |
关键点:如果dfn[i]==low[i],意味着在节点i的子树中没有任何的节点可以访问到节点i的祖先,说明节点i与仍然在栈内子节点(必要条件)构成了一个强连通分量
不在栈内的子节点无法与节点i构成强连通分量,原因是不在栈内则说明它本身已经作为一个强连通分量被弹出栈了。
void tarjan(int now)
{
dfn[now]=++tim;//记录dfs序
low[now]=tim;//当前能访问到dfs序最小的点就是自己
stk[++cnt]=now;
vis[now]=true;
for (int i=head[now];i;i=e[i].nxt)
{
int to=e[i].to;
if (!dfn[to])
{
tarjan(to);
low[now]=min(low[now],low[to]);
//如果该点没被遍历过,那么就进行遍历。
}
else
{
if (vis[to]) low[now]=min(low[now],dfn[to]);
//必须判断是否在栈中。只有在同时在栈内的点才有可能构成强连通分量。
}
}
if (low[now]==dfn[now])
{
tot++;//强连通分量的编号
while (stk[cnt]!=now)
{
scc[stk[cnt]]=tot;
val[tot]+=a[stk[cnt]];
vis[stk[cnt]]=false;
cnt--;
}
scc[stk[cnt]]=tot;
val[tot]+=a[stk[cnt]];
vis[stk[cnt]]=false;
cnt--;
//将栈中比u后进入的点和u本身出栈,这些点构成一个强联通分量,打上标记
}
}
结合代码进行理解。
拓扑排序和缩点操作
对一个有向无环图(Directed Acyclic Graph简称DAG)G进行拓扑排序,是将G中所有顶点排成一个线性序列,使得图中任意一对顶点u和v,若边<u,v>∈E(G),则u在线性序列中出现在v之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
来源于百度百科
我个人的理解:对有向无环图中所有节点排序形成一个合法的访问次序。
做一个比喻:吃饭之前要端盘子,端盘子之前要炒菜——那么对于这三件事的拓扑次序就是炒菜→端盘子→吃饭。
那么具体应该如何处理呢?
事实上有两种实现方法,但是我个人暂时只会一种。
利用队列的方式,把所有入度为0的点入队,然后把这几个点对其他点入度的贡献删除,然后再把入度为0的点入队,直到排序完成为止。
完成拓扑排序后就可以利用拓扑次序进行动态规划了。
完整代码如下:
#include<cstdio>
#include<queue>
using namespace std;
queue<int> que;
struct data
{
int sta,to,nxt;
}e[500005],ine[500005],oute[500005];
int dfn[100005],tim,low[100005],stk[100005],cnt,cnti,cnto,head[100005],inhead[100005],outhead[100005];
bool vis[10005];
int tot,scc[100005],val[100005],a[100005],n,in[100005],order[100005],m,u,v,f[100005],ans;
void tarjan(int now)
{
dfn[now]=++tim;//记录dfs序
low[now]=tim;//当前能访问到dfs序最小的点就是自己
stk[++cnt]=now;
vis[now]=true;
for (int i=head[now];i;i=e[i].nxt)
{
int to=e[i].to;
if (!dfn[to])
{
tarjan(to);
low[now]=min(low[now],low[to]);
//如果该点没被遍历过,那么就进行遍历。
}
else
{
if (vis[to]) low[now]=min(low[now],dfn[to]);
//必须判断是否在栈中。只有在同时在栈内的点才有可能构成强连通分量。
}
}
if (low[now]==dfn[now])
{
tot++;//强连通分量的编号
while (stk[cnt]!=now)
{
scc[stk[cnt]]=tot;
val[tot]+=a[stk[cnt]];
vis[stk[cnt]]=false;
cnt--;
}
scc[stk[cnt]]=tot;
val[tot]+=a[stk[cnt]];
vis[stk[cnt]]=false;
cnt--;
//将栈中比u后进入的点和u本身出栈,这些点构成一个强联通分量,打上标记
}
}
void topo()//拓扑排序
{
cnt=0,cnti=0,cnto=0;
for (int i=1;i<=n;i++)
{
for (int j=head[i];j;j=e[j].nxt)
{
if (scc[i]!=scc[e[j].to])
{
oute[++cnto].to=scc[e[j].to];
oute[cnto].nxt=outhead[scc[i]];
outhead[scc[i]]=cnto;
in[scc[e[j].to]]++;
ine[++cnti].sta=scc[i];
ine[cnti].nxt=inhead[scc[e[j].to]];
inhead[scc[e[j].to]]=cnti;
//out前缀的变量是出边的记录
//in前缀的变量是入边的记录,使用了一种另类的链式前向星
}
}
}
for (int i=1;i<=tot;i++)
if (in[i]==0) que.push(i);//入度为零则入队
cnt=0;
while (!que.empty())
{
int u=que.front();
que.pop();
order[++cnt]=u;//记录顺序
for (int i=outhead[u];i;i=oute[i].nxt)
{
int v=oute[i].to;
in[v]--;
if (in[v]==0) que.push(v);
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
scanf("%d",&a[i]);
for (int i=1;i<=m;i++)
{
scanf("%d%d",&u,&v);
e[i].to=v;
e[i].nxt=head[u];
head[u]=i;
//链式前向星存原图
}
tim=0;
for (int i=1;i<=n;i++) if (!dfn[i]) tarjan(i);
//如果没有被遍历过的点要继续遍历。
topo();
for (int i=1;i<=tot;i++)
{
f[order[i]]=val[order[i]];
for (int j=inhead[order[i]];j;j=ine[j].nxt)
f[order[i]]=max(f[order[i]],f[ine[j].sta]+val[order[i]]);
//很容易的一个动态规划
}
for (int i=1;i<=tot;i++) ans=max(f[i],ans);//统计答案
printf("%d",ans);
return 0;
}