[codechef] TABRARRAY
TABRARRAY
题意
给定 \(n,K\),有一棵以 \(1\) 为根的 \(n\) 个点的有根树,\(i\) 的父亲是 \(f_{i}\)。你要在每个点上写一个正整数 \(a_i\),使得:
-
\(\forall 2 \leq i \leq n,a_{f_{i}}\) \(mod\) \(a_i = 0\)
-
\(\prod a_i \leq K\)
求填写正整数的方案数取模 \(998244353\)。
\(n\leq 10^3,K \leq 10^{12}\)
分析
注意到每个质数都是独立的,我们将每个质数分开考虑,设 \(f_k\) 表示凑出 \(p^k\) 的方案数(其中,\(p\) 为任意质数)。
我们可以记状态 \(dp[u][s][i]\) 表示考虑以节点 \(u\) 为根的子树中质数 \(p\) 一共出现了 \(s\) 次且在当前节点出现了 \(i\) 次。我们可以以该状态加上前缀和优化写出一个复杂度为 \(O(k^3n)\) 的树形背包求出 \(f\) 数组(\(k = log(K)\))。
考虑如何统计方案?
注意到,除非这个质数只在 \(1\) 出现,否则必然有出现次数 \(> 1\)。
所以我们可以强行让 \(a_1 = lcm(a_2,a_3……a_n)\),这样 \(\prod a_i\) 中每个质数必然至少出现两次,而这样的数是 \(powerful number\),只存在 \(\sqrt{K}\) 个,全部枚举出来,进行统计,最后再给 \(a_1\) 乘上少算的一部分贡献。
具体地,设 \(X = p_1^{c_1}\times p_2^{c_2}……p_n^{c_n}\)(所有 \(P\) 都是质数)。则其对答案的贡献即为 \(\prod f_{c_i} \times \lfloor \frac{K}{X} \rfloor\)。
最后需要注意的是,如果 \(f\) 数组仍然是之前的定义,我们会算重。我们需要给 \(f\) 数组加上一个限制,即统计的是所有非根节点的 \(lcm = a_1\)。这不难以 \(O(k^3n)\) 实现,与此不再赘述。
CODE
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e3 + 10, K = 1e6 + 10, LOG_K = 41, MAX = 45, lim = 1e6, mod = 998244353;
inline int read()
{
int s = 0, w = 1;
char ch = getchar();
while(ch < '0' || ch > '9') { if(ch == '-') w *= -1; ch = getchar(); }
while(ch >= '0' && ch <= '9') s = s * 10 + ch - '0', ch = getchar();
return s * w;
}
struct node{ int x, id, val, num; };
int n, k, ans; //全是 1 的情况
int tem[MAX], f[MAX], g[MAX][MAX], h[MAX][MAX], dp1[N][MAX][MAX], dp2[N][MAX][MAX];
int cnt, pri[K];
bool vis[K];
vector<int> T[N];
queue<node> q;
inline void initial() //一共约八万个质数
{
for(register int i = 2; i <= lim; i++){
if(!vis[i]){
pri[++cnt] = i;
for(register int j = 1; i * j <= lim; j++) vis[i * j] = true;
}
}
}
inline void DFS(int u, int fa) //求 f 数组
{
for(register int i = 0; i < LOG_K; i++) dp1[u][i][i] = 1; //s = 0 的情况
for(register int to : T[u]){
if(to == fa) continue;
DFS(to, u);
//求 dp1
for(register int i = 0; i < LOG_K; i++)
for(register int j = 0; j <= i; j++) g[i][j] = dp1[u][i][j];
for(register int s = 1; s < LOG_K; s++){ //枚举以 to 为根的子树中有多少次
//前缀和
memset(tem, 0, sizeof(tem));
for(register int i = 1; i <= s; i++) tem[i] = (tem[i - 1] + dp1[to][s][i]) % mod;
for(register int i = s + 1; i < LOG_K; i++) tem[i] = tem[i - 1];
for(register int i = 1; i + s < LOG_K; i++) //枚举已经填了
for(register int j = 1; j <= i; j++) //枚举当前节点填 k 次
dp1[u][i + s][j] += g[i][j] * tem[j] % mod, dp1[u][i + s][j] %= mod;
}
if(u != 1) continue; //根节点特殊处理
for(register int i = 0; i < LOG_K; i++)
for(register int j = 0; j <= i; j++) h[i][j] = dp2[u][i][j];
for(register int s = 1; s < LOG_K; s++){ //枚举以 to 为根的子树中有多少次
memset(tem, 0, sizeof(tem));
for(register int i = 1; i <= min(LOG_K - 1, s); i++) tem[i] = (tem[i - 1] + dp1[to][s][i]) % mod;
for(register int i = min(LOG_K - 1, s) + 1; i < LOG_K; i++) tem[i] = tem[i - 1];
for(register int i = 1; i + s < LOG_K; i++){ //枚举已经填了
for(register int j = 1; j <= i; j++){ //枚举当前节点填 j 次
if(j <= s){
dp2[u][i + s][j] = (dp2[u][i + s][j] + g[i][j] * dp1[to][s][j] % mod) % mod;
dp2[u][i + s][j] = (dp2[u][i + s][j] + h[i][j] * ((tem[j] - dp1[to][s][j] + mod) % mod) % mod) % mod;
}
else dp2[u][i + s][j] = (dp2[u][i + s][j] + h[i][j] * tem[j] % mod) % mod;
}
}
}
}
}
inline void Sol(int pos,int now,int val)
{
ans = (ans + (k / now) % mod * val % mod) % mod;
for(register int i = pos; i <= cnt; i++){
if(k / pri[i] / pri[i] < now) break;
int S = now * pri[i] * pri[i];
for(register int j = 2; true; j++){
Sol(i + 1, S , val * f[j] % mod);
if(k / pri[i] < S) break;
S *= pri[i];
}
}
}
signed main()
{
initial();
n = read(), k = read();
for(register int i = 2; i <= n; i++){
int x = read();
T[x].push_back(i), T[i].push_back(x); //建图
}
DFS(1, 0);
for(register int i = 0; i < LOG_K; i++)
for(register int j = 0; j <= i; j++) f[i] += dp2[1][i][j], f[i] %= mod;
Sol(1, 1, 1), printf("%lld\n", ans);
return 0;
}