LGP4630 [APIO 2018] 铁人两项 学习笔记
LGP4630 [APIO 2018] 铁人两项 学习笔记
前言
从这题入门圆方树吧。
题意简述
给你一个 \(n\) 点 \(m\) 边的无向无权图。我们规定一个三元组 \((s,c,f)\) 合法,当且仅当 \(s,c,f\) 两两不同,且存在一条 \(s\to c\to f\) 的路径满足经过每个点至多一次。问有多少合法三元组。
本题保证无自环或重边。\(n\le 10^5\)。
做法解析
这是一个什么问题呢?这是一个无向图上的、对于一定范围内的所有路径(或者说较为多的连通性)有所询问、而且数据范围不接受 \(n^2\) 的问题。再加上这道题有一个“经过每个点至多一次”,考虑圆方树。
圆方树是什么?众所周知,对一个(连通的)无向图求点双后按点双缩点,会缩出一棵树。而圆方树约等于是往这棵树上挂上了原图的结点所形成。具体来说,圆方树由原图的所有结点(称为圆点)与所有代表点双的点(称为方点)组成,圆点与其所在点双的方点连边。
但我们为什么需要圆方树呢?因为 \(s\to f\) 的所有路径的并等于 \(s\to f\) 的所有点双的并,所以这个问题等价于我们要统计每一对 \((s,f)\) 其间路径上所有点双的并的大小。而圆方树相当于对点双缩点结构的提炼。
证明:
从 \(s\) 走到 \(f\) 的路径肯定都得从 \(s\) 所在的点双 \(u\) 走到 \(f\) 所在的点双 \(v\),而又因为点双缩点后整个图的结构是一棵树,且一个点只能经过一次,所以 \(s\to f\) 的路径要是跑到了缩点后 \(u\to v\) 简单路径的外面,那想绕回来就要重复经过一个割点两次,不合法。这意味着合法的 \(s\to f\) 路径的点可以且只可以经过 \(u\to v\) 路径上所有点双。
考虑构造:令方点权值为其所代表的点双的大小,圆点权值为 \(-1\)(为了去除重复计算的割点),这样子,这对 \((s,f)\) 的答案就是在圆方树上 \(s\to f\) 简单路径的权值和了(\(s\) 和 \(f\) 本身不应该算进去,不过这个刚好被 \(s\) 和 \(f\) 自己 \(-1\) 的权值去重了)。
但是我们要算的是“每一对 \((s,f)\)”的答案。怎么办呢?答案是做一遍 \(\text{dfs}\),详见下文。
代码实现
由定义可以看出,圆方树的大小显然不超过 \(2n\)。所以数组开两倍。
csbn
是用来记录原图上当前极大连通块的大小的。因为题目并没有保证全图连通。
另外,我们来分析一下这段 \(\text{dfs}\) 到底如何工作。
void dfs(int u,int f){
siz[u]=(u<=N);
for(int v : Gr2[u]){
if(v==f)continue;dfs(v,u);
ans+=2ll*siz[u]*siz[v]*val[u],siz[u]+=siz[v];
}
ans+=2ll*siz[u]*(csbn-siz[u])*val[u];
}
\(\text{dfs}\) 相当于把这棵树当成有根了的,下文方便叙述一些。
首先,这个 2ll
的系数是因为对于每一对 \((s,c,f)\),也有一对 \((f,c,s)\) 可以作为答案。
然后这个 siz[u]
显然记的是 \(u\) 子树内圆点的数量。
ans+=2ll*siz[u]*siz[v]*val[u],siz[u]+=siz[v];
这一行显然是在计算形如 \((v_1,u,v_2)\) 类的答案,而 ans+=2ll*siz[u]*(csbn-siz[u])*val[u];
是在计算形如 \((v,u,anc)\) 与 \((anc,u,v)\) 类的答案。
乘上的这个系数 val[u]
,作为方点的时候其正确性很好理解,毕竟既然要经过当前这个方点,那它统辖的圆点肯定都能作为 \(c\),所以要有这个系数。不过圆点的 val[u]
是 -1
,这个容斥系数是因为一个割点可能同时属于多个点双,如果两种方案的区别仅仅在于统计时某个圆点所属的方点不同,那就要去重。
#include <bits/stdc++.h>
using namespace std;
using namespace obasic;
const int MaxN=1e5+5;
int N,M,X,Y;lolo ans;
vector<int> Gr1[MaxN];
void addudge1(int u,int v){
Gr1[u].push_back(v);
Gr1[v].push_back(u);
}
int dfn[MaxN],low[MaxN],tot,csbn;
int stk[MaxN],ktp,nln,val[MaxN<<1];
vector<int> Gr2[MaxN<<1];
void addudge2(int u,int v){
Gr2[u].push_back(v);
Gr2[v].push_back(u);
}
void tarjan(int u,int f){
dfn[u]=low[u]=++tot,val[u]=-1;
stk[++ktp]=u,csbn++;int vc=0;
for(int v : Gr1[u]){
if(!dfn[v]){
vc++,tarjan(v,u);
minner(low[u],low[v]);
if(low[v]>=dfn[u]){
nln++;
while(stk[ktp+1]!=v)addudge2(stk[ktp--],nln),val[nln]++;
addudge2(u,nln),val[nln]++;
}
}
else if(v!=f)minner(low[u],dfn[v]);
}
if(!f&&!vc)val[++nln]++,addudge2(u,nln);
}
int siz[MaxN<<1];
void dfs(int u,int f){
siz[u]=(u<=N);
for(int v : Gr2[u]){
if(v==f)continue;dfs(v,u);
ans+=2ll*siz[u]*siz[v]*val[u],siz[u]+=siz[v];
}
ans+=2ll*siz[u]*(csbn-siz[u])*val[u];
}
int main(){
readis(N,M);nln=N;
for(int i=1;i<=M;i++)readis(X,Y),addudge1(X,Y);
for(int i=1;i<=N;i++)if(!dfn[i])ktp=csbn=0,tarjan(i,0),dfs(i,0);
writi(ans);
return 0;
}