题目
题目大意
有一棵树,每条边有
0
0
0或
1
1
1两种颜色。
求满足存在
(
u
,
v
)
(u,v)
(u,v)路径上的点
x
x
x使得
(
u
,
x
)
(u,x)
(u,x)和
(
x
,
v
)
(x,v)
(x,v)路径上的两种颜色出现次数相同的点对
(
u
,
v
)
(u,v)
(u,v)数量。
思考历程
在看到这题之前我就已经知道这题是点分治了……
然而看了题目后,很长一段时间搞错了题目大意……
后来终于搞懂了题目大意,于是开始想正解。
想了半天,心态崩了……点分治我在差不多一年前才打过一题啊!
很久后搜题解,粗略看一看,没有一个看得懂……
于是又开始自己刚……然后刚出来了……
结果WA了……
调了不知道多久,终于AC……
正解
正解就是点分治啦……
显然的思路是将
0
0
0颜色的边权值记为
1
1
1,
1
1
1颜色的边权值记为
−
1
-1
−1。
如果
(
u
,
v
)
(u,v)
(u,v)满足条件,一定有点
x
x
x使得
(
u
,
x
)
(u,x)
(u,x)和
(
x
,
v
)
(x,v)
(x,v)路径上的权值和为
0
0
0。
现在设重心为
r
o
o
t
root
root,现在我们要求的路径必须经过
r
o
o
t
root
root。
求出每个点到
r
o
o
t
root
root路径上的权值和
d
i
s
x
dis_x
disx。
显然,如果
(
u
,
v
)
(u,v)
(u,v)满足条件,一定有
d
i
s
u
+
d
i
s
v
=
0
dis_u+dis_v=0
disu+disv=0。
还有一个条件是存在
u
u
u或
v
v
v的祖先
x
x
x,使得
d
i
s
x
=
d
i
s
u
dis_x=dis_u
disx=disu或
d
i
s
x
=
d
i
s
v
dis_x=dis_v
disx=disv。
然后我们就搬出几个桶……
设
t
o
t
i
tot_i
toti表示
d
i
s
dis
dis值为
i
i
i的点的数量,
c
a
n
i
can_i
cani表示
d
i
s
dis
dis值为
i
i
i并且其祖先有
d
i
s
dis
dis值和它相同的点的数量。这两个都是前面计算过的
r
o
o
t
root
root的所有子树的。同理,设
s
u
b
i
sub_i
subi和
s
c
n
i
scn_i
scni,意义相同,表示现在正在计算的这个子树的。
s
u
b
i
sub_i
subi的计算方法显然,至于
s
c
n
i
scn_i
scni,我们在
d
f
s
dfs
dfs的过程中再维护一个桶
a
n
c
i
anc_i
anci,表示
d
i
s
dis
dis值为
i
i
i的祖先有多少个,这样就可以快速判断了。
t
o
t
i
tot_i
toti和
c
a
n
i
can_i
cani可以由后两者累加而得。
计算答案的时候,枚举
d
i
s
dis
dis值,用前面四个桶里的值来计算,具体来说,如果
(
u
,
v
)
(u,v)
(u,v)能被算入答案中,则
u
u
u和
v
v
v至少有一个在
c
a
n
can
can或
s
c
n
scn
scn中。
注意,我们还没有计算
v
=
r
o
o
t
v=root
v=root的情况。针对这种情况,我们维护一个
c
n
t
0
cnt0
cnt0,在
d
f
s
dfs
dfs的时候遇到
a
n
c
d
i
s
i
>
1
anc_{dis_i}>1
ancdisi>1时,便意味着除了
r
o
o
t
root
root之外,还有一个和它
d
i
s
dis
dis值相同的祖先,那就将其加入
c
n
t
0
cnt0
cnt0中。答案加上
c
n
t
0
cnt0
cnt0即可。
所以说实际上这是一道点分治的模板题啊……
代码
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <climits>
#include <cassert>
#define N 100010
int n;
struct EDGE{
int to,len;
EDGE *las;
bool ok;
} e[N*2];
int ne;
EDGE *last[N];
inline void link(int u,int v,int len){
e[ne]={v,len,last[u],1};
last[u]=e+ne++;
}
#define rev(ei) (e+(int((ei)-e)^1))
long long ans;
int siz[N],all;
void get_siz(int x,int fa){
siz[x]=1;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa && ei->ok){
get_siz(ei->to,x);
siz[x]+=siz[ei->to];
}
}
int find(int x,int fa){//找重心
bool bz=(all-siz[x]<<1<=all);
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa && ei->ok){
int tmp=find(ei->to,x);
if (tmp)
return tmp;
bz&=(siz[ei->to]<<1<=all);
}
if (bz)
return x;
}
int dis[N];
int _tot[N*2],*tot=_tot+N;//为了代码方便,所以用指针来处理了……
int _sub[N*2],*sub=_sub+N;
int _can[N*2],*can=_can+N;
int _scn[N*2],*scn=_scn+N;
int cnt0;
int _anc[N*2],*anc=_anc+N;
int mn,mx,smn,smx;//记录mn,mx是为了方便清空桶。
void get_dis(int x,int fa){
smn=min(smn,dis[x]),smx=max(smx,dis[x]);
sub[dis[x]]++;
if (anc[dis[x]]){
scn[dis[x]]++;
if (anc[dis[x]]>1 && dis[x]==0)
cnt0++;
}
anc[dis[x]]++;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa && ei->ok){
dis[ei->to]=dis[x]+ei->len;
get_dis(ei->to,x);
}
anc[dis[x]]--;
}
void divide(int x){
get_siz(x,0);
all=siz[x];
int root=find(x,0);
dis[root]=0;
mn=INT_MAX,mx=INT_MIN;
anc[0]=1;
for (EDGE *ei=last[root];ei;ei=ei->las)
if (ei->ok){
smn=INT_MAX,smx=INT_MIN;
cnt0=0;
dis[ei->to]=dis[root]+ei->len;
get_dis(ei->to,root);
for (int j=smn;j<=smx;++j)
ans+=1ll*sub[j]*can[-j]+1ll*scn[j]*tot[-j]-1ll*scn[j]*can[-j];//容斥原理
ans+=cnt0;
for (int j=smn;j<=smx;++j){
tot[j]+=sub[j],sub[j]=0;
can[j]+=scn[j],scn[j]=0;
}
mn=min(mn,smn);
mx=max(mx,smx);
}
anc[0]=0;
for (int i=mn;i<=mx;++i)
tot[i]=can[i]=0;
for (EDGE *ei=last[root];ei;ei=ei->las)
if (ei->ok){
ei->ok=rev(ei)->ok=0;
divide(ei->to);
}
}
int main(){
scanf("%d",&n);
for (int i=1;i<n;++i){
int u,v,type;
scanf("%d%d%d",&u,&v,&type);
link(u,v,type==0?1:-1);
link(v,u,type==0?1:-1);
}
divide(1);
printf("%lld\n",ans);
return 0;
}
总结
点分治的题目还是打得太少了……