集训第一天,好累,果然停OI一个月还是不习惯
讨论圆方树之前,我们先来考虑如下定义:
点双:无向图的极大子图,使得该子图内无割点
边双,无向图的极大子图,使得该子图内无割边
易发现,一条边至多属于一个边双,一个点却可能不只属于一个点双
求割边:考虑到当边(u,v)满足dfn[u] < dfn[v]且low[v] > dfn[u],此时割掉(u,v),v及其子树为一个边双
易得将所有割边删去,原图即为若干边双
求割点类似,不过多讨论一种为根的情况即可
当发现 low[v] >= dfn[u]时,把v及v以上的点从栈中弹出,再加上u, 共同形成⼀个点双。
void tarjan(int u) { dfn[u] = low[u] = ++idx; st[++top] = u; for(int i = fir[u];i;i = e[i].next) { int v = e[i].to; if(!dfn[v]) { tarjan(v); low[u] = min(low[u],low[v]); if(low[v] >= dfn[u]) { ++cnt,a[n + cnt] = 1; tradde(u,n + cnt); int t = 0; while(t != v) { t = st[top--]; tradde(t,n + cnt); a[n + cnt]++; } } } else low[u] = min(low[u],dfn[v]); } }
圆方树
原图中的每个点为圆点。
现在将点双内部的边全部拆去,建立一个方点存储该点双内需要的信息,并将该方点与各个圆点相连
这样处理后,整个图就变成一颗树,且易发现,只有圆点和方点之间有边
因此就可以利用这些性质,做一些看似没有思路的题
CF487E :tourists : https://www.luogu.org/problemnew/show/CF487E
给出⼀张图,点有点权。每次询问两点之间的简单路径中,权值的
最⼩值最⼩是多少。带修。n, m, q ≤ 1000000
View Code
考虑建圆方树,每个方点存储该点双中权值最小值,树剖即可
[APIO2018]铁人两项
给出⼀个(不⼀定连通)的图,求有多少个三元组 (s, c, f) 满⾜ s, c, f 都是图中的点,且存在⼀条从s到c的路径和⼀条从c到f的路径,使得两条路径没有公共点(除c外)。三元组有序
考虑O(n^2)暴力,建圆方树,枚举圆点点对,这时计算他们路径上有多少个点即可
考虑到圆点会被计算两次,因此将方点的权值设为点双大小,圆点的权值设为-1,计算路径长度即可
考虑优化,我们可以尝试计算每个点在路径中被包含了多少次,即两种:
1.作为中转点:枚举每个子树v,则对答案贡献为sz[v] * (sum - (u <= n) - sz[v]) * a[u],这里a[u]为u的权值
2.作为起点或终点,此时该点必须为圆点,答案为(总节点数-1) * 2(起终点各为一遍)
另外再考虑u的子树以外的点的路径,则对答案贡献(sz[u] - (u <= n)) * (sum - sz[u])
注意:sz只计算圆点数,因此要特别考虑(可以综合上面式子理解)
#define O(x) cout << #x << " " << x << endl; #include<cstdio> #include<iostream> #include<cstring> #include<algorithm> #include<queue> using namespace std; typedef long long ll; inline int read() { int ans = 0,op = 1; char ch = getchar(); while(ch < '0' || ch > '9') { if(ch == '-') op = -1; ch = getchar(); } while(ch >= '0' && ch <= '9') { (ans *= 10) += ch - '0'; ch = getchar(); } return ans * op; } const int maxn = 4e5 + 5; struct egde { int to,next; }e[maxn],tr[maxn]; int head[maxn],tot; void tradde(int u,int v) { tr[++tot].next = head[u]; head[u] = tot; tr[tot].to = v; swap(u,v); tr[++tot].next = head[u]; head[u] = tot; tr[tot].to = v; } int fir[maxn],alloc; void adde(int u,int v) { e[++alloc].next = fir[u]; fir[u] = alloc; e[alloc].to = v; swap(u,v); e[++alloc].next = fir[u]; fir[u] = alloc; e[alloc].to = v; } int n,m; int low[maxn],st[maxn],dfn[maxn],a[maxn],idx,top,cnt; int sum; ll ans; int sz[maxn]; void tarjan(int u) { dfn[u] = low[u] = ++idx; st[++top] = u; for(int i = fir[u];i;i = e[i].next) { int v = e[i].to; if(!dfn[v]) { tarjan(v); low[u] = min(low[u],low[v]); if(low[v] >= dfn[u]) { ++cnt,a[n + cnt] = 1; tradde(u,n + cnt); int t = 0; while(t != v) { t = st[top--]; tradde(t,n + cnt); a[n + cnt]++; } } } else low[u] = min(low[u],dfn[v]); } } void dfs1(int u,int fa) { if(u <= n) sz[u] = 1; for(int i = head[u];i;i = tr[i].next) { int v = tr[i].to; if(v == fa) continue; dfs1(v,u); sz[u] += sz[v]; } } void dfs(int u,int fa) { ans = ans + 1ll * a[u] * (sz[u] - (a[u] == -1)) * (sum - sz[u]); for(int i = head[u];i;i = tr[i].next) { int v = tr[i].to; if(v == fa) continue; dfs(v,u); ans += 1ll * a[u] * sz[v] * (sum - (a[u] == -1) - sz[v]); } if(a[u] == -1) ans += 2ll * (sum - 1) * a[u]; } int main() { memset(a,-1,sizeof(a)); n = read(),m = read(); for(int i = 1;i <= m;i++) { int u = read(),v = read(); adde(u,v); } for(int i = 1;i <= n;i++) { if(!dfn[i]) { sum = 0; tarjan(i); dfs1(i,0); sum = sz[i]; dfs(i,0); } } printf("%lld",ans); }
总结:
据说圆方树还可以解决仙人掌问题,但是菜鸡也不知道啥是仙人掌...
感觉圆方树还有很多奇妙性质没有理解透彻,以后再巩固