NOIP2025 T3 题解
好题,比较能启发思维,来一篇启发式的题解。
如果遇到某一步想不到,可以先看看提示,然后再思考一段时间,再往下看。
::::warning[记号]{open}
跟正解没啥关系的 48 分暴力。
::::warning[提示]
首先肯定不能直接把每个子树的权值集合放到状态里面,但
那么考虑当某个子树的
注意到
转移类似树形背包状物,比较简单,不再赘述。
时间复杂度是
第二维是
由于和正解关系不大,这部分的代码没写。 :::: 一定不劣/不优的性质
以下性质的意义为,一定存在满足该性质的最优解。注意,是存在,不一定所有最优解都满足该性质。 ::::warning[提示] 考虑每个点填的数有没有大小顺序关系。 :::: ::::info[性质 1]
这个部分很接近正解,有 76pts。
::::info[经典结论]
所有点的子树大小之和等于所有点深度之和,在这题里是
转移的时候每个点都枚举子树是可以的。
::::
::::info[算法 2]
设
为了方便转移,设两个辅助数组
时间复杂度瓶颈在于
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define pii pair<ll,ll>
#define ve vector<ll>
#define fi first
#define se second
#define pb push_back
#define mid (l+r>>1)
#define lx (x<<1)
#define rx (x<<1|1)
using namespace std;
const ll N=8002,M=802;
ll T,n,m,d[N],p[N],dfn[N],dfm[N],dfp[N],f[N][M],g[N][M],h[N];
vector<ll>e[N];
ll&cmx(ll&x,ll y){return x=(x>y?x:y);}
void dfs(ll x){
d[x]=d[p[x]]+1,dfp[dfn[x]=++n]=x;
for(ll i:e[x])dfs(i);
dfm[x]=n;
}
void Dfs(ll x){
for(ll i:e[x])Dfs(i);
for(ll i=1;i<=m;i++){
ll s=i;
for(ll y:e[x])s+=g[y][i];
f[x][i]=s;
for(ll y:e[x])
cmx(f[x][i],s-g[y][i]+f[y][i+1]);
}
for(ll i=1;i<=m;i++){
g[x][i]=(dfm[x]-dfn[x]+1)*i;
for(ll j=dfn[x],y;j<=dfm[x];j++){
y=dfp[j];
h[y]=(x==y?0:h[p[y]]-g[y][i]);
ll s=0;
for(ll k:e[y])s+=g[k][i];
h[y]+=s;
if(d[y]-d[x]+1==i)
cmx(g[x][i],h[y]-s+f[y][i]+i*(i-1));
}
}
}
void slv(){
cin>>n>>m,m++;
for(ll i=1;i<=n;i++)e[i].clear();
for(ll i=2;i<=n;i++)cin>>p[i],e[p[i]].pb(i);
n=0,dfs(1),Dfs(1);
cout<<f[1][1]<<'\n';
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>T;
while(T--)slv();
return 0;
}
::::
这个部分是正解。
::::warning[提示]
小常数带
考虑能不能做到每个子树只遍历一次。
::::
::::info[算法 3]
把每次都会重新计算的
且由于转移条件
用树状数组维护子树加(dfn 序区间加)/单点查询即可做到
#include<bits/stdc++.h>
#define ll long long
#define ld long double
#define pii pair<ll,ll>
#define ve vector<ll>
#define fi first
#define se second
#define pb push_back
#define mid (l+r>>1)
#define lx (x<<1)
#define rx (x<<1|1)
using namespace std;
const ll N=8002,M=802;
ll T,n,m,d[N],p[N],dfn[N],dfm[N],dfp[N],f[N][M],g[N][M],h[N][M];
void add(ll x,ll k,ll y){
for(;x<=n;x+=x&-x)h[x][k]+=y;
}
void mdf(ll l,ll r,ll k,ll y){
add(l,k,y),add(r+1,k,-y);
}
ll qry(ll x,ll k,ll y=0){
for(;x;x-=x&-x)y+=h[x][k];
return y;
}
vector<ll>e[N];
ll&cmx(ll&x,ll y){return x=(x>y?x:y);}
void dfs(ll x){
d[x]=d[p[x]]+1,dfp[dfn[x]=++n]=x;
for(ll i:e[x])dfs(i);
dfm[x]=n;
}
void Dfs(ll x){
for(ll i:e[x])Dfs(i);
for(ll i=1;i<=m;i++){
ll s=0;
for(ll y:e[x])s+=g[y][i];
f[x][i]=s+i;
for(ll y:e[x])
cmx(f[x][i],s+i-g[y][i]+f[y][i+1]);
g[x][i]=(dfm[x]-dfn[x]+1)*i;
mdf(dfn[x],dfn[x],i,i*(i-1)+f[x][i]);
for(ll y:e[x])
mdf(dfn[y],dfm[y],i,s-g[y][i]);
}
for(ll j=dfn[x],k;j<=dfm[x];j++)
k=d[dfp[j]]-d[x]+1,cmx(g[x][k],qry(j,k));
}
void slv(){
cin>>n>>m,m++;
for(ll i=1;i<=n;i++)
e[i].clear(),fill(h[i],h[i]+m+1,0);
for(ll i=2;i<=n;i++)cin>>p[i],e[p[i]].pb(i);
n=0,dfs(1),Dfs(1);
cout<<f[1][1]<<'\n';
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>T;
while(T--)slv();
return 0;
}
::::
带
如果没有多测的话可以加强到
但是真的有必要遍历整个子树吗?能不能把不同的点再压缩一下呢?
::::
::::info[算法 4]
其实转移只和深度有关(只有唯一的限制
深度有关的 dp 经典方法就是长链剖分优化,不会的左转 P4292。
修改操作,对每个轻儿子的子树加,直接暴力枚举所有有效的深度一个一个加就可以了。
对于重儿子的子树加,不能直接枚举不然复杂度会炸,但是可以打标记,这样
根据长链剖分的复杂度原理,即每条长链都只会在自己的顶端的点作为轻儿子被合并一次,所以复杂度是
其实这场 NOIP 题还是很好的,这题也算是那种思路高妙,代码清新,做起来特别爽的类型,适合放 NOI D1T2。
但正赛出 NOIPlus 过于搞人心态,sale 调不出来直接 AFO 了,崩溃。