题解——星之界
题解
考虑化简这个可恶的式子
\[\prod\limits_{i = l}^{r} C_{\sum_{j = l}^{i}a_j}^{a_i}
\]
拆开,设\(S\)为前缀和数组
\[=\prod^r_{i=l}\frac{(S_i-S_{l-1})!}{a_i!(S_i-S_{l-1}-a_i)!}
\]
\[=\prod_{i=l}^rC_{S_{i-1}-S_{l-1}}^{S_i-S_{l-1}}
\]
\[=\frac{\prod_{i=l}^r\left(S_i-S_{l-1}\right)!}{\left(\prod_{i=l}^ra_i!\right)\prod_{i=l-1}^{r-1}{(S_i-S_{l-1})!}}
\]
\[=\frac{(S_r-S_{l-1})!}{\prod_{i=l}^ra_i!}
\]
现在我们需要一个维护区间和和区间阶乘积并且支持区间定值修改的数据结构
貌似线段树之类的不行,考虑分块实际上是看见了1e5
统计比较容易,主要难点在于区间定值修改。注意到值域与\(n\)同级,考虑开桶来维护每个值,为了保证复杂度,对于每一个块开一个桶,空间复杂度\(O(N\sqrt N)\),勉强可以承受
但是一个桶只能表示这个值出现的数量,为了维护\(O(N\sqrt N )\)的复杂度,需要更好的工具来维护。或许可以类比懒标记的思想,延迟修改。那么我们只需要知道每个位置最终表示哪个数即可。如果把每个桶中的元素看作集合,那么区间定值修改就类似于合并集合,这启发我们联系并查集算法。那么最初将每个位置看做单独的集合,初始化块的时候将值相同的位置合并成一个集合,为了方便代码,可以选取每个值第一次出现的位置为代表元。在修改的时候,对于每一个块将表示值\(x\)的集合合并到\(y\)去,注意有可能\(y\)所在集合是空的,此时只需要将代表元的值更改即可。在散块更改的时候,可以直接暴力重置这个块。
核心代码
struct node {
int id, cnt;
}f[S][N];//桶
struct Node {
int sum, mul;
};
int get(int x) {
return x == fa[x] ? x : fa[x] = get(fa[x]);
}
inline void init(int id) {//重置这个块
mul[id] = 1;
sum[id] = 0;
for (re int i = L[id]; i <= R[id]; i++) {
if (f[id][a[i]].id) {
f[id][a[i]].cnt++;
fa[i] = f[id][a[i]].id;
}
else {
f[id][a[i]].id = i;
fa[i] = i;
val[i] = a[i];//val是真实值
f[id][a[i]].cnt = 1;
}
sum[id] += a[i];
mul[id] = 1ll * mul[id] * inv[a[i]] % p;
}
}
inline void clear(int id) {
for (re int i = L[id]; i <= R[id]; i++) {
a[i] = val[get(i)];
f[id][a[i]].cnt = f[id][a[i]].id = 0;
}
for (re int i = L[id]; i <= R[id]; i++) {
fa[i] = 0;
}
}
inline void change_only(int id, int l, int r, int x, int y) {
clear(id);
for (re int i = l; i <= r; i++) {
if (a[i] == x)a[i] = y;
}
init(id);
}
inline void change_all(int id, int x, int y) {
f[id][y].cnt += f[id][x].cnt;
sum[id] -= (x - y) * f[id][x].cnt;
mul[id] = 1ll * mul[id] * power_jc[x][f[id][x].cnt] % p * power_inv[y][f[id][x].cnt] % p;//为了保证复杂度,预处理阶乘及其逆元的幂
if (f[id][y].id == 0)f[id][y].id = f[id][x].id, val[f[id][x].id] = y;
else fa[f[id][x].id] = f[id][y].id;
f[id][x] = { 0,0 };
}
inline void change(int l, int r, int x, int y) {
if (pos[l] == pos[r]) {
change_only(pos[l], l, r, x, y);
return;
}
change_only(pos[l], l, R[pos[l]], x, y);
change_only(pos[r], L[pos[r]], r, x, y);
for (re int i = pos[l] + 1; i < pos[r]; i++)change_all(i, x, y);
}
查询的时候就很简单,将涉及到的积乘上,将和加上
inline Node find_only(int l, int r) {
Node ans = { 0,1 };
for (re int i = l; i <= r; i++) {
ans.sum += val[get(i)];
ans.mul = 1ll * ans.mul * inv[val[get(i)]] % p;
}
return ans;
}
inline Node find_all(int id) {
return { sum[id],mul[id] % p };
}
inline Node merge(Node a, Node b) {
return { a.sum + b.sum,1ll * a.mul * b.mul % p };
}
inline int find(int l, int r) {
if (pos[l] == pos[r]) {
Node ans = find_only(l, r);
return 1ll * jc[ans.sum] * ans.mul % p;
}
Node ans = merge(find_only(l, R[pos[l]]), find_only(L[pos[r]], r));
for (re int i = pos[l] + 1; i < pos[r]; i++) {
ans = merge(ans, find_all(i));
}
return 1ll * jc[ans.sum] * ans.mul % p;
}
预处理的时候就常规预处理,然后处理阶乘逆元,预处理他们的幂保证复杂度(反正空间够),注意逆元得递推求
inline void init() {
jc[1] = inv[1] = 1;
for (re int i = 2; i <= M - 50; i++) {
jc[i] = 1ll * jc[i - 1] * i % p;
inv[i] = p - (1ll * p / (1ll * i) * 1ll * inv[p % i] % p) % p;
}
for (re int i = 2; i <= M - 50; i++) {
inv[i] = 1ll * inv[i - 1] * inv[i] % p;
}
scanf("%d%d", &n, &m);
for (re int i = 1; i <= n; i++)scanf("%d", &a[i]);
block = sqrt(n);
siz = n % block ? n / block + 1 : n / block;
for (re int i = 1; i <= siz; i++) {
L[i] = (i - 1) * block + 1;
R[i] = min(i * block, n);
}
for (re int i = 1; i <= siz; i++) {
for (re int j = L[i]; j <= R[i]; j++) {
pos[j] = i;
}
}
for (re int i = 1; i <= N - 5; i++) {
power_jc[i][0] = power_inv[i][0] = 1;
for (re int j = 1; j <= block; j++) {
power_jc[i][j] = 1ll * power_jc[i][j - 1] * jc[i] % p;
power_inv[i][j] = 1ll * power_inv[i][j - 1] * inv[i] % p;
}
}
for (re int i = 1; i <= siz; i++)init(i);
}
注意有个细节(没注意到就是25pts,调我好久,还是看题解才发现的),当修改时\(x=y\),需要直接跳过不变,因为这会导致\(cnt\)等的值不正确