Codechef GRAPHCNT 支配树学习及tarjan算法求解

[Codechef GRAPHCNT]新年的有向图

【题目描述】


zlx同学在学习数论时,被虐了一脸,丧心病狂的他决定去报复社会。

zlx在公园里埋下N颗地雷,用来炸飞在春节期间秀恩爱的情侣。这N颗地雷由M条有向边连接成为一个有向图,zlx则在1号地雷处引爆1号地雷可以到达的地雷。现在,为了更好的实施这个计划,zlx需要知道存在多少对地雷(x,y),使得存在一条1到x和一条1到y的路径,这两条路径不经过同一个点(点1除外)。


【输入格式】

第一行为两个正整数N,M。

之后的M行,每行两个正整数v,u。代表存在一条v到u的有向边。

【输出格式】

输出有多少点对满足题目要求。

【样例输入】

6 6
1 2
1 3
1 4
2 5
2 6
3 6

【样例输出】

14

【提示】

对于30%的数据,图为有向无环图。

对于100%的数据,N<=100000,M<=500000


支配树简介

什么是支配树?支配树是什么?
对于一张有向图(可以有环)我们规定一个起点r(为什么是r呢?因为网上都是这么规定的),从r点到图上另一个点w可能存在很多条路径(下面将r到w简写为r->w)。 
如果对于r->w的任意一条路径中都存在一个点p,那么我们称点p为w的支配点(当然这也是r->w的必经点),注意r点不讨论支配点。下面用idom[u]表示离点u最近的支配点。 
对于原图上除r外每一个点u,从idom[u]向u建一条边,最后我们可以得到一个以r为根的树。这个树我们就叫它“支配树”。

相似

这个东西看上去有点眼熟? 
支配点和割点(删掉后图联通块数增加)有什么区别? 
我们考虑问题给定一个起点r和一个终点t,询问删掉哪个点能够使r无法到达t。 
很显然,我们删掉任意一个r->t的必经点就能使r无法到达t,删掉任意一个非必经点,r仍可到达t。 
从支配树的角度来说,我们只需删掉支配树上r到t路径上的任意一点即可 
从割点的角度来说,我们是不是只需要考虑所有割点,判断哪些割点在r->t的路径上即可?是否将某个割点删掉即可让r无法到达t? 
这当然是不正确的,我们可以从两个方面来说明它的错误:

  1. 删掉割点不一定使r无法到达t
    情况1
    这个图中点u是关键点(删掉后图联通块个数增加)
    并且u在r->t的路径上,然而删掉点u后r仍然可以到达t
  2. 图中不一定存在割点
    情况2
    在这个图中不存在任何割点

所以我们没有办法使用割点来解决这个问题。

