题目链接:传送门
题目大意:给一棵 n n n个节点、以 1 1 1为根的树。有 Q Q Q次询问,每次给出一个 K K K,用最少的操作次数遍历完整棵树,输出最少操作次数。每次操作可以选择访问不超过 K K K个未访问的点,且这些点的父亲必须在之前被访问过。
需要点亮的技能:
1.
d
p
1.dp
1.dp(雾)
2.
2.
2.斜率(大雾)
3.
3.
3.斜率优化基本操作
首先题目的样例长相奇特,在这里解释一下:
(借用一下这个题解的图,虽然他的样例解释是错的qwq)
样例的
K
=
3
K=3
K=3,即每次最多珂以访问3个父节点已被访问过的节点。
最少要8次操作遍历完整棵树:
第1次:1
第2次:2,3,4
第3次:5,6,8
第4次:7,9,10
第5次:11,12
第6次:17,18,19
第7次:13,14,20
第8次:15,16
Step 1.贪心
思考如何让操作次数最小:
每次尽量多访问节点,且尽量访问有儿子的节点。
{\color{red}\text{每次尽量多访问节点,且尽量访问有儿子的节点。}}
每次尽量多访问节点,且尽量访问有儿子的节点。
尽量访问有儿子的节点与都访问没有儿子的节点相比,下一次可访问的节点数量会更多,因此尽量访问有儿子的节点会更优。
这样贪心之后答案一定正确。
然后发现,每次贪心复杂度是
O
(
n
)
O(n)
O(n),总共贪心
q
q
q次,因此总时间复杂度是
O
(
n
q
)
O(nq)
O(nq),显然过不了qwq(
O
(
n
q
)
O(nq)
O(nq)过百万?)。
Step 2.dp
如果珂以把答案预处理出来,就珂以
O
(
1
)
O(1)
O(1)查询了qwq。
这里yy一下, 显然这是个
d
p
dp
dp。
令
d
p
[
i
]
dp[i]
dp[i]表示当
K
=
i
K=i
K=i,即每次操作最多访问
i
i
i个节点时,所需要的最少操作次数。
然后发现这个东西好像递推不了QWQ?
某不知名dalao:于是我们可以考虑上网搜题解。
考虑从
j
j
j推出
i
i
i:
令
s
[
i
]
s[i]
s[i]表示深度
>
i
>i
>i的节点个数。
则
d
p
[
i
]
=
m
a
x
(
j
+
⌈
s
[
j
]
i
⌉
)
dp[i]=max(j+\lceil \frac{s[j]}{i} \rceil)
dp[i]=max(j+⌈is[j]⌉)
(毒瘤!)
{\color{red}\text{(毒瘤!)}}
(毒瘤!)
思考几个问题:
{\color{red}\text{思考几个问题:}}
思考几个问题:
1.如何解释这个转移方程?(为什么其他题解都说这个式子显然呢qwq)
2.为什么是max而不是min?
方程表示的是用
j
j
j次访问完前
j
j
j层节点,之后每次操作,除最后一次访问完所有节点外,都可以访问
i
i
i次节点。
那么这为什么是正确的呢?
{\color{red}\text{那么这为什么是正确的呢?}}
那么这为什么是正确的呢?
脑补一下发现,大部分情况下, j + ⌈ s [ j ] i ⌉ j+\lceil \frac{s[j]}{i} \rceil j+⌈is[j]⌉是比 K = i K=i K=i时的真实最小操作数小的!
因此这里要取
m
a
x
max
max值。
但怎么证明这个
m
a
x
max
max值就一定为
d
p
[
i
]
dp[i]
dp[i]呢?
由贪心策略可知,取完前
j
j
j层之后,每次应该尽量访问有儿子的节点。
如果此时能访问的节点数不超过 i i i,则可以把第 j + 1 j+1 j+1层节点取完,于是用 j + 1 j+1 j+1次取完前 j + 1 j+1 j+1层时的最小操作次数也会计入答案,且这个答案会比当前第 j j j层的答案大(比较显然)。
如果此时能访问的节点超过
i
i
i,那么按照贪心策略优先访问有儿子的节点。
再分类:
1.
1.
1.如果有儿子的节点的个数不超过
i
i
i,则把有儿子的节点访问完,和无儿子的节点共访问其中
i
i
i个。
2.
2.
2.如果有儿子的节点的个数超过
i
i
i,则不断访问
i
i
i个有儿子的节点,直到有儿子的节点的个数不超过
i
i
i,然后回到1。
yy一下, 显然这样最后每次操作可以访问
i
i
i个节点。
于是
d
p
dp
dp方程就解释完了。
(为什么其他题解几句话就完事……估计是我太蒻了qwq)
Step 3.斜率优化
按照套路,若
j
>
k
j>k
j>k且
j
j
j比
k
k
k优,则:
j
+
⌈
s
[
j
]
i
⌉
>
k
+
⌈
s
[
k
]
i
⌉
j+\lceil \frac{s[j]}{i} \rceil>k+\lceil \frac{s[k]}{i} \rceil
j+⌈is[j]⌉>k+⌈is[k]⌉
通分,得
⌈
i
∗
j
+
s
[
j
]
i
⌉
>
⌈
i
∗
k
+
s
[
k
]
i
⌉
\lceil \frac{i*j+s[j]}{i} \rceil>\lceil \frac{i*k+s[k]}{i} \rceil
⌈ii∗j+s[j]⌉>⌈ii∗k+s[k]⌉。
两边同乘
i
i
i,把取整消掉,得
i
∗
j
+
s
[
j
]
>
i
∗
k
+
s
[
k
]
i*j+s[j]>i*k+s[k]
i∗j+s[j]>i∗k+s[k]
乱移个项,得
s
[
j
]
−
s
[
k
]
>
i
∗
(
k
−
j
)
s[j]-s[k]>i*(k-j)
s[j]−s[k]>i∗(k−j)
因为
j
>
k
j>k
j>k,即
j
−
k
>
0
,
k
−
j
<
0
j-k>0 , k-j<0
j−k>0,k−j<0,
所以
s
[
j
]
−
s
[
k
]
j
−
k
>
i
\frac{s[j]-s[k]}{j-k}>i
j−ks[j]−s[k]>i (乘一些奇奇怪怪的
−
1
-1
−1之类的)。
然后就是斜率优化基本套路辣QuQ~
不过这题中
d
p
[
i
]
dp[i]
dp[i]是从
[
1
,
m
a
x
d
e
e
p
]
[1,maxdeep]
[1,maxdeep]推来的,所以一开始要把所有可能的
j
j
j入队,然后转移时只用考虑出队。
代码
丑陋、毒瘤的代码:
(忘记输出空格调了一年QAQ)
#include<stdio.h>
#include<cstring>
#include<algorithm>
#include<math.h>
#define re register int
#define rl register ll
#define lc rt<<1
#define rc rt<<1|1
using namespace std;
typedef long long ll;
int read() {
re x=0,f=1;
char ch=getchar();
while(ch<'0' || ch>'9') {
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0' && ch<='9') {
x=10*x+ch-'0';
ch=getchar();
}
return x*f;
}
const int Size=1000005;
ll n,q,dp[Size],s[Size],k[Size];
int hd,tl,Queue[Size];
inline double slope(int i,int j) {
return (s[j]-s[i])/(j-i);
}
int cnt,maxd,head[Size],deep[Size],num[Size];
struct Edge {
int v,next;
} w[Size<<1];
void AddEdge(int u,int v) {
w[++cnt].v=v;
w[cnt].next=head[u];
head[u]=cnt;
}
void dfs(int x,int fa) {
deep[x]=deep[fa]+1;
if(deep[x]>maxd) {
maxd=deep[x];
}
num[deep[x]]++;
for(int i=head[x]; i; i=w[i].next) {
dfs(w[i].v,x);
}
}
int main() {
n=read();
q=read();
int maxk=0;
for(re i=1; i<=q; i++) {
k[i]=read();
if(k[i]>maxk) maxk=k[i];
}
for(re i=2; i<=n; i++) {
int f=read();
AddEdge(f,i);
}
dfs(1,0);
for(re i=maxd; i; i--) {
s[i]=s[i+1]+num[i+1];
}
hd=tl=0;
for(re i=1; i<=maxd; i++) {
while(hd<tl && slope(Queue[tl],i)>=slope(Queue[tl-1],Queue[tl])) tl--;
Queue[++tl]=i;
}
for(rl i=1; i<=maxk; i++) {
while(hd<tl && i*Queue[hd+1]+s[Queue[hd+1]]>=i*Queue[hd]+s[Queue[hd]]) hd++;
dp[i]=Queue[hd]+ceil(1.0*s[Queue[hd]]/i);
}
for(re i=1; i<=q; i++) {
printf("%lld ",dp[k[i]]);
}
return 0;
}