(Tips : 若无需“导读”可以直接往下翻至1.4节)
我们不妨先回顾一下,我们有什么方法把一张任意的有向图变成有向无环图(DAG)的?
比较熟悉的方法就是tarjan
缩点。
所以我们对于无向图,我们也先缩点——把点双连通分量缩在一起。
特别注意,我们要把只有一条边,只连接 2 2 2个点的“伪点双”也要算进去。
即使把点双缩起来了,我们得到的依然是一张无向图——那怎么把它统领成树呢?很容易想到就是对于这个缩好之后的点新建一个点,我们称之为方点。
那么对应的缩点前原图的点就称之为圆点。
这时图就显得很杂乱了,为了保证这个树建出来有用,我们需要整理一下,具体方案如下:
简而言之,记原图点为圆点,建立圆方树就是找出每一个点双,然后建一个对应这个点双的方点,方点连向每一个点双内的圆点,删去每个点双内部圆点原有的连边。这样每一个点双就成为了一个树的形态,这样所有的都连起来也就是一颗树了。
(这里特别解释一下怎么“全部连起来”。实际上并不需要额外的操作。就是因为我们把只有 2 2 2个点 1 1 1条边的也算进去,因此相邻点双之间都会有公共点。然后两个点双就会是以“方点 1 → _1 \to 1→ 公共点 → \to → 方点 2 _2 2 ”的顺序自然连接。)
这个圆方树肯定有一些特别的性质嘛,不然要了干什么……
- 对于任意无向图 G = { V , E } G=\{V , E\} G={V,E}均满足建出来的是森林;
- 对于任意连通无向图 G G G均满足建出来的是一颗无根树;
- 对于每一个圆点,都对应着有且仅有一个方点;
- 对于任意一条路径,路径上的圆点和方点都是相间出现,或者说同种形状的点不相邻。
- 原图的割点是圆方树中度数 > 1 >1 >1的圆点。
这些性质都是很显然的,无需过多证明。第 4 4 4个性质是之前多次强调的 2 2 2个点 1 1 1条边的特殊点双保证的。
以上为理论部分,实现上还有一些细节需要注意的。
tarjan
求点双+维护圆方树代码如下:void dfs(int u,int fa){//tarjan
dfn[u]=low[u]=++ind;
w[u]=-1;sz[u]=1;
q.push(u);
for(edge *i=head[u];i!=NULL;i=i->nxt)
if(i->v!=fa){
int v=i->v;
if(!dfn[v]){
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
w[++col]=1;
int k;
do{
k=q.top();q.pop();
c[col].push_back(k);
sz[col]+=sz[k];
w[col]++;
}while(k!=v);
sz[u]+=sz[col];
c[u].push_back(col);
}
}
else
low[u]=min(low[u],dfn[v]);
}
}
其中vector
存的是每个点的子节点(包括圆点和方点), c o l col col存的是点双的编号。方便起见,我们从 n + 1 n+1 n+1开始计算,这样就不会和圆点产生混淆,又可以统一地维护(即初始时col=n
)。
讲了这么多,我们再来看一看例题——[APIO2018] Duathlon 铁人两项.
先变成圆方树,然后尝试赋点权。
我们发现,如果说把圆点点权赋为 − 1 -1 −1,方点为点双大小,那么就转化成(圆方树上)统计所有路径上的点权和。
然后令 c n t i cnt_i cnti表示每个点可以被多少条路径覆盖,答案
a n s = ∑ i n w i × c n t i ans=\displaystyle\sum_{i}^{n} w_i\times cnt_i ans=i∑nwi×cnti
然后 d f s + d p dfs+dp dfs+dp即可。代码如下:
#include
#include
#include
#include
using std::vector;
using std::stack;
const int MAXN=200010;
struct edge{
int v;
edge *nxt;
edge(){
v=0;
nxt=NULL;
}
}*head[MAXN];
void adde(int u,int v){
edge *p=new edge;
p->v=v;
p->nxt=head[u];
head[u]=p;
}
vector<int>c[MAXN];
stack<int>q;
int n,m;
int dfn[MAXN],low[MAXN],ind=0;
int sz[MAXN],w[MAXN],col=0;
inline int min(int x,int y){
return x<y?x:y;
}
inline int max(int x,int y){
return x>y?x:y;
}
void dfs(int u,int fa){
dfn[u]=low[u]=++ind;
w[u]=-1;sz[u]=1;
q.push(u);
for(edge *i=head[u];i!=NULL;i=i->nxt)
if(i->v!=fa){
int v=i->v;
if(!dfn[v]){
dfs(v,u);
low[u]=min(low[u],low[v]);
if(low[v]>=dfn[u]){
w[++col]=1;
int k;
do{
k=q.top();q.pop();
c[col].push_back(k);
sz[col]+=sz[k];
w[col]++;
}while(k!=v);
sz[u]+=sz[col];
c[u].push_back(col);
}
}
else
low[u]=min(low[u],dfn[v]);
}
}
long long ans=0,sum=0;
void treedp(int u){
int tmp=(u<=n);
for(int i=0;i<c[u].size();i++){
int v=c[u][i];treedp(v);
ans+=tmp*(long long)sz[v]*w[u];
tmp+=sz[v];
}
ans+=sz[u]*(long long)(sum-sz[u])*w[u];//roate
}
int main(){
scanf("%d%d",&n,&m);col=n;
for(int i=1;i<=m;i++){
int u,v;scanf("%d%d",&u,&v);
adde(u,v);adde(v,u);
}
for(int i=1;i<=n;i++)
if(!dfn[i]){
dfs(i,0);
sum=sz[i];
treedp(i);
}
printf("%lld\n",ans<<1);
return 0;
}
图论新技能:圆方树实现图转树。
注意细节吧。