「学习笔记」圆方树
圆方树最初是处理「仙人掌图」(每条边在不超过一个简单环中的无向图)的一种工具,不过发掘它的更多性质,有时我们可以在一般无向图上使用它。
个人觉得,圆方树是一个很好的工具。圆方树的题目更多的侧重于想,而不是怎么建圆方树。
前置知识——点双连通分量
点双连通分量:不存在割点的图。
一个点双连通分量在圆方树上,就是一个方点,而点双连通分量中的原来的点就是圆点。方点连接着圆点,每一个点双都形成了一个菊花图,多个菊花图靠着割点连接在一起构成一棵树,或许,这就是圆方树名称的由来。
「学习笔记」双连通分量、割点与桥 - yi_fan0305 - 博客园 (cnblogs.com)
如果原图联通,则会形成一棵圆方树,否则会形成一片圆方树森林。
下面是一个图对应的点双和圆方树形态。
过程
tarjan 的代码
void tarjan(int u, int fa) {
dfn[u] = low[u] = ++ tim;
st.emplace_back(u);
for (pii it : e[u]) {
int v = it.first, i = it.second;
if (i == fa) continue ;
if (!dfn[v]) {
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
++ tot;
int x;
do {
x = st.back();
st.pop_back();
ne[x].emplace_back(tot);
ne[tot].emplace_back(x);
} while (x != v);
ne[tot].emplace_back(u);
ne[u].emplace_back(tot);
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
其中,ne
是新图的边,e
是原图的边。
性质
-
不存在圆点-圆点边以及方点-方点边,即与圆点相连的边都是方点,与方点相连的边都是圆点。
-
圆方树一定是一棵树。
-
如果原图连通,构建出的圆方树也一定连通。
树具有许多良好的性质,我们可以用圆方树将仙人掌等无向图上的问题转化为树上问题。
注意
一定要开两倍空间!
由于方点的出现,点的个数肯定大于等于 \(n\),因此,开空间时要开两倍空间。
在一些情况下(例如统计子树大小),方点不能算作是一个实点(即 siz = 0
)。
题目
P4630 [APIO2018] 铁人两项 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
统计方案数,在圆方树上进行组合数计算。
//The code was written by yifan, and yifan is neutral!!!
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define bug puts("NOIP rp ++!");
template<typename T>
inline T read() {
T x = 0;
bool fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
const int N = 1e5 + 5;
using pii = pair<int, int>;
int n, m, tim, tot;
ll ans;
int dfn[N], low[N];
int siz1[N << 1], siz2[N << 1], fa[N << 1];
vector<pii> e[N];
vector<int> st, ne[N << 1];
void tarjan(int u, int fa) { // 找点双、建圆方树
dfn[u] = low[u] = ++ tim;
st.emplace_back(u);
for (pii it : e[u]) {
int v = it.first, i = it.second;
if (i == fa) continue ;
if (! dfn[v]) {
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
int x = 0;
++ tot;
do {
x = st.back();
st.pop_back();
ne[tot].emplace_back(x);
ne[x].emplace_back(tot);
} while (x != v);
ne[tot].emplace_back(u);
ne[u].emplace_back(tot);
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
void dfs1(int u) { // 找以 u 为根的子树大小
siz1[u] = (u <= n ? 1 : 0);
for (int v : ne[u]) {
if (v == fa[u]) continue ;
fa[v] = u;
dfs1(v);
siz1[u] += siz1[v];
}
}
void dfs2(int u) { // 更新以 u 为根的父节点及往上的节点个数(不包括 u)
if (fa[u]) {
siz2[u] = siz2[fa[u]] + siz1[fa[u]] - siz1[u];
}
for (int v : ne[u]) {
if (v == fa[u]) continue ;
dfs2(v);
}
int sum = siz1[u] + siz2[u];
if (u <= n) {
for (int v : ne[u]) {
if (v == fa[u]) continue ;
ans += (1ll * sum - siz1[v] - 1) * siz1[v];
}
ans += (1ll * sum - siz2[u] - 1) * siz2[u];
} else {
for (int v : ne[u]) {
if (v == fa[u]) continue ;
ans += (1ll * sum - siz1[v]) * siz1[v] * (ne[u].size() - 2); // 两个点在同一个点双中的情况
}
ans += (1ll * sum - siz2[u]) * siz2[u] * (ne[u].size() - 2); // 两个点有一个在点双中,要处理割点
}
}
int main() {
n = read<int>(), m = read<int>();
tot = n;
for (int i = 1, u, v; i <= m; ++ i) {
u = read<int>(), v = read<int>();
e[u].emplace_back(v, i);
e[v].emplace_back(u, i);
}
for (int i = 1; i <= n; ++ i) {
if (!dfn[i]) {
tarjan(i, 0);
}
}
for (int i = 1; i <= tot; ++ i) {
if (! siz1[i]) {
dfs1(i);
}
}
for (int i = 1; i <= tot; ++ i) {
if (! siz2[i]) {
dfs2(i);
}
}
cout << ans;
return 0;
}
Tourists - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
每一个方点维护一个 multiset
,圆方树 + 树链剖分。
//The code was written by yifan, and yifan is neutral!!!
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define bug puts("NOIP rp ++!");
#define mid ((l + r) >> 1)
template<typename T>
inline T read() {
T x = 0;
bool fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
using pii = pair<int, int>;
const int N = 1e5 + 5;
int n, m, q;
int tim, tot;
int dfn[N << 1], low[N];
int dep[N << 1], fa[N << 1], son[N << 1], siz[N << 1];
int tp[N << 1], pos[N << 1];
ll w[N << 1];
vector<pii> e[N];
vector<int> ne[N << 1], st;
multiset<ll> s[N];
struct seg {
ll minn;
} t[N << 3];
seg operator + (const seg& a, const seg& b) {
seg res;
res.minn = min(a.minn, b.minn);
return res;
}
inline int ls(int u) {
return (u << 1);
}
inline int rs(int u) {
return (u << 1 | 1);
}
void tarjan(int u, int fa) {
dfn[u] = low[u] = ++ tim;
st.emplace_back(u);
for (pii it : e[u]) {
int v = it.first, i = it.second;
if (i == fa) continue ;
if (!dfn[v]) {
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
++ tot;
int x;
do {
x = st.back();
st.pop_back();
ne[x].emplace_back(tot);
ne[tot].emplace_back(x);
} while (x != v);
ne[tot].emplace_back(u);
ne[u].emplace_back(tot);
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
void dfs(int u, int fat) {
dep[u] = dep[fat] + 1;
fa[u] = fat;
siz[u] = 1;
for (int v : ne[u]) {
if (v == fat) continue ;
dfs(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) {
son[u] = v;
}
}
}
void gettop(int u, int Top) {
tp[u] = Top;
dfn[u] = ++ tim;
pos[tim] = u;
if (! son[u]) return ;
gettop(son[u], Top);
for (int v : ne[u]) {
if (v == fa[u] || v == son[u]) continue ;
gettop(v, v);
}
}
void build(int u, int l, int r) {
if (l == r) {
t[u].minn = w[pos[l]];
return ;
}
build(ls(u), l, mid);
build(rs(u), mid + 1, r);
t[u] = t[ls(u)] + t[rs(u)];
}
void modify(int u, int l, int r, int pos, ll v) {
if (l == r) {
t[u].minn = v;
return ;
}
if (pos <= mid) {
modify(ls(u), l, mid, pos, v);
} else {
modify(rs(u), mid + 1, r, pos, v);
}
t[u] = t[ls(u)] + t[rs(u)];
}
void Modify(int u, ll v) {
modify(1, 1, tot, dfn[u], v);
}
ll query(int u, int l, int r, int lr, int rr) {
if (lr <= l && r <= rr) {
return t[u].minn;
}
ll ans = 1e18, res;
if (lr <= mid) {
res = query(ls(u), l, mid, lr, rr);
ans = min(ans, res);
}
if (rr > mid) {
res = query(rs(u), mid + 1, r, lr, rr);
ans = min(ans, res);
}
return ans;
}
ll Query(int x, int y) {
ll ans = 1e18, res;
while (tp[x] != tp[y]) {
if (dep[tp[x]] < dep[tp[y]]) {
swap(x, y);
}
res = query(1, 1, tot, dfn[tp[x]], dfn[x]);
ans = min(ans, res);
x = fa[tp[x]];
}
if (dep[x] > dep[y]) {
swap(x, y);
}
res = query(1, 1, tot, dfn[x], dfn[y]);
ans = min(ans, res);
if (x > n) {
ans = min(ans, w[fa[x]]);
}
return ans;
}
int main() {
n = read<int>(), m = read<int>(), q = read<int>();
tot = n;
for (int i = 1; i <= n; ++ i) {
w[i] = read<ll>();
}
for (int i = 1, x, y; i <= m; ++ i) {
x = read<int>(), y = read<int>();
e[x].emplace_back(y, i);
e[y].emplace_back(x, i);
}
for (int i = 1; i <= n; ++ i) {
if (!dfn[i]) {
tarjan(i, 0);
}
}
tim = 0;
for (int i = 1; i <= tot; ++ i) {
dfn[i] = 0;
}
dfs(1, 0);
gettop(1, 1);
for (int i = 2; i <= n; ++ i) {
s[fa[i] - n].emplace(w[i]);
}
for (int i = n + 1; i <= tot; ++ i) {
w[i] = (s[i - n].empty() ? 1e18 : *s[i - n].begin());
}
build(1, 1, tot);
for (int i = 1, a, b; i <= q; ++ i) {
char opt[2];
scanf("%s", opt);
a = read<int>(), b = read<int>();
if (opt[0] == 'C') {
Modify(a, b);
if (fa[a]) {
int fat = fa[a];
s[fat - n].erase(s[fat - n].lower_bound(w[a]));
s[fat - n].emplace(b);
int minv = (*s[fa[a] - n].begin());
if (w[fat] != minv) {
w[fat] = minv;
Modify(fat, w[fat]);
}
}
w[a] = b;
} else {
ll ans = Query(a, b);
cout << ans << '\n';
}
}
return 0;
}
P4606 [SDOI2018] 战略游戏 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
查找圆方树上包含所有关键点的最少点数联通块的圆点数量减去关键点的数量。
多测数据,注意清空。
//The code was written by yifan, and yifan is neutral!!!
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define bug puts("NOIP rp ++!");
template<typename T>
inline T read() {
T x = 0;
bool fg = 0;
char ch = getchar();
while (ch < '0' || ch > '9') {
fg |= (ch == '-');
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = (x << 3) + (x << 1) + (ch ^ 48);
ch = getchar();
}
return fg ? ~x + 1 : x;
}
using pii = pair<int, int>;
const int N = 2e5 + 5;
int T, n, m, tim, tot, q;
int dfn[N << 1], low[N], lg[N << 1];
int dep[N << 1], siz[N << 1], son[N << 1], fa[N << 1];
int dis[N << 1];
int tp[N << 1];
int sk[N << 1];
vector<pii> e[N];
vector<int> ne[N << 1], st;
void tarjan(int u, int fa) {
dfn[u] = low[u] = ++ tim;
st.emplace_back(u);
for (pii it : e[u]) {
int v = it.first, i = it.second;
if (i == fa) continue ;
if (! dfn[v]) {
tarjan(v, i);
low[u] = min(low[u], low[v]);
if (low[v] >= dfn[u]) {
++ tot;
int x;
do {
x = st.back();
st.pop_back();
ne[x].emplace_back(tot);
ne[tot].emplace_back(x);
} while (x != v);
ne[tot].emplace_back(u);
ne[u].emplace_back(tot);
}
} else {
low[u] = min(low[u], dfn[v]);
}
}
}
void dfs(int u, int fat) {
dfn[u] = ++ tim;
siz[u] = 1;
dis[u] = dis[fat] + (u <= n);
fa[u] = fat;
dep[u] = dep[fat] + 1;
for (int v : ne[u]) {
if (v == fat) continue ;
dfs(v, u);
siz[u] += siz[v];
if (siz[v] > siz[son[u]]) {
son[u] = v;
}
}
}
void gettop(int u, int Top) {
tp[u] = Top;
if (!son[u]) return ;
gettop(son[u], Top);
for (int v : ne[u]) {
if (v == fa[u] || v == son[u]) continue ;
gettop(v, v);
}
}
int Lca(int x, int y) {
while (tp[x] != tp[y]) {
if (dep[tp[x]] < dep[tp[y]]) {
swap(x, y);
}
x = fa[tp[x]];
}
if (dep[x] > dep[y]) swap(x, y);
return x;
}
int Dis(int x, int y) {
return dis[x] + dis[y] - 2 * dis[Lca(x, y)];
}
void solve() {
n = read<int>(), m = read<int>();
tot = n;
for (int i = 1, x, y; i <= m; ++ i) {
x = read<int>(), y = read<int>();
e[x].emplace_back(y, i);
e[y].emplace_back(x, i);
}
for (int i = 1; i <= n; ++ i) {
if (! dfn[i]) {
tarjan(i, 0);
}
}
for (int i = 1; i <= tot; ++ i) {
dfn[i] = 0;
}
dis[1] = 0;
dep[0] = 0;
tim = 0;
dfs(1, 0);
gettop(1, 1);
q = read<int>();
while (q --) {
int k = read<int>();
for (int i = 1; i <= k; ++ i) {
sk[i] = read<int>();
}
sort(sk + 1, sk + k + 1, [](int x, int y) {
return dfn[x] < dfn[y];
});
int ans = 0;
sk[k + 1] = sk[1];
for (int i = 1; i <= k; ++ i) {
ans += Dis(sk[i], sk[i + 1]);
}
ans >>= 1;
ans -= k;
ans += (Lca(sk[1], sk[k]) <= n);
cout << ans;
putchar('\n');
}
}
void init() {
for (int i = 0; i <= n; ++ i) {
e[i].clear();
low[i] = 0;
}
for (int i = 1; i <= tot; ++ i) {
ne[i].clear();
dfn[i] = 0;
dep[i] = siz[i] = 0;
son[i] = dis[i] = 0;
fa[i] = sk[i] = 0;
tp[i] = 0;
}
tim = 0;
st.clear();
}
int main() {
T = read<int>();
while (T --) {
solve();
init();
}
return 0;
}