题目来源:
洛谷P4183
做出来的第一道黑题,写一篇题解纪念一下。
首先考虑暴力
40
p
t
s
40pts
40pts :
我们假设现在
A
l
i
c
e
Alice
Alice 在点
i
i
i 处,那么现在就存在两种情况:
- 点 i i i 为叶子节点,那么显然让一个 B o b Bob Bob 在点 i i i 处即可。
- 点 i i i 不为叶子节点,那么考虑每一个叶子,假设一个叶子节点为 j j j, k k k 为 i , j i,j i,j 路径之间的一个中间点,在 j j j 处放置一个 B o b Bob Bob 。首先 A l i c e Alice Alice 肯定不能去往 j j j 节点了;其次,如果 d i s ( j , k ) < = d i s ( i , k ) dis(j,k)<=dis(i,k) dis(j,k)<=dis(i,k) ,那么以 k k k 为根节点的所有叶子节点 A l i c e Alice Alice 均不可能到达,因为 A l i c e Alice Alice 想要到达以 k k k 为根节点的子树内的任意一个叶子结点都势必要经过 k k k 结点,而 B o b Bob Bob 可以先其一步或与 A l i c e Alice Alice 同时到达 k k k 点,从而实行围堵或抓捕。
我们再进一步转化一下上面
2
2
2 的思想,设
g
[
i
]
g[i]
g[i] 表示距离
i
i
i 最近的叶子结点到
i
i
i 的距离,
d
i
s
[
i
]
[
j
]
dis[i][j]
dis[i][j] 表示
i
,
j
i,j
i,j 之间的简单路径长度,如果
d
i
s
[
x
]
[
k
]
>
=
g
[
k
]
dis[x][k]>=g[k]
dis[x][k]>=g[k],那么这颗子树上总共只需要一个
B
o
b
Bob
Bob 即可(放在离
k
k
k 最近的那个叶子结点上)。
据此我们就可以写出我们的暴力思路了:
\quad\;\, 对于每一个点 i i i ,如果其为叶子结点,那么直接输出 − 1 -1 −1 即可;如果不为叶子结点,暴力做 d f s dfs dfs ,对于访问到的子辈节点 j j j ,如果其满足 g [ j ] < = d i s [ i ] [ j ] g[j]<=dis[i][j] g[j]<=dis[i][j] ,说明整颗子树的总贡献为 1 1 1 ,直接统计答案后 b r e a k break break 即可,如果不满足,则继续递归子树。
对于
g
g
g 数组的求法,我们要求的是多个点到几个点中某一个的最短距离,直接跑多源
b
f
s
bfs
bfs 即可。而
d
i
s
dis
dis 数组在
d
f
s
dfs
dfs 过程中就可以顺便递推求出。
时间复杂度
O
(
n
2
)
O(n^2)
O(n2) 。
#include<bits/stdc++.h>
using namespace std;
int n,g[100000],du[100000],head[100000],tot,ans;
bool v[100000];
queue<int> q;
struct node
{
int to,nex;
} edge[200000];
void add(int x,int y)
{
edge[++tot].nex=head[x];
edge[tot].to=y;
head[x]=tot;
}
void dfs(int x,int fa,int d)
{
if(g[x]<=d)
{
ans++;
return;
}
for(int i=head[x];i;i=edge[i].nex)
{
if(edge[i].to==fa)
continue;
dfs(edge[i].to,x,d+1);
}
}
int main()
{
freopen("run.in","r",stdin);
freopen("run.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
du[x]++;
du[y]++;
add(x,y);
add(y,x);
}
for(int i=1;i<=n;i++)
if(du[i]==1)
{
q.push(i);
v[i]=1;
}
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=edge[i].nex)
{
if(v[edge[i].to])
continue;
v[edge[i].to]=1;
g[edge[i].to]=g[x]+1;
q.push(edge[i].to);
}
}
for(int i=1;i<=n;i++)
{
ans=0;
dfs(i,0,0);
printf("%d\n",ans);
}
return 0;
}
100 p t s 100pts 100pts :
我们发现暴力程序中对于时间复杂度的浪费是在于
d
f
s
dfs
dfs 过程中对于信息的浪费,每一个点都做一遍显然会浪费很多信息,那么有没有充分利用信息的方法?当然有!不过得先推性质。
在上述
d
f
s
dfs
dfs 过程中对于每个点,我们求的是很多极大子树的根,且每个根的贡献为
1
1
1 ,直接维护极大子树的根想想都很复杂,那么我们考虑能不能把根的贡献变成所有子树中点的贡献。
设
d
e
g
[
i
]
deg[i]
deg[i] 表示点
i
i
i 的度数,
S
S
S 表示以
i
i
i 为根的子树的点集,那么有:
∑
i
∈
S
d
e
g
[
i
]
=
2
×
(
∣
S
∣
−
1
)
+
1
=
2
×
∣
S
∣
−
1
\quad\sum\limits_{i\in S}deg[i]=2\times(|S|-1)+1=2\times|S|-1
i∈S∑deg[i]=2×(∣S∣−1)+1=2×∣S∣−1
⇒
2
×
∣
S
∣
−
∑
i
∈
S
d
e
g
[
i
]
=
1
\Rightarrow2\times|S|-\sum\limits_{i\in S}deg[i]=1
⇒2×∣S∣−i∈S∑deg[i]=1
⇒
∑
i
∈
S
2
−
∑
i
∈
S
d
e
g
[
i
]
=
1
\Rightarrow\sum\limits_{i\in S}2-\sum\limits_{i\in S}deg[i]=1
⇒i∈S∑2−i∈S∑deg[i]=1
⇒
∑
i
∈
S
2
−
d
e
g
[
i
]
=
1
\Rightarrow\sum\limits_{i\in S}{2-deg[i]}=1
⇒i∈S∑2−deg[i]=1
当我们化简到这一步的时候,我们就可以发现,对于每一个极大子树的根的贡献可以化简为极大子树中每一个点的点权(
2
−
d
e
g
[
i
]
2-deg[i]
2−deg[i] )。这样对于每一个点
i
i
i 的答案统计,我们只需要统计所有
g
[
j
]
<
=
d
i
s
[
i
]
[
j
]
g[j]<=dis[i][j]
g[j]<=dis[i][j] 的点
j
j
j 的点权和。这显然可以用点分治来求解。
点分治的过程具体来说:
设当前重心为
x
x
x ,我们要求
d
i
s
[
i
]
[
j
]
>
=
g
[
i
]
dis[i][j]>=g[i]
dis[i][j]>=g[i] (
i
,
j
i,j
i,j 不在同一颗子树中),也就是求
d
i
s
[
x
]
[
i
]
+
d
i
s
[
x
]
[
j
]
>
=
g
[
i
]
dis[x][i]+dis[x][j]>=g[i]
dis[x][i]+dis[x][j]>=g[i] ,也就是
d
i
s
[
x
]
[
j
]
>
=
g
[
i
]
−
d
i
s
[
x
]
[
i
]
dis[x][j]>=g[i]-dis[x][i]
dis[x][j]>=g[i]−dis[x][i],开一个树状数组,我们对于每一颗子树中的每一个点求答案时,直接在树状数组中查找即可,处理完一颗子树后,我们把这颗子树中每个点的点权再插入树状数组中即可。
细节:1.注意处理重心的贡献以及重心的答案。2.插入树状数组过程中,以
g
[
i
]
−
d
i
s
[
x
]
[
i
]
g[i]-dis[x][i]
g[i]−dis[x][i] 为下标有可能出现下标
≤
0
\le0
≤0 的情况,把每一个下标加上
n
+
1
n+1
n+1 即可。
#include<bits/stdc++.h>
using namespace std;
int n,g[100000],du[100000],head[100000],tot,size[100000],jl=0x3f3f3f3f,id,len[100000],c[1000000],ans[100000],a[100000];
bool v[100000];
queue<int> q;
vector<int> ins;
struct node
{
int to,nex;
} edge[200000];
void addE(int x,int y)
{
edge[++tot].nex=head[x];
edge[tot].to=y;
head[x]=tot;
}
void getsize(int x,int fa)
{
size[x]=1;
for(int i=head[x];i;i=edge[i].nex)
{
if(edge[i].to==fa||v[edge[i].to])
continue;
getsize(edge[i].to,x);
size[x]+=size[edge[i].to];
}
}
void Find(int x,int fa,int S)
{
int maxx=0;
len[x]=1;
for(int i=head[x];i;i=edge[i].nex)
{
if(edge[i].to==fa||v[edge[i].to])
continue;
Find(edge[i].to,x,S);
len[x]+=len[edge[i].to];
maxx=max(maxx,len[edge[i].to]);
}
maxx=max(maxx,S-len[x]);
if(maxx<jl)
{
jl=maxx;
id=x;
}
}
inline int lowbit(int x)
{
return x&(-x);
}
void add(int x,int y)
{
for(;x<=3*n;x+=lowbit(x))
c[x]+=y;
}
int ask(int x)
{
int y=0;
for(;x;x-=lowbit(x))
y+=c[x];
return y;
}
void solve(int x,int fa,int d)
{
ans[x]+=ask(d+n+1);
for(int i=head[x];i;i=edge[i].nex)
{
if(edge[i].to==fa||v[edge[i].to])
continue;
solve(edge[i].to,x,d+1);
}
}
void Add(int x,int fa,int d,int y)
{
add(g[x]-d+n+1,y*a[x]);
for(int i=head[x];i;i=edge[i].nex)
{
if(edge[i].to==fa||v[edge[i].to])
continue;
Add(edge[i].to,x,d+1,y);
}
}
void dfs(int x)
{
v[x]=1;
ins.clear();
add(g[x]+n+1,a[x]);
for(int i=head[x];i;i=edge[i].nex)
{
if(v[edge[i].to])
continue;
ins.push_back(edge[i].to);
solve(edge[i].to,x,1);
Add(edge[i].to,x,1,1);
}
ans[x]+=ask(n+1);
add(g[x]+n+1,-a[x]);
for(int i=0;i<ins.size();i++)
Add(ins[i],x,1,-1);
for(int i=ins.size()-1;~i;i--)
{
solve(ins[i],x,1);
Add(ins[i],x,1,1);
}
for(int i=0;i<ins.size();i++)
Add(ins[i],x,1,-1);
for(int i=head[x];i;i=edge[i].nex)
{
if(v[edge[i].to])
continue;
getsize(edge[i].to,x);
jl=0x3f3f3f3f;
id=0;
Find(edge[i].to,0,size[edge[i].to]);
dfs(id);
}
}
int main()
{
freopen("run.in","r",stdin);
freopen("run.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<n;i++)
{
int x,y;
scanf("%d%d",&x,&y);
du[x]++;
du[y]++;
addE(x,y);
addE(y,x);
}
for(int i=1;i<=n;i++)
if(du[i]==1)
{
q.push(i);
v[i]=1;
}
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=head[x];i;i=edge[i].nex)
{
if(v[edge[i].to])
continue;
v[edge[i].to]=1;
g[edge[i].to]=g[x]+1;
q.push(edge[i].to);
}
}
for(int i=1;i<=n;i++)
a[i]=2-du[i];
memset(v,0,sizeof v);
getsize(1,0);
Find(1,0,size[1]);
dfs(id);
for(int i=1;i<=n;i++)
if(du[i]!=1)
printf("%d\n",ans[i]);
else
puts("1");
return 0;
}
总结:把点权转化为子树中每一个点的权值,之后跑点分治的思路很妙,需要多加理解。