题目大意:
给定 n n n个节点的树,问删除尽可能小的点使得树的直径不超过 K K K,输出最小删除的点数, ( 1 < = k < = n < = 3 e 5 ) (1<=k<=n<=3e5) (1<=k<=n<=3e5)
解题思路:
这道题和AGC001的c题是一模一样的,但是这道题数据量比较大,但是核心思想还是不变的还是,枚举每个点作为直径的中间点,或者枚举边作为中心边,但是AGC001的c题数据量比较小可以暴力枚举这个题有 3 e 5 3e5 3e5,我们应该考虑如何优化暴力。
对于树上统计问题我们最先想到的就是点分治。
1.对于
K
K
K是偶数的情况:因为
K
K
K是偶数所以直径上的点就只有奇数个,那么对于每一个点
u
u
u我们假设这个点是直径的中点,那么距离
u
u
u长度大于
k
/
2
k/2
k/2的都要删掉,
那么每次就取重心,重心和孩子分别考虑;
1.1对于每个点
u
u
u我们考虑和点
u
u
u不在同一颗子树里面的点对这个点的影响,在同一个子树里面的点通过分治会递归下去求解。
1.2首先我们先整个分治的树求一共深度计数
n
u
m
[
]
(
就
是
统
计
一
下
各
个
深
度
的
点
有
多
少
个
)
num[](就是统计一下各个深度的点有多少个)
num[](就是统计一下各个深度的点有多少个)。
1.3对于每个子树求解答案的时候,我们先对这个子树求出属于它的
n
u
m
1
[
]
num1[]
num1[],
n
u
m
num
num和
n
u
m
1
num1
num1做差就是其他子树的影响了。
1.4为了快速减我们可以求个后缀和。因为是大于
K
/
2
K/2
K/2都要减掉
2.对于
K
K
K是奇数的情况:因为
K
K
K奇数那么中心位置是一个边,那么我们的答案就是枚举边做直径的中点,对于距离当前边的距离大于等于
k
/
2
+
1
k/2+1
k/2+1的点均要去掉。
2.1:注意一点就是我们点分治的时候是枚举点,但是统计答案的时候是边。你从父节点到下一个分治重心的时候,过渡的边
(
r
o
o
t
−
>
s
o
n
)
(root->son)
(root−>son)会只统计其他子树对这条边的影响,所以我们要为这些边直接加上自己子树上大于等于
K
/
2
+
1
K/2+1
K/2+1的点的个数。
#pragma comment(linker,"/STACK:102400000,102400000")
#pragma GCC optimize(2)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 3e5 + 10;
struct node {
int nxt, to;
}edge[maxn<<1];
int head[maxn], cnt;
int n, k;
inline void add(int from, int to) {
edge[++cnt] = {head[from],to};
head[from] = cnt;
}
//..........
bool vis[maxn];
int MX, root, max_son[maxn], now_num ,siz[maxn];
void getroot(int u, int fa) {
max_son[u] = 0;
siz[u] = 1;
for(int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if(v == fa || vis[v]) continue;
getroot(v,u);
siz[u] += siz[v];
max_son[u] = max(max_son[u],siz[v]);
}
max_son[u] = max(max_son[u],now_num - siz[u]);
if(max_son[u] < MX) MX = max_son[u], root = u;
}
//..................................
int num[maxn], max_depth;
int res[maxn];
void dfs1(int u, int fa, int depth) {//对整个树dfs
num[depth] ++;//深度计数
max_depth = max(max_depth,depth);
for(int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if(vis[v] || v == fa) continue;
dfs1(v,u,depth+1);
}
}
int num1[maxn], max_depth1;
void dfs2(int u, int fa, int depth) {//对某颗子树dfs
num1[depth]++;
max_depth1 = max(max_depth1,depth);
for(int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if(v == fa || vis[v]) continue;
dfs2(v,u,depth+1);
}
}
void dfs3(int u, int fa, int depth, int cntedge) {//统计答案的dfs
if(k & 1) {//如果是边是中心
int D = max(0,(k/2+1)-depth+1);
res[cntedge] += num[D] - num1[D];
if(fa == root) ///当前边是和root连的边时,要算子树u中对当前边的贡献
res[cntedge] += num1[k/1+1+1];///之所以后者要+1,时是因为要从当前边开始往下找,而当前边深度对于root来说一定是1;
} else {//点是中心
int D = max(0,k/2+1-depth);
res[u] += num[D] - num1[D];
}
for(int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if(v == fa || vis[v]) continue;
dfs3(v,u,depth+1,i/2);
}
}
void Div(int u) {
vis[u] = 1;
max_depth = 0;
dfs1(u,0,0);
for(int i = max_depth-1; i >= 0; -- i) num[i] += num[i + 1];
if(k % 2 == 0) res[u] += num[k/2+1]; //跟新分治节点的答案
for(int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if(vis[v]) continue;
max_depth1 = 0;
dfs2(v,u,1); //计算当前子树大小
for(int j = max_depth1-1; j >= 0; -- j) num1[j] += num1[j + 1];
dfs3(v,u,1,i/2);
for(int j = max_depth1; j >= 0; -- j) num1[j] = 0;
}
for(int i = max_depth; i >= 0; -- i) num[i] = 0;
for(int i = head[u]; i; i = edge[i].nxt) {
int v = edge[i].to;
if(vis[v]) continue;
MX = 1e9, root = 0;
now_num = siz[v];
getroot(v,u);
Div(root);
}
}
inline void init() {
cnt = 1;
for(int i = 0; i <= n; ++ i) vis[i] = 0, res[i] = 0, num[i] = 0, head[i] = 0;
}
int main() {
int T;
scanf("%d",&T);
while(T --) {
scanf("%d%d",&n,&k);
init();
for(int i = 0; i < n - 1; ++ i) {
int l, r;
scanf("%d%d",&l,&r);
add(l,r);
add(r,l);
}
root = 0, MX = 1e9;
now_num = n;
getroot(1,0);
Div(root);
int ans = 1e9;
for(int i = 1; i < n; ++ i)
ans = min(ans,res[i]);
if(k % 2 == 0) ans = min(ans,res[n]);//因为边只有n-1条,但是点却又n条。
printf("%d\n",ans);
}
return 0;
}