仙人掌&圆方树学习笔记+简单应用

仙人掌&圆方树学习笔记

前言

一直觉得仙人掌和圆方树是非常高深的算法。
直到连续随机跳题跳到两道。我受不了啦!!!
于是点进了一个链接。(传销现场有木有啊!)

推荐链接 :戳这里

然后发现并没有想象中那么难。
而且,,,
很好玩。
日常赛前学算法(大雾
退役之前还是多多益善一波吧,错过了可能就遇不上了!

定义:仙人掌

任意一条边至多在一个环里的无向联通图

定义:仙人掌的圆方树

  • 仙人掌 G = ( V , E ) G = (V, E) G=(V,E)的圆方树 T = ( V T , E T ) T = (V_T , E_T) T=(VT,ET)为满足以下条件的无向图:
    V T = R T ∪ S T , R T = V , R T ∩ S T = ∅ V_ T = R_ T ∪ S_ T , R_ T = V, R_ T ∩ S_ T = ∅ VT=RTST,RT=V,RTST=,我们称 R T R_ T RT 集合为圆点、 S T S_ T ST集合为方点
    ∀ e ∈ E \forall e \in E eE,若 e e e 不在任何简单环中,则 e ∈ E T e ∈ E_T eET
    对于每个仙人掌中的简单环 R R R,存在方点 p R ∈ S T p_ R ∈ S_ T pRST ,并且 ∀ p ∈ R \forall p \in R pR满足 ( p R , p ) ∈ E T (p _R , p) \in E_ T (pR,p)ET ,即对每个环建方点连所有点

严格的定义总是格外地难懂啊。
简单地说,把除了环上的所有边保留。对于每个环,新建一个方点,把所有环上的点与这个方点连边,删除原有的环上的边。
在这里插入图片描述

如何证明圆方树是一棵树?
首先显然,它仍然联通。
其次,对于每个环,新建的边数等于删除的边数,点数多了一个。
所以最后一定是一棵树。

圆方树的构造与性质

构造就是 T a r j a n Tarjan Tarjan缩环啊。
性质:

  • 方点不会和方点连边。(不然就有一条边在两个环里了)
  • 圆方数是一颗无根树,无论以仙人掌的哪个根构建出来的圆方树都是一样地,进一步地,仙人掌的圆方树和仙人掌是一一对应的
  • 定义以 r r r为根的仙人掌的 p p p子仙人掌为除去 p p p r r r的所有简单路径后, p p p所在联通块,则 p p p子仙人掌对应圆方树就是 r r r的圆方树上以 p p p为根的子树。

最后一条性质看上去很拗口。实际上,它想表达的意思是,仙人掌并不改变原图任意两点之间的连通性。这里的连通性包括路径信息,连边情况等。

简单圆方树问题

例1:bzoj4316: 小C的独立集

题目大意:给定一个仙人掌,求最大独立集。
这道题其实并不需要真正建立出圆方树。只需要在 T a r j a n Tarjan Tarjan的时候把环的情况处理一下。
具体地,对于树边,照常转移。直接跳过环。然后在环上合并子树的信息。唯一不同的是如果当前子仙人掌的根要选,那么环上和根相邻的两个节点都不能选(处理方法和基环树类似)。

#include
const int M = 130000, N = 55000;
int ri() {
    char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f  = -1;
    for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int to[M], nx[M], pr[N], g[N], dfn[N], low[N], f[N][2], tp, tm;
void add(int u, int v) {to[++tp] = v; nx[tp] = pr[u]; pr[u] = tp;}
void adds(int u, int v) {add(u, v); add(v, u);}
void Tra(int x[2], int y[2]) {x[0] += std::max(y[0], y[1]); x[1] += y[0];}
void Mg(int x[2], int y[2]) {int t = x[0] + y[0]; x[0] = std::max(t, x[1] + y[1]); x[1] = t;}
void Rdp(int u, int v) {
    int x[2] = {0};
    for(int i = v; i != u; i = g[i]) Mg(x, f[i]);
    f[u][0] += x[0]; x[0] = 0; x[1] = -1e9;
    for(int i = v; i != u; i = g[i]) Mg(x, f[i]);
    f[u][1] += x[1];
}
void Tar(int u, int fa) {
    dfn[u] = low[u] = ++tm; g[u] = fa; f[u][1] = 1; f[u][0] = 0;
    for(int i = pr[u]; i; i = nx[i]) {
        if(!dfn[to[i]]) Tar(to[i], u), low[u] = std::min(low[u], low[to[i]]);
        else if(to[i] != fa) low[u] = std::min(low[u], dfn[to[i]]);
        if(low[to[i]] > dfn[u]) Tra(f[u], f[to[i]]); //树边 
    }
    for(int i = pr[u]; i; i = nx[i]) //环 
        if(g[to[i]] != u && dfn[u] < dfn[to[i]]) 
            Rdp(u, to[i]);
}
int main() {
    int n = ri(), m = ri();
    for(int i = 1;i <= m; ++i) adds(ri(), ri());
    Tar(1, 0); printf("%d\n", std::max(f[1][1], f[1][0]));
    return 0;
}
例2:bzo2125: 最短路

题目大意:求仙人掌上任意两点最短路,没有负边权。
这题必须建出圆方树。
考虑边权。圆圆边不变,圆方边定义为这个节点到环顶的最短距离(两个方向比一下)。
这样,如果穿过了某个环,考虑两种情况。

  1. 从环上某个结点 v v v进入,从环顶 p p p出。会经过一条边权为 d i s t ( v , p ) dist(v,p) dist(v,p)的圆方边和一条边权为0的圆方边( d i s t ( p , p ) = 0 dist(p,p)=0 dist(p,p)=0),正好是穿过环的最短路。
  2. 从环上某个结点 v v v进入,从环上某个节点 u u u出。这样子的话。起点终点在圆方树上的 L c a Lca Lca必定是当前这个环对应的方点。用树剖/倍增求出 u u u, v v v,判断哪个方向更加优秀即可。(类似基环树的处理方法)

其余情况不变,直接跑 L c a Lca Lca即可。
这道题就很好地体现了圆方树与仙人掌的对应关系。同时也提醒了我们在圆方树上处理问题要重点考虑圆方边,让其尽量反映出原图的各种性质

#include
const int N = 2e4 + 10;
int ri() {
    char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f  = -1;
    for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int fa[N], de[N], tot, n; 
struct Edge {
    int to[N << 2], nx[N << 2], w[N << 2], pr[N], tp;
    void add(int u, int v, int W) {to[++tp] = v; nx[tp] = pr[u]; pr[u] = tp; w[tp] = W;}
    void adds(int u, int v, int w) {add(u, v, w); add(v, u, w);} 
};
struct Round_Square_Tree {
    Edge T; int sz[N], fa[N], ds[N], de[N], d[N], di[N], c[N];
    void dfs1(int u, int ff) {
        fa[u] = ff; de[u] = de[ff] + 1; sz[u] = 1;
        for(int i = T.pr[u], v; i; i = T.nx[i]) 
        if((v = T.to[i]) != ff){
            di[v] = di[u] + T.w[i]; dfs1(v, u);
            sz[u] += sz[v]; sz[ds[u]] < sz[v] ? ds[u] = v : 0;
        }
    }
    void dfs2(int u, int c) {
        d[u] = c; if(!ds[u]) return ; dfs2(ds[u], c);
        for(int i = T.pr[u]; i; i = T.nx[i]) 
            if(T.to[i] != ds[u] && T.to[i] != fa[u]) 
                dfs2(T.to[i], T.to[i]);
    }
    int Lca(int u, int v) {
        for(;d[u] != d[v]; u = fa[d[u]]) de[d[u]] < de[d[v]] ? u ^= v ^= u ^= v : 0;
        return de[u] < de[v] ? u : v;
    }
    int Jump(int u, int c) {
        int r; for(;d[u] != d[c];) r = d[u], u = fa[d[u]];
        return u == c ? r : ds[c];
    }
    int Query(int u, int v) {
        int x = Lca(u, v);
        if(x <= n) return di[u] + di[v] - (di[x] << 1);
        int cu = Jump(u, x), cv = Jump(v, x), cd = abs(c[cu] - c[cv]);
        cd = std::min(cd, c[x] - cd);
        return di[u] - di[cu] + di[v] - di[cv] + cd;
    }
}rst;
struct Tarjan {
    Edge G; int di[N], fa[N], dfn[N], low[N], tm;
    void Build(int u, int v, int d) {
        int tp = de[v] - de[u] + 1, s = d + di[v] - di[u], D = d; rst.c[++tot] = s;
        for(int i = v; i != u; i = fa[i])
            rst.T.adds(tot, i, std::min(D, s - D)), rst.c[i] = D, D += di[i] - di[fa[i]];
        rst.T.adds(tot, u, 0);
    }
    void dfs(int u, int ff) {
        fa[u] = ff; dfn[u] = low[u] = ++tm; de[u] = de[ff] + 1;
        for(int i = G.pr[u]; i; i = G.nx[i]) {
            int v = G.to[i]; if(v == ff) continue;
            if(!dfn[v]) di[v] = di[u] + G.w[i], dfs(v, u), low[u] = std::min(low[u], low[v]);
            else low[u] = std::min(low[u], dfn[v]);
            if(dfn[u] < low[v]) rst.T.adds(u, v, G.w[i]);
        }
        for(int i = G.pr[u]; i;i = G.nx[i]) {
            int v = G.to[i]; if(v == ff) continue;
            if(fa[v] != u && dfn[u] < dfn[v]) Build(u, v, G.w[i]);
        }
    }
}tar;
int main() {
    tot = n = ri(); int m = ri(), q = ri();
    for(int i = 1;i <= m; ++i) {
        int u = ri(), v = ri(), w = ri();
        tar.G.adds(u, v, w);
    }
    tar.dfs(1, 0); rst.dfs1(1, 0); rst.dfs2(1, 1);
    for(;q--;) printf("%d\n", rst.Query(ri(), ri()));
    return 0;
}

定义:广义圆方树

如果给你一张普通无向图怎么搞圆方树?
考虑我们刚才在仙人掌上的做法。事实上,一个环就是一个点双联通分量
这样子的话思路就很清楚了。求无向图的点双后对于每个点双都开一个方点,删除原来树上的所有边改为和方点连边,这就是广义圆方树。
在这里插入图片描述
这样建出来的树仍然能比较好地反应原图的性质。
同时我们发现圆圆边不见了。两个不在任何环内的点也会被视为一个点双,中间塞一个方点进去。
不过,有一个致命的缺点,这样的做法对于一个点双联通分量来说,边的信息丢失了,也就是说,广义圆方树并不是和图一一对应的,不过这并不妨碍广义圆方树具有的优美的性质和广泛的应用。

例3:luoguP4630 [APIO2018] Duathlon 铁人两项

题目大意:给定一张无向图,求存在简单路径 ( s , c ) , ( c , f ) (s,c),(c,f) (s,c),(c,f) ( s , c , f ) (s,c,f) (s,c,f)三元组个数。
这道题考场基环树骗分失败(第77个点WA了),至今不知道为什么。
考虑枚举 ( s , f ) (s,f) (s,f) c c c就是 ( s , f ) (s,f) (s,f)所有简单路径点集并的大小,假设为 ∣ V ( s , f ) ∣ |V_{(s,f)}| V(s,f)
这个时候圆方树就派上大用场了。考虑圆方树上的路径,两个圆点之间的 V V V实际上就是圆方树上路径每个方点代表的点双联通分量的点集的并。
一个很巧妙的方法是,一个方点权值设置为点双大小,这个时候发现,由于每个圆点连接的方点代表的联通块必定包含这个圆点,所以经过两个方点的时候这个圆点会被重复计算一次。如果把每个圆点的权值设为-1,那么两点之间的 ∣ V ∣ |V| V就是按照上述方法赋值的圆方树上两点间路径上的点权和。
问题转化为,求一颗圆方树上任意两个圆点树上路径的点权和。
考虑统计有多少条路径经过某个点,这显然是一个简单的Dp问题,可以 O ( n ) O(n) O(n)解决。

#include
const int N = 1e6 + 10;
int ri() {
    char c = getchar(); int x = 0, f = 1; for(;c < '0' || c > '9'; c = getchar()) if(c == '-') f  = -1;
    for(;c >= '0' && c <= '9'; c = getchar()) x = (x << 1) + (x << 3) - '0' + c; return x * f;
}
int tot, n, S, w[N]; long long A;
struct Edge {
    int to[N << 1], nx[N << 1], pr[N], tp;
    void add(int u, int v) {to[++tp] = v; nx[tp] = pr[u]; pr[u] = tp;}
    void adds(int u, int v) {add(u, v); add(v, u);} 
};
struct Round_Square_Tree {
    Edge T; int sz[N], w[N];
    void Work(int u, int fa) {
        if(u <= n) sz[u] = 1; long long r = 0;
        for(int i = T.pr[u], v; i; i = T.nx[i]) 
        if((v = T.to[i]) != fa)
            Work(v, u), r += (1LL * sz[u] * sz[v] << 1), sz[u] += sz[v];
        r += 1LL * sz[u] * (S - sz[u]) << 1; A += r * w[u];
    }
}rst;
struct Tarjan {
    Edge G; int dfn[N], low[N], st[N], tp, tm;
    void dfs(int u, int fa) {
        dfn[u] = low[u] = ++tm; rst.w[u] = -1; st[++tp] = u; ++S;
        for(int i = G.pr[u], v; i; i = G.nx[i]) 
        if((v = G.to[i]) != fa) {
            if(!dfn[v]) {
                dfs(v, u), low[u] = std::min(low[u], low[v]); 
                if(low[v] >= dfn[u])
                    for(rst.T.adds(u, ++tot), rst.w[tot] = 1; st[tp + 1] != v;)
                        rst.T.adds(st[tp--], tot), ++rst.w[tot];
            }
            else low[u] = std::min(low[u], dfn[v]);
        }
    }
}tar;
int main() {
    tot = n = ri(); int m = ri();
    for(;m--;) tar.G.adds(ri(), ri());
    for(int i = 1;i <= n; ++i) if(!tar.dfn[i]) S = 0, tar.dfs(i, 0), rst.Work(i, 0);
    printf("%lld\n", A);
    return 0;
}

小结

圆方树这个东西了不起的地方在于,将一张图转化成了一棵树,同时比较完整地反映了图的信息(仙人掌图可以做到一一对应),而解决树上的问题的出路显然比图多得多。
处理圆方树的问题要注意维护圆方边。让圆方边尽可能地体现原图上我们需要的信息。
还是一个很有用的,很好玩的算法。

你可能感兴趣的:(图论-Tarjan)