题解:P17145 [NOI 2026] 彩虹树
Daylighten · · 题解
非常好的计数题,给了我大大的震撼。
这道题,我认为最难的地方在于写出一个多项式时间复杂度做法,后面的优化或许没有那么难。
思路
对于这种题,我们可以尝试的方向就是找给定一个缤纷序列
1. 找判定方法
以下默认
首先需要满足的就是
然后我们记录一个数组
考虑如何对于一个点
我们先算出
我们可以把属于不同的子树的两个限制定为同一种颜色,然后将这两个限制合并成一个限制。
当
再考虑
首先
当然,特殊的情况是
接着考虑
-
如果
u\not\in S ,此时u 不会产生新的限制,综上,此时d_u=\max(0,d'-1) -
如果
u\in S ,此时u 会产生新的限制,于是就要从上种情况加一,就是:d_u=\max(1,d')
综上,我们可以
2. 得出暴力 dp 做法
我们得到了判定条件和流程,接下来就可以考虑 dp 了。
设
这个过程,就很像 dp 套 dp。
转移的时候,最暴力的做法就是先枚举
转移就是枚举儿子
算出
这样做时间复杂度是
下面放了部分代码,其中变量 sc 和 sd 分别代表 md 代表 j 是枚举的
:::info[代码实现关键部分]
//add(x,y) 是模意义下 x+=y
for(int j=1;j<=sz[u];j++)
{
f[0][0][0]=1;
int now=0,mxd=0;
for(auto v:e[u])
{
for(int sc=0;sc<=now;sc++)
{
for(int sd=0;sd<=sc;sd++)
{
for(int md=0;md<=mxd;md++)
{
if(!f[sc][sd][md]) continue;
for(int c=1;c<=min(j,sz[v]);c++)
{
for(int d=0;d<=c;d++)
{
if(!dp[v][c][d]) continue;
add(g[sc+c][sd+d][max(md,d)],1ll*f[sc][sd][md]*dp[v][c][d]%mod);
}
}
}
}
}
mxd=max(mxd,sz[v]);
now+=min(j,sz[v]);
for(int sc=0;sc<=now;sc++)
{
for(int sd=0;sd<=sc;sd++)
{
for(int md=0;md<=mxd;md++) f[sc][sd][md]=g[sc][sd][md],g[sc][sd][md]=0;
}
}
}
for(int sc=j-1;sc<=now;sc++)
{
for(int sd=0;sd<=sc;sd++)
{
for(int md=0;md<=mxd;md++)
{
int nk=max(md,sd-(sc-j));
add(dp[u][j][max(0,nk-1)],f[sc][sd][md]);
add(dp[u][j][max(1,nk)],f[sc][sd][md]);
}
}
}
for(int sc=0;sc<=now;sc++) for(int sd=0;sd<=sc;sd++) for(int md=0;md<=mxd;md++) f[sc][sd][md]=0;
}
:::
3. 优化
接下来就是优化了。
发现外层枚举
注意一下
我们分别考虑这两段:
-
对于左边那个水平段,其
c_u 的定义域是[\max c_v,T] 。值都是\max d_v 。我们还是需要维护至少
\max d_v,\max c_v,\sum {c_v}-\sum d_v 三个信息才能算,但是因为我们更新的是一段连续的c_u ,所以考虑差分。此时发现差分的左右两个端点都只用维护两个信息就可以算出来:
- 对于左边那个端点,
c_u=\max c_v,d'=\max d_v 。只用知道\max c_v 和\max d_v 两个信息就可以表示出来。 - 对于右边那个端点,
c_u=T+1,d'=\max d_v 。只用知道\max d_v 和\sum c_v-\sum d_v 就可以表示出来。
于是可以用上面算
f 的方式,我们可以设一个g_{i,j} 为\max c_v=i,\max d_v=j 的方案数,和h_{i,j} 为\max d_v=i,\sum c_v-\sum d_v=j 的方案数。这样状态数就可以少一维算出g,h ,然后开一个差分数组,存下所有更新。这里给出
g 的计算代码,下面用的变量名是f::::info[部分代码]
f[0][0]=1; int mx=0,now=0;// max(c), max(d) for(auto v:e[u]) { for(int mc=0;mc<=mx;mc++) { for(int md=0;md<=mx;md++) { if(!f[mc][md]) continue; for(int c=1;c<=sz[v];c++) { for(int d=0;d<=c;d++) { if(!dp[v][c][d]) continue; add(tmp[max(mc,c)][max(md,d)],1ll*f[mc][md]*dp[v][c][d]%mod); } } } } mx=max(mx,sz[v]); now+=sz[v]; for(int i=0;i<=mx;i++) { for(int j=0;j<=mx;j++) f[i][j]=tmp[i][j],tmp[i][j]=0; } } for(int mc=0;mc<=mx;mc++) { for(int md=0;md<=mx;md++) { if(!f[mc][md]) continue; add(cha1[mc][max(0,md-1)],f[mc][md]); add(cha1[mc][max(1,md)],f[mc][md]); f[mc][md]=0; } }:::
- 对于左边那个端点,
-
对于斜率为一的右边那一段,定义域为
[T+1,\sum c_v+1] ,值域[\max d_v+1,\sum d_v+1] 同样地,可以注意到我们同样可以使用差分,而且两个端点都只用两个信息就可以知道了:
- 左端点
c_u=T+1,d'=\max d_v+1 ,只用知道\max d_v 和\sum c_v-\sum d_v 就可以了。 - 右端点
c_u=\sum c_v+2,d'=\sum d_v+2 ,只用知道\sum c_v 和\sum d_v 就可以了。
而且注意到左端点需要的信息在上面一种情况已经算了一遍了,因为本质上是一个点。所以只用再跑一遍记录
\sum c_v,\sum d_v 的树上背包就可以了。然后差分是斜着的差分。 - 左端点
综上,我们只用跑三遍记录两个信息的树上背包,再加上两次差分,就可以算出
于是时间复杂度优化到了
代码比较丑,因为我把三次树上背包复制了三遍分开写的。
:::info[代码]
#include<bits/stdc++.h>
using namespace std;
const int N=205,mod=998244353;
int n,dp[N][N][N],sz[N];
int f[N][N],tmp[N][N];
int cha1[N][N],cha2[N][N];
vector<int> e[N];
int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
void add(int &x,int y)
{
x+=y;
if(x>=mod) x-=mod;
}
void dec(int &x,int y)
{
x-=y;
if(x<0) x+=mod;
}
void update(int x,int y)
{
for(int i=0;i<=x;i++)
{
for(int j=0;j<=y;j++) f[i][j]=tmp[i][j],tmp[i][j]=0;
}
}
void dfs(int u,int fa)
{
sz[u]=1;
if(!e[u].size())
{
dp[u][1][1]=1;
dp[u][1][0]=1;
return;
}
for(auto v:e[u]) dfs(v,u),sz[u]+=sz[v];
f[0][0]=1;
int mx=0,now=0;// max(c), max(d)
for(auto v:e[u])
{
for(int mc=0;mc<=mx;mc++)
{
for(int md=0;md<=mx;md++)
{
if(!f[mc][md]) continue;
for(int c=1;c<=sz[v];c++)
{
for(int d=0;d<=c;d++)
{
if(!dp[v][c][d]) continue;
add(tmp[max(mc,c)][max(md,d)],1ll*f[mc][md]*dp[v][c][d]%mod);
}
}
}
}
mx=max(mx,sz[v]);
now+=sz[v];
update(mx,mx);
}
for(int mc=0;mc<=mx;mc++)
{
for(int md=0;md<=mx;md++)
{
if(!f[mc][md]) continue;
add(cha1[mc][max(0,md-1)],f[mc][md]);
add(cha1[mc][max(1,md)],f[mc][md]);
f[mc][md]=0;
}
}
f[0][0]=1;
mx=0,now=0;// sum(c)-sum(d), max(d)
for(auto v:e[u])
{
for(int sum=0;sum<=now;sum++)
{
for(int md=0;md<=mx;md++)
{
if(!f[sum][md]) continue;
for(int c=1;c<=sz[v];c++)
{
for(int d=0;d<=c;d++)
{
if(!dp[v][c][d]) continue;
add(tmp[sum+c-d][max(md,d)],1ll*f[sum][md]*dp[v][c][d]%mod);
}
}
}
}
mx=max(mx,sz[v]);
now+=sz[v];
update(now,mx);
}
for(int sum=0;sum<=now;sum++)
{
for(int md=0;md<=mx;md++)
{
if(!f[sum][md]) continue;
int mid=md+sum;
dec(cha1[mid+1][max(0,md-1)],f[sum][md]);
dec(cha1[mid+1][max(1,md)],f[sum][md]);
add(cha2[mid+1][max(0,md)],f[sum][md]);
add(cha2[mid+1][max(1,md+1)],f[sum][md]);
f[sum][md]=0;
}
}
f[0][0]=1;
mx=0,now=0;// sum(c), sum(d)
for(auto v:e[u])
{
for(int sc=0;sc<=now;sc++)
{
for(int sd=0;sd<=now;sd++)
{
if(!f[sc][sd]) continue;
for(int c=1;c<=sz[v];c++)
{
for(int d=0;d<=c;d++)
{
if(!dp[v][c][d]) continue;
add(tmp[sc+c][sd+d],1ll*f[sc][sd]*dp[v][c][d]%mod);
}
}
}
}
mx=max(mx,sz[v]);
now+=sz[v];
update(now,now);
}
for(int sc=0;sc<=now;sc++)
{
for(int sd=0;sd<=now;sd++)
{
if(!f[sc][sd]) continue;
dec(cha2[sc+2][max(0,sd+1)],f[sc][sd]);
dec(cha2[sc+2][max(1,sd+2)],f[sc][sd]);
f[sc][sd]=0;
}
}
for(int a=0;a<=sz[u];a++)
{
for(int b=0;b<=a;b++)
{
if(a) add(cha1[a][b],cha1[a-1][b]);
if(a&&b) add(cha2[a][b],cha2[a-1][b-1]);
add(dp[u][a][b],cha1[a][b]);
add(dp[u][a][b],cha2[a][b]);
}
}
for(int a=0;a<=sz[u]+1;a++)
{
for(int b=0;b<=sz[u]+1;b++) cha1[a][b]=cha2[a][b]=0;
}
}
int rainbow(int ID,int N,vector<int> f)
{
n=N;
for(int i=2;i<=n;i++) e[f[i-1]+1].push_back(i);
dfs(1,0);
int ans=0;
for(int i=1;i<=n;i++) add(ans,dp[1][i][0]);
return ans;
}
// int main()
// {
// int C=read(),n=read();
// vector<int> f;
// f.push_back(0);
// for(int i=2;i<=n;i++)
// {
// int x=read();
// f.push_back(x);
// }
// cout<<rainbow(C,n,f);
// return 0;
// }
:::