简化问题

  • 对于一棵树,我们用r表示根节点,u表示树上的某个非根节点。很容易发现从r->u路径上的所有点都是支配点,而idom[u]就是u的父节点。 
    这个可以在 O(n) 的时间内实现。

  • DAG(有向无环图)

    因为是有向无环图,所以我们可以按照拓扑序构建支配树。 
    假设当前我们构造到拓扑序中第x个节点编号为u,那么拓扑序中第1 ~ X-1个节点已经处理好了,考虑所有能够直接到达点u的节点,对于这些节点我们求出它们在支配树上的最近公共祖先v,这个点v就是点u在支配树上的父亲。 
    如果使用倍增求LCA,这个问题可以在   O((n+mlog2       n) 的时间内实现。

对于这两个问题我们能够很简便的求出支配树。

有向图

对于一个有向图,我们应该怎么办呢?

简单方法

我们可以考虑每次删掉一个点,判断哪些点无法从r到达。 
假设删掉点u后点v无法到达,那么点u就是r->v的必经点(点u就是v的支配点)。 
这个方法我们可以非常简单的在 O(nm) 的时间内实现。 
其中 n 是点数, m 是点数。

更快的方法

这里,我将介绍Lengauer-Tarjan算法。 
这个算法能在很快的时间内求出支配树。 
要介绍这个算法我们还需引入两个定理和一些概念

大概步骤

首先来介绍一些这个算法的大概步骤

  1. 对图进行DFS(深度优先遍历)并求出搜索树和DFS序。这里我们用 dfn[x] 表示点 x 在dfs序中的位置。
  2. 根据半必经点定理计算出所有的半必经点作为计算必经点的根据
  3. 根据必经点定理修正我们的半必经点,求出支配点
半必经点

我们用idom[x]表示点x的最近支配点,用semi[x]表示点x的半必经点。 
那什么是半必经点呢?

对于一个节点 Y ,存在某个点 X 能够通过一系列点 p i (不包含 X Y )到达点 Y i   dfn[i]>dfn[Y] ,我们就称 X Y 的半必经点,记做 semi[Y]=X

当然一个点 X 的“半必经点”会有多个,而且这些半必经点一定是搜索树中点 X 的祖先(具体原因这里不做详细解释,请自行思考)。 
对于每个点,我们只需要保存其半必经点中 dfn 最小的一个,下文中用 semi[x] 表示点 x 的半必经点中 dfn 值最小的点的编号。 
我们可以更书面一点的描述这个定理:

  • 对于一个节点 Y 考虑所有能够到达它的节点,设其中一个为 X
  • dfn[X]<dfn[Y],则  X Y 的一个半必经点
    图片1
  • dfn[X]>dfn[Y] ,那么对于 X     在搜索树中的祖先  Z   (包括 X ),如果满足 dfn[Z]>dfn[Y] 那么 semi[Z] 也是 Y 的半必经点
    图片2

在这些必经点中,我们仅需要 dfn 值最小的 
这个半必经点有什么意义呢?

我们求出深搜树后,考虑原图中所有非树边(即不在树上的边),我们将这些边删掉,加入一些新的边  {(semi[w],w)|wV   and   wr} ,我们会发现构建出的新图中每一个点的支配点是不变的,通过这样的改造我们使得原图变成了DAG

是否接下来使用DAG的做法来处理就可以做到 nlog2n 呢?我没试过,不过我有更好的方法。

必经点

一个点的半必经点有可能是一个点的支配点,也有可能不是。我们需要使用必经点定理对这个半必经点进行修正,最后得到必经点

对于一个点 X ,我们考虑搜索树上 semi[X] X 路径上的所有点 p0p1p2p3...pk 。对于所有 pi(1i<k) ,我们找出 dfn[semi[  pi    ]]    最小的一个      pi   记为 Z

  • 考虑搜索树上 X semi[X] 之间的其他节点(即不包含 X semi[X] ),其中半必经点 dfn 值最小的记为 Z
  • 如果 semi[Z]=semi[X],则 idom[X]=semi[X]
    图片3
  • 如果 semi[Z]semi[X] ,则 idom[X]=idom[Z]
    图片4

具体实现

对于求半必经点与必经点我们都需要处理一个问题,就是对于一个节点 X 的前驱 Y ,我们需要计算 Y 在搜索树上所有 dfn 值大于 dfn[X] 的祖先中 semi 值最小的一个,我们可以按 dfn 从大到小的顺序处理,使用并查集维护,这样处理到节点 X 值时所有 dfn 值比 X 大的点都被维护起来了。 
对于 Y 的所有满足条件的祖先,就是在并查集中 Y 的祖先,可以通过带权并查集的方法,维护祖先中的最小值,并记下 semi 最小的具体是哪个节点。 
这样我们就能够在 O((n+m)×α(n)) 时间内解决这个问题。

代码

 Codechef   GRAPHCNT 的代码

#include <iostream>
#include <cstdio>
using namespace std;
typedef long long lld;
const int MaxN = 100000 + 10, MaxE = (5 * 100000) * 2 + MaxN;
int head[MaxN], pre[MaxN], dom[MaxN], to[MaxE], nxt[MaxE], top;
void addedge(int *h,int fr,int tt)
{
    top ++;
    nxt[top] = h[fr];
    to[top] = tt;
    h[fr] = top;
}
int n, m;
void init()
{
    scanf("%d%d", &n, &m);
    int a, b;
    for(int i = 1; i <= m; ++i)
    {
        scanf("%d%d", &a, &b);
        addedge(head, a, b);
        addedge(pre, b, a);
    }
}
int bcj[MaxN], semi[MaxN], idom[MaxN], best[MaxN], dfn[MaxN], id[MaxN], fa[MaxN], dfs_clock;
int push(int v)
{
    if(v == bcj[v]) return v;
    int y = push(bcj[v]);
    if(dfn[semi[best[bcj[v]]]] < dfn[semi[best[v]]]) best[v] = best[bcj[v]];
    return bcj[v] = y;
}//带权并查集路径压缩
void dfs(int rt)
{
    dfn[rt] = ++dfs_clock;
    id[dfs_clock] = rt;
    for(int i = head[rt]; i; i = nxt[i])
        if(!dfn[to[i]])
        {
            dfs(to[i]);
            fa[to[i]] = rt;
        }

}//求出dfs序
void tarjan()
{
    for(int i = dfs_clock, u; i >= 2; --i)
    {//按dfs序从大到小计算
        u = id[i];
        for(int j = pre[u]; j; j = nxt[j])//semi
        {
            if(!dfn[to[j]]) continue;
            push(to[j]);
            if(dfn[semi[best[to[j]]]] < dfn[semi[u]]) semi[u] = semi[best[to[j]]];
        }
        addedge(dom, semi[u], u);
        bcj[u] = fa[u];u = id[i - 1];
        for(int j = dom[u]; j; j = nxt[j])//idom
        {
            push(to[j]);
            if(semi[best[to[j]]] == u) idom[to[j]] = u;
            else idom[to[j]] = best[to[j]];
        }
    }
    for(int i = 2, u; i <= dfs_clock; ++i)
    {
        u = id[i];
        if(idom[u] != semi[u]) idom[u] = idom[idom[u]];
    }
}
int sons[MaxN];
lld ans;
void calc_son()
{
    for(int i = dfs_clock, u; i >= 2; --i)
    {
        u = id[i];
        ++ sons[u];
        if(idom[u] != 1) sons[idom[u]] += sons[u];
        else ans -= ((lld)sons[u] * (lld)(sons[u] - 1)) / 2ll;
    }
}
void solve()
{
    for(int i = 1; i <= n; ++i) bcj[i] = semi[i] = best[i] = i;
    dfs_clock = 0;
    dfs(1);
    tarjan();
    ans = ((lld)dfs_clock * (lld)(dfs_clock - 1)) / 2ll;
    calc_son();
    cout << ans << endl;
}
int main()
{
    init();
    solve();
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值