Solution P10175 / 模数性质与分配律展开

· · 题解

验题人题解。

枚举 |S|,然后我们知道了每一个 a_i+|S|,暴力跑一遍 \mathcal{O}(n^2) 的树上背包即可。设 f_{i,j} 为以 i 为根的子树选取了大小为 j 的连通块,每种方案 \prod(a_i+|S|) 的总和,易转移。

发现每次枚举 |S| 其实制造了浪费,又有模数为 U^V 的性质没有使用。

好像只寄希望于它变成 $0$ 并没有什么用。 继续尝试,为了让每个式子里出现 $U$,同时联想到分配律展开的 trick,我们可以将乘积中的每个数写成除以 $U$ 的带余除法形式:令 $a_i+|S| = Ux+y \ (0 \le y < U)$。 我们惊喜地发现,一堆 $(Ux+y)$ 的乘积看成每个括号中选取一项乘起来再加和(分配律)的形式后,若 $Ux$ 的选取个数不少于 $V$ 个,那么整个这一项模 $U^V$ 就变成 $0$ 了。 为了方便实现,由于 $a_i$ 已经确定,我们不必去拆 $a_i$,而只把 $|S|$ 写成 $Ux+y$ 的形式,则 $\prod (a_i+|S|) = \prod (Ux + (y+a_i))$。 $y$ 的取值范围是很小的,启发我们直接枚举 $y$,然后 DP 时记录选取 $Ux$ 的次数(最多 $V-1$ 次)。而 $Ux$ 具体是多少,我们可以最后再计算,它对整个 DP 过程是不影响的! 此时我们就不需要 24 分做法中的枚举 $|S|$,而是枚举 $|S| \mod U$ 再 DP。 设 $f_{i,j,y,k}$ 表示以 $i$ 为根,选取 $j$ 个点的连通块,枚举的 $|S| \mod U = y$,选取 $Ux$ 的个数为 $k(k<V)$,树上背包转移它。 DP 完后再枚举 $|S|$ 具体的值,此时 $|S| = Ux+y$ 中 $x,y$ 的具体值已经确定,直接访问 $f$ 数组加和计算即可。 ```cpp vector<ll>E[2005]; ll n,U,V,Mod,a[2005],p[2005],sz[2005],I; ll f[2001][2001][7], tmp[2001][7]; void dfs(ll x){ f[x][1][0] = (a[x] + I) % Mod; f[x][1][1] = 1; sz[x]=1; for(auto y: E[x]){ dfs(y); memset(tmp,0,sizeof(tmp)); for(ll j=0;j<V;j++){ for(ll k=1;k<=sz[x];k++){ for(ll l=0;j+l<V;l++){ for(ll k2=0; k2<=sz[y]; k2++) tmp[k+k2][j+l] += f[x][k][j] * f[y][k2][l]; } } } sz[x] += sz[y]; for(ll k=1;k<=sz[x];k++) for(ll j=0;j<V;j++) f[x][k][j] = tmp[k][j] % Mod; } f[x][0][0] = 1; } void solve(){ n=read(), U=read(),V=read(), Mod=1; for(ll i=1;i<=V;i++) Mod*=U; for(ll i=2;i<=n;i++) p[i]=read(), E[p[i]].emplace_back(i); for(ll i=1;i<=n;i++) a[i]=read(); ll ans = 0; for(I=0;I<U;I++){ memset(f,0,sizeof(f)); dfs(1); for(ll x=1;x<=n;x++){ for(ll i=I+((I==0)?U:0);i<=sz[x];i+=U){ ll p=i/U*U, w=1; for(ll j=0;j<V;j++){ ans = (ans + f[x][i][j] * w); w = w * p % Mod; } } ans %= Mod; } } printf("%lld\n", ans); } ```