一直觉得仙人掌和圆方树是非常高深的算法。
直到连续随机跳题跳到两道。我受不了啦!!!
于是点进了一个链接。(传销现场有木有啊!)
然后发现并没有想象中那么难。
而且,,,
很好玩。
日常赛前学算法(大雾
退役之前还是多多益善一波吧,错过了可能就遇不上了!
任意一条边至多在一个环里的无向联通图
严格的定义总是格外地难懂啊。
简单地说,把除了环上的所有边保留。对于每个环,新建一个方点,把所有环上的点与这个方点连边,删除原有的环上的边。
如何证明圆方树是一棵树?
首先显然,它仍然联通。
其次,对于每个环,新建的边数等于删除的边数,点数多了一个。
所以最后一定是一棵树。
构造就是 T a r j a n Tarjan Tarjan缩环啊。
性质:
最后一条性质看上去很拗口。实际上,它想表达的意思是,仙人掌并不改变原图任意两点之间的连通性。这里的连通性包括路径信息,连边情况等。
题目大意:给定一个仙人掌,求最大独立集。
这道题其实并不需要真正建立出圆方树。只需要在 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;
}
题目大意:求仙人掌上任意两点最短路,没有负边权。
这题必须建出圆方树。
考虑边权。圆圆边不变,圆方边定义为这个节点到环顶的最短距离(两个方向比一下)。
这样,如果穿过了某个环,考虑两种情况。
其余情况不变,直接跑 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;
}
如果给你一张普通无向图怎么搞圆方树?
考虑我们刚才在仙人掌上的做法。事实上,一个环就是一个点双联通分量。
这样子的话思路就很清楚了。求无向图的点双后对于每个点双都开一个方点,删除原来树上的所有边改为和方点连边,这就是广义圆方树。
这样建出来的树仍然能比较好地反应原图的性质。
同时我们发现圆圆边不见了。两个不在任何环内的点也会被视为一个点双,中间塞一个方点进去。
不过,有一个致命的缺点,这样的做法对于一个点双联通分量来说,边的信息丢失了,也就是说,广义圆方树并不是和图一一对应的,不过这并不妨碍广义圆方树具有的优美的性质和广泛的应用。
题目大意:给定一张无向图,求存在简单路径 ( 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;
}
圆方树这个东西了不起的地方在于,将一张图转化成了一棵树,同时比较完整地反映了图的信息(仙人掌图可以做到一一对应),而解决树上的问题的出路显然比图多得多。
处理圆方树的问题要注意维护圆方边。让圆方边尽可能地体现原图上我们需要的信息。
还是一个很有用的,很好玩的算法。