圆方树
圆方树的定义
圆方树是用来解决仙人掌图的问题的,那什么是仙人掌图呢?
即不存在边同时属于多个环的无向连通图是一棵仙人掌。
点双连通分量的定义
要介绍圆方树,首先要介绍点双连通分量。
一个点双连通图的一个定义是:图中任意两不同点之间都有至少两条点不重复的路径。
一种简单的定义:不存在割点的图。
但这种定义对于两点一边的图时是没用的,它没有割点,但是并不能找到两条不相交的路径,因为只有一条路径。(也可以理解为那一条路径可以算两次,但的确没有相交,因为不经过其他点)。
在点双连通图内,一个点可能属于多个点双,但是一条边属于恰好一个点双。
更多关于有向图的强连通分量的知识,请看我的博客 强连通分量
更多关于点双连通分量的知识,请看我的博客 双连通分量
继续介绍圆方树
关于圆方树的建图,也比较简单,将一个点双连通分量内的所有边删去,再将一个点双连通分量中的每个点向一个新建的点连边,这个新建的点即是方点。
所以在圆方树中有 个点,其中 是原图点数, 是原图点双连通分量的个数。
每个点双都可以形成一个菊花图,多个菊花图通过原图中的割点连接在一起(因为点双的分隔点是割点)。
显然,圆方树中每条边连接一个圆点和一个方点。
在下面这张图中, 是圆点, 是方点。
而如果圆方树连通,则有以下性质:
-
方点之间不会存在连边。
-
原图的割点就是圆方树中度数大于 的圆点。
-
圆方数是一棵非常好的树,即点数等于边数加 。
-
圆方树上任意一条路径上圆点方点间隔分布。
-
如果圆点的 为 ,那么一个圆点子树的 和就是它下面的所有点的数量。
-
对于一个点双中的两点,它们之间简单路径的并集,恰好完全等于这个点双,即同一个点双中的两不同点 , 之间一定存在一条简单路径经过给定的在同一个点双内的另一点 。也就是说,考虑两圆点在圆方树上的路径,与路径上经过的方点相邻的圆点的集合,就等于原图中两点简单路径上的点集。
如果原图中某个连通分量只有一个点,则需要具体情况具体分析,我们在后续讨论中不考虑孤立点。
注意一条边连接两个点的在这里不算点双。
广义圆方树
普通圆方树只能解决仙人掌图上的问题,而广义圆方树则可以将所有无向图转化为圆方树处理。
广义圆方树性质:圆点方点相间,不存在两个‘’相同形状‘’的点相连。
与圆方树不同的是,广义圆方树需要把一条边连接两个点也看成一个点双,原本两个圆点有一条边相连,现在在中间插入一个方点间隔开就好了。
可以参照这张图
圆方树的应用
洛谷 P4320 道路相遇
题目大意
给定一无向图,现在 yzh
要从 点到处于 点的 cxr
家,求所有从 到 的路径中的必经点。
解题思路
介绍完上面的圆方树后,就会发现这题很简单,必经点个数其实就是两点之间割点的个数。
其实就是上面这条定理:
原图的割点就是圆方树中度数大于 的圆点。
得出,必经点数等于圆方树上两点路径上圆点数。
由于圆方树上任意一条路径上圆点方点间隔分布,所以需要除以 。
一条(从圆点到圆点的)树上简单路径代表什么?
代表原图中的一堆路径,其中:所有经过的割点(树上的圆点)都是必经的,而在点双内(树上的方点)可以随便走。
直接跑一边点双。
再建图。
然后跑一边树链剖分,记录深度和最近公共祖先。
对于每一组询问,直接用树上差分的知识求解就行了。
即设 为 和 的最近公共祖先,则 。
AC CODE
阅读时请省略前面的快读。
#include <bits/stdc++.h>
using namespace std;
const int _ = 2e6 + 5;
struct Fastio
{
template <typename T>
Fastio operator>>(T &x)
{
x = 0;
char c = getchar();
while (c < '0' || c > '9')
c = getchar();
while (c >= '0' && c <= '9')
x = (x << 3) + (x << 1) + (c ^ 48), c = getchar();
return *this;
}
Fastio &operator<<(const char *str)
{
int cur = 0;
while (str[cur])
putchar(str[cur++]);
return *this;
}
template <typename T>
Fastio &operator<<(T x)
{
if (x == 0)
{
putchar('0');
return *this;
}
if (x < 0)
putchar('-'), x = -x;
static int sta[45];
int top = 0;
while (x)
sta[++top] = x % 10, x /= 10;
while (top)
putchar(sta[top] + '0'), --top;
return *this;
}
} io;
int n, m, q, tp;
int cnt_node, cntn;
int dfn[_], low[_];
int dep[_], top[_], siz[_], hson[_], fa[_];
stack<int> s;
struct Graph
{
int tot, head[_], nxt[_ << 1], to[_ << 1];
void add(int u, int v)
{
nxt[++tot] = head[u];
to[tot] = v;
head[u] = tot;
nxt[++tot] = head[v];
to[tot] = u;
head[v] = tot;
}
} G, T;
void tarjan(int u)
{
dfn[u] = low[u] = ++cnt_node;
s.push(u);
for (int i = G.head[u], v; i; i = G.nxt[i])
{
v = G.to[i];
if (!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u])
{
T.add(++cntn, u);
while (1)
{
int now = s.top();
s.pop();
T.add(cntn, now);
if (now == v)
break;
}
}
}
else
low[u] = min(low[u], dfn[v]);
}
}
void dfs1(int u, int d = 1)
{
siz[u] = 1;
dep[u] = d;
for (int i = T.head[u], v; i; i = T.nxt[i])
{
v = T.to[i];
if (dep[v])
continue;
fa[v] = u;
dfs1(v, d + 1);
siz[u] += siz[v];
if (siz[v] > siz[hson[u]])
hson[u] = v;
}
}
void dfs2(int u, int topf = 1)
{
top[u] = topf;
if (!hson[u])
return;
dfs2(hson[u], topf);
for (int i = T.head[u], v; i; i = T.nxt[i])
{
v = T.to[i];
if (top[v])
continue;
dfs2(v, v);
}
}
int LCA(int x, int y)
{
while (top[x] != top[y])
{
if (dep[top[x]] < dep[top[y]])
swap(x, y);
x = fa[top[x]];
}
return dep[x] < dep[y] ? x : y;
}
signed main()
{
io >> n >> m;
cntn = n;
for (int i = 1, u, v; i <= m; i++)
{
io >> u >> v;
G.add(u, v);
}
tarjan(1);
dfs1(1);
dfs2(1);
io >> q;
while (q--)
{
int u, v;
io >> u >> v;
int lca = LCA(u, v);
io << (dep[u] + dep[v] - 2 * dep[lca]) / 2 + 1 << "\n";
}
return 0;
}
CF487E Tourists
题目大意
给定一张简单无向连通图,要求支持两种操作:
-
修改一个点的点权。
-
询问两点之间所有简单路径上点权的最小值。
解题思路
同样地,我们建出原图的圆方树,令方点权值为相邻圆点权值的最小值,问题转化为求路径上最小值。
路径最小值可以使用树链剖分和线段树维护,但是修改呢?
一次修改一个圆点的点权,需要修改所有和它相邻的方点,这样很容易被卡到 。
这时我们利用圆方树是棵很好的树的性质,令方点权值为自己的儿子圆点的权值最小值,这样的话修改时只需要修改父亲方点。
对于方点的维护,只需要对每个方点开一个 multiset
维护权值集合即可。(因为 multiset
支持实时排序和随机访问任意位置的值)。
需要注意的是查询时若 LCA
是方点,则还需要查 LCA
的父亲圆点的权值。
AC CODE
阅读时请省略快读。
题外话:为啥在洛谷上跑 CF
的题跑了整个下午 ?
#include <bits/stdc++.h>
using namespace std;
const int MN = 100005;
const int MS = 524288;
const int Inf = 0x7fffffff;
int N, M, Q, cnt;
int w[MN * 2];
vector<int> G[MN], T[MN * 2];
multiset<int> S[MN * 2];
int dfn[MN * 2], low[MN], dfc;
int stk[MN], tp;
void Tarjan(int u)
{
low[u] = dfn[u] = ++dfc;
stk[++tp] = u;
for (int v : G[u])
{
if (!dfn[v])
{
Tarjan(v);
low[u] = min(low[u], low[v]);
if (low[v] == dfn[u])
{
++cnt;
for (int x = 0; x != v; --tp)
{
x = stk[tp];
T[cnt].push_back(x);
T[x].push_back(cnt);
}
T[cnt].push_back(u);
T[u].push_back(cnt);
}
}
else
low[u] = min(low[u], dfn[v]);
}
}
int idf[MN * 2], fa[MN * 2], siz[MN * 2], dep[MN * 2], son[MN * 2],
top[MN * 2];
void DFS0(int u, int fz)
{
fa[u] = fz, dep[u] = dep[fz] + 1, siz[u] = 1;
for (int v : T[u])
if (v != fz)
{
DFS0(v, u);
siz[u] += siz[v];
if (siz[son[u]] < siz[v])
son[u] = v;
}
}
void DFS1(int u, int fz, int tp)
{
dfn[u] = ++dfc, idf[dfc] = u, top[u] = tp;
if (son[u])
DFS1(son[u], u, tp);
for (int v : T[u])
if (v != fz && v != son[u])
DFS1(v, u, v);
}
#define li (i << 1)
#define ri (i << 1 | 1)
#define mid ((l + r) >> 1)
#define ls li, l, mid
#define rs ri, mid + 1, r
int dat[MS];
void Build(int i, int l, int r)
{
if (l == r)
{
dat[i] = w[idf[l]];
return;
}
Build(ls), Build(rs);
dat[i] = min(dat[li], dat[ri]);
}
void update(int i, int l, int r, int p, int x)
{
if (l == r)
{
dat[i] = x;
return;
}
if (p <= mid)
update(ls, p, x);
else
update(rs, p, x);
dat[i] = min(dat[li], dat[ri]);
}
int query(int i, int l, int r, int a, int b)
{
if (r < a || b < l)
return Inf;
if (a <= l && r <= b)
return dat[i];
return min(query(ls, a, b), query(rs, a, b));
}
int main()
{
scanf("%d%d%d", &N, &M, &Q);
for (int i = 1; i <= N; ++i)
scanf("%d", &w[i]);
cnt = N;
for (int i = 1; i <= M; ++i)
{
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
Tarjan(1), DFS0(1, 0), dfc = 0, DFS1(1, 0, 1);
for (int i = 1; i <= N; ++i)
if (fa[i])
S[fa[i]].insert(w[i]);
for (int i = N + 1; i <= cnt; ++i)
w[i] = *S[i].begin();
Build(1, 1, cnt);
for (int q = 1; q <= Q; ++q)
{
char opt[3];
int x, y;
scanf("%s%d%d", opt, &x, &y);
if (*opt == 'C')
{
update(1, 1, cnt, dfn[x], y);
if (fa[x])
{
int u = fa[x];
S[u].erase(S[u].lower_bound(w[x]));
S[u].insert(y);
if (w[u] != *S[u].begin())
{
w[u] = *S[u].begin();
update(1, 1, cnt, dfn[u], w[u]);
}
}
w[x] = y;
}
else
{
int Ans = Inf;
while (top[x] != top[y])
{
if (dep[top[x]] < dep[top[y]])
swap(x, y);
Ans = min(Ans, query(1, 1, cnt, dfn[top[x]], dfn[x]));
x = fa[top[x]];
}
if (dfn[x] > dfn[y])
swap(x, y);
Ans = min(Ans, query(1, 1, cnt, dfn[x], dfn[y]));
if (x > N)
Ans = min(Ans, w[fa[x]]);
printf("%d\n", Ans);
}
}
return 0;
}
洛谷 P4630 [APIO2018] Duathlon 铁人两项
题目大意
给定一张简单无向图,问有多少对三元组 (,, 互不相同)使得存在一条简单路径从 出发,经过 到达 。
解题思路
考虑怎么计算这种三元组,可以枚举 和 ,然后计算从 到 的点不重复路径中可以经过的点的个数。
到 点不重复路径中可以经过的点必定在也只能在这条路径所经过的点双内。
所以可以考虑缩点双之后建出圆方树,然后就只需要在树上求出每一对 之间经过的点双点数大小。
但是直接将点双大小加起来的话两个点双的公共点会算重,于是将公共点的权值(圆点)设为 ,方点的权值设为点双的大小。
原因是路径中除端点外每个圆点(即割点)都会被相邻的两点双算两遍,而两端点虽然只被算一遍但本身并不能被统计,故每个点都需要减一。
那么问题又进一步转化成了求树上所有路径的权值和,显然只要分别计算每个点的贡献即可,就是一个简单的 DP
。
于是这题便转化成了求树上所有的圆点对之间的路径权值和。
直接做是 ,
可以换个角度考虑,改为统计每一个点对答案的贡献,即权值乘以经过它的路径条数,这可以通过简单的树形 DP
求出。
AC CODE
#include <bits/stdc++.h>
using namespace std;
const int _ = 100005;
int n, m, cnt;
vector<int> G[_], T[_ * 2];
long long Ans;
int dfn[_], low[_], cnt_node, num;
stack<int> s;
int wjy[_ * 2];
int vis[_ * 2], siz[_ * 2];
void Tarjan(int u)
{
low[u] = dfn[u] = ++cnt_node;
s.push(u);
++num;
for (auto v : G[u])
{
if (!dfn[v])
{
Tarjan(v);
low[u] = min(low[u], low[v]);
if (low[v] == dfn[u])
{
wjy[++cnt] = 0;
while (1)
{
int x = s.top();
s.pop();
T[cnt].push_back(x);
T[x].push_back(cnt);
++wjy[cnt];
if (x == v)
break;
}
T[cnt].push_back(u);
T[u].push_back(cnt);
++wjy[cnt];
}
}
else
low[u] = min(low[u], dfn[v]);
}
}
void dfs(int u, int fz)
{
vis[u] = 1;
siz[u] = (u <= n);
for (auto v : T[u])
if (v != fz)
{
dfs(v, u);
Ans += 2ll * wjy[u] * siz[u] * siz[v];
siz[u] += siz[v];
}
Ans += 2ll * wjy[u] * siz[u] * (num - siz[u]);
}
signed main()
{
scanf("%d%d", &n, &m);
cnt = n;
for (int i = 1; i <= n; ++i)
wjy[i] = -1;
for (int i = 1; i <= m; ++i)
{
int u, v;
scanf("%d%d", &u, &v);
G[u].push_back(v);
G[v].push_back(u);
}
for (int i = 1; i <= n; ++i)
if (!dfn[i])
{
num = 0;
Tarjan(i);
dfs(i, 0);
}
printf("%lld\n", Ans);
return 0;
}
洛谷 P4606 [SDOI2018]战略游戏
题目大意
给出一个无向图,和 个询问,每次给出 个点,问存在几个点,使得这个点和他相连的边被去除后,这 个点中,至少存在一对点互不相通。
解题思路
首先考虑删掉哪些点才能使得图上原本连通的两点变为不连通。
显然删掉两点的简单路径中必经的割点可以使得图上原本连通的两点变为不连通。
而这在圆方树上对应的就是两点路径上的圆点。
于是,我们轻松的想到一个办法: 直接找出所有的圆点不就好了?
然而,我们的时间复杂度是过不去的。
那么,如何快速求出所有圆点呢? 不妨换一种思路。 对于一个圆方树,如果我们能找到其包含所有点的最小的连通块,然后将其减掉 ,就是我们的圆点的数量。
例如这张图:
假设给出的 个点分别为:。
则建完圆方树就变成这样:
图中没加深的点就是圆方树中的方点。
易得,使用 Tarjan
算法建出圆方树,然后答案就是圆方树上包含所有关键点的最少点数联通块的圆点数量减去关键点的数量。
为了方便,我们设圆点的权值设为 ,方点的权值为 ,将点权放到这个点与其父节点的边上。
然后画一个图,发现,如果由 dfs
序从小到大,以此走过所有的点,然后再从第 个点走回第 个点。
在走过路径中,如果不考虑每相邻两个点的 LCA
(此时我们走的是树上最短路径,显然会经过 LCA
,这里说的不考虑就是不把它计入在内),每个点恰好被走了两次,而这些被走过的点恰好就是我们要求的联通块。
不过这样会有一个问题,就是第一个点和第 个点的 LCA
会不被统计,所以如果这个点是个圆点答案就再加 。
AC CODE
#include <bits/stdc++.h>
using namespace std;
const int _ = 100005;
int n, m, q, cnt;
vector<int> G[_], T[_ * 2];
int dfn[_ * 2], low[_], cnt_node;
stack<int> s;
void Tarjan(int u)
{
low[u] = dfn[u] = ++cnt_node;
s.push(u);
for (auto v : G[u])
{
if (!dfn[v])
{
Tarjan(v);
low[u] = min(low[u], low[v]);
if (low[v] == dfn[u])
{
++cnt;
while (1)
{
int x = s.top();
s.pop();
T[cnt].push_back(x);
T[x].push_back(cnt);
// cout << cnt << " " << x << endl;
if (x == v)
break;
}
T[cnt].push_back(u);
T[u].push_back(cnt);
// cout << cnt << " " << u << endl;
}
}
else
low[u] = min(low[u], dfn[v]);
}
}
int dep[_ * 2], fa[_ * 2][18], dis[_ * 2];
void dfs(int u, int fz)
{
dfn[u] = ++cnt_node;
dep[u] = dep[fa[u][0] = fz] + 1;
dis[u] = dis[fz] + (u <= n);
for (int j = 0; j < 17; ++j)
fa[u][j + 1] = fa[fa[u][j]][j];
for (auto v : T[u])
if (v != fz)
dfs(v, u);
}
int LCA(int x, int y)
{
if (dep[x] < dep[y])
swap(x, y);
for (int j = 0, d = dep[x] - dep[y]; d; ++j, d >>= 1)
if (d & 1)
x = fa[x][j];
if (x == y)
return x;
for (int j = 17; ~j; --j)
if (fa[x][j] != fa[y][j])
x = fa[x][j], y = fa[y][j];
return fa[x][0];
}
int main()
{
int TT;
scanf("%d", &TT);
while (TT--)
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
{
G[i].clear();
dfn[i] = low[i] = 0;
}
for (int i = 1; i <= n * 2; ++i)
T[i].clear();
for (int i = 1, x, y; i <= m; ++i)
{
scanf("%d%d", &x, &y);
G[x].push_back(y);
G[y].push_back(x);
}
cnt = n;
cnt_node = 0;
Tarjan(1);
cnt_node = 0;
dfs(1, 0);
scanf("%d", &q);
while (q--)
{
int S, A[_];
scanf("%d", &S);
int Ans = -2 * S;
for (int i = 1; i <= S; ++i)
scanf("%d", &A[i]);
sort(A + 1, A + S + 1, [](int i, int j)
{ return dfn[i] < dfn[j]; });
for (int i = 1; i <= S; ++i)
{
int u = A[i], v = A[i % S + 1];
Ans += dis[u] + dis[v] - 2 * dis[LCA(u, v)];
}
if (LCA(A[1], A[S]) <= n)
Ans += 2;
printf("%d\n", Ans / 2);
}
}
return 0;
}
本文来自博客园,作者:蒟蒻orz,转载请注明原文链接:https://www.cnblogs.com/orzz/p/18122138