莫队+分块

· · 题解

一个很典的 trick,我竟然一直不知道,这题让我第一次学到这个 trick。

本题前置知识:分块,莫队,线段树(也许不需要)

简要题意

有一个序列,设 cnt_{l,r,c}=\sum_{i=l}^r[a_i=c],每次给定 l,r,x,求 \frac{(\sum_{i=1}^{x-1}cnt_{l,r,i})!}{\prod_{i=1}^{x-1}cnt_{l,r,i}!}

我会暴力(与正解关系不大)

期望得分:0。 ## 我会带修莫队(与正解关系不算太大) 通过思考暴力方法,我们发现增加一个元素是好处理的,像这种维护一个桶(也就是我们计算答案用的 $cnt_{l,r}$)的题,我们自然可以想到莫队。 看见这个 $l,r$,看到这个 $x$,看到这个不大的数据范围。 莫队的同时维护一个桶来处理当前的 $cnt_{l,r}$,另外需要维护答案的分母和分子。 带修莫队乎上去,$O(n^\frac53)$ 获得和作者一样的 TLE×15。 期望得分:0。 ## 我会莫队加线段树 (我并不会) 观察上面的复杂度(当然不观察暴力直接想到这一步可以),发现把 $x$ 这一维移来移去是一个很慢的操作,我们就考虑不进行带修,用建立在 $x$ 这一维上的线段树维护 $\prod cnt!$ 和 $\sum cnt$,每次查询一个前缀积就可以了。 如果插入一个值为 $i$ 的元素,那么就让线段树维护的 $cnt_{l,r,i}!$ 乘上一个 $cnt_{l,r,i}+1$ 即可,删除同理。 观察复杂度,每次移动一个端点不管是删除还是加入都涉及到线段树单点修改,复杂度是 $O((n+q)\sqrt n\log n)$ 的,移动好之后对于每一个题目的查询需要再线段树上查询前缀积,复杂度是 $O(q\log n)$ 的。 期望得分:0。 ## 我会莫队加分块 (我并不会) 还是观察上面的解法的复杂度,我们发现:莫队的**移动**和**查询**的复杂度分配不合理。一个是 $O((n+q)\sqrt n\log n)$,一个是 $O(q\log n)$,差别太大,我们可以考虑再这方面上下手,优化移动复杂度。 这里就是一个用于优化这种东西的典 trick 了,我们使用**分块**而非**线段树**维护,我们把上面的线段树直接替换为分块就好了,在 $x$ 一维上分块,莫队的每次**移动**只会修改一个 $cnt$ 和一个块的 $\prod cnt$ 和 $\sum cnt$,然而**查询**需要访问 $O(\sqrt n)$ 个块的 $\prod cnt!$ 和 $\sum cnt$ 以及一个散块的 $O(\sqrt n)$ 个 $cnt$。通过预处理阶乘和逆元和每个数的逆元等可以做到 $O((n+q)\sqrt n)$。 期望得分:625。 [完整代码链接](https://atcoder.jp/contests/abc405/submissions/65711729) 核心代码: ```cpp using modulo_int::fixed_modulo::mint; // 我的自动取模机,此题卡常,需稍作更改 #define K 500 // 莫队快长 #define K2 500 // 值域快长 int n, m, a[2000005], ansans[2000005]; // 输入与答案 mint fac[2000005], inv[2000005]; // 预处理,方便将一些 O(log) 优化为 O(1) int curl, curr, cnt[2000005], sum_cnt[2000005]; // 莫队维护 mint ans, product_cnt_fac[2000005]; // 莫队维护 struct query_struct { int l, r, x, id; } q[1000005]; // 查询 void add(int x) { // 莫队扩展 x = a[x]; cnt[x]++; product_cnt_fac[x / K2] *= cnt[x]; sum_cnt[x / K2]++; } void del(int x) { // 莫队收缩 x = a[x]; product_cnt_fac[x / K2] *= inv[cnt[x]]; sum_cnt[x / K2]--; cnt[x]--; } int query_sum(int x) // 查询 { int ans = 0; for (int i = 0; i < x / K2; i++) { // 整块 ans += sum_cnt[i]; } for (int i = x / K2 * K2; i <= x; i++) { // 散块 ans += cnt[i]; } return ans; } mint query_fac_product(int x) { // 查询 mint ans = 1; for (int i = 0; i < x / K2; i++) { // 整块 ans *= product_cnt_fac[i]; } for (int i = x / K2 * K2; i <= x; i++) { // 散块 ans *= fac[cnt[i]]; } return ans; } signed main() { for (int i = 0; i < 2000005; i++) { product_cnt_fac[i] = 1; } MATH::build_factorial<mint, mint>(2000000, fac, nullptr); for (int i = 1; i <= 2000000; i++) { inv[i] = mint(1) / mint(i); } // 上面都是预处理 cin >> n >> m; for (int i = 1; i <= n; i++) { cin >> a[i]; } for (int i = 1; i <= m; i++) { cin >> q[i].l >> q[i].r >> q[i].x; q[i].id = i; } sort(q + 1, q + m + 1, [](query_struct a, query_struct b)->bool { if (a.l / K != b.l / K) { return a.l / K < b.l / K; } else if (a.l / K & 1) { // 莫队奇偶优化 return a.r > b.r; } return a.r < b.r; }); curl = 1; for (int i = 1; i <= m; i++) { // 标准莫队 while (curl > q[i].l) add(--curl); while (curr < q[i].r) add(++curr); while (curl < q[i].l) del(curl++); while (curr > q[i].r) del(curr--); ansans[q[i].id] = fac[query_sum(q[i].x - 1)] / (query_fac_product(q[i].x - 1) + (mint)mod); } for (int i = 1; i <= m; i++) { cans << (ansans[i] + mod) % mod << endl; } return 0; } ```