根号分治 学习笔记

Tarantula

2021-10-04 15:32:01

Personal

个人感觉这个思想还是蛮神奇的(? 膜拜发明人/bx --- [来看题](https://www.luogu.com.cn/problem/P3396) 先考虑暴力。对于每个询问,执行以下代码: ```cpp for (int i = y; i <= n; i += x) ans += a[i]; ``` 但是这段代码的复杂度是 $O(\frac{n}{x})$ 的,最坏情况将会达到 $O(n)$,无法承受。 分析一下,发现当 $x$ 比较大时,时间复杂度是可以保证的。 再来考虑另一种暴力。我们预处理出一个数组 $cnt$,$cnt_{x,y}$ 表示序列中模 $x$ 余 $y$ 的所有位置上的数的和。把 $a_x$ 修改为 $y$ 时,执行以下代码: ```cpp for (int j = 1; j <= n; j++) cnt[j][x % j] -= (a[x] - y); a[x] = y; ``` 这样询问的复杂度是 $O(1)$,修改复杂度是 $O(n)$。 这个算法当所有 $x$ 都比较小时,复杂度是正确的,可以做到 $O(x)$。 于是,我们是否可以把上述两种暴力组合起来——当 $x$ 较大时,执行暴力一;当 $x$ 较小时,执行暴力二。 具体来说,我们设置一个阀值 $lim$,当询问的 $x\le lim$时,直接输出我们维护的 $cnt$;反之,暴力循环统计。修改时正确维护 $cnt$ 即可。 时间复杂度为 $O(n(lim+\frac{n}{lim}))$。由均值不等式可知,取 $lim=\sqrt{n}$ 时,理论复杂度最优,为 $O(n\sqrt{n})$。空间复杂度为 $O(n)$。 ```cpp #include<bits/stdc++.h> using namespace std; const int maxn = 150005, maxb = sqrt(150000) + 5; int n, m, block; int a[150005]; int cnt[maxb][maxb]; int main() { cin >> n >> m; block = sqrt(n); for (int i = 1; i <= n; i++) cin >> a[i]; for (int i = 1; i <= block; i++) for (int j = 1; j <= n; j++) cnt[i][j % i] += a[j];//预处理出cnt for (int i = 1; i <= m; i++) { char p; int x, y; cin >> p >> x >> y; if (p == 'A') { if (x <= block) cout << cnt[x][y] << endl; else { int ans = 0; for (int j = y; j <= n; j += x) ans += a[j]; cout << ans << endl; }//分情况采用不同方法统计答案 } else { for (int j = 1; j <= block; j++) cnt[j][x % j] -= (a[x] - y); a[x] = y;//维护cnt } } return 0; } ``` 我们发现,修改复杂度为 $O(lim)$,询问复杂度有时为 $O(lim)$,有时为 $O(\frac{n}{lim})$。因此可以考虑适当调小 $lim$ 使实际效率更高。经过测试,当 $lim$ 取 $n^{\frac{1}{3}}$ 时,实际表现最佳。 ~~卡常技巧一定要学好,做Ynoi很有用。~~ --- [再来看这个](https://www.luogu.com.cn/problem/CF1039D) 先考虑暴力贪心。对于每个$k$,直接dfs整棵树,能拼成一条长度为$k$的路径就拼。正确性显然。时间复杂度$O(n^2)$。 考虑根号分治。设$ans_i$表示$i$合法路径集的答案。设置阈值$lim$,对于$k\le lim$,暴力单独求解。 接下来处理大于$lim$的$k$。显然,$x$合法路径的集合大小不超过$\frac{n}{x}$,因此$ans_{lim+1},ans_{lim+2},\cdots,ans_n$显然至多只有$\frac{n}{lim}$种取值。我们又发现$ans$是单调不增的,因此可以考虑以下流程: 1. 设置指针$i$,首先指向$lim+1$; 2. dfs求出$ans_i$,同时在$i$的右边二分,找出最后一个与$i$答案相同的点,并把这一段的答案都更新为$ans_i$; 3. 重复以上过程,直至求解完毕。 分析一波复杂度。对于前一部分的求解,时间复杂度为$O(n\times lim)$;对于后一部分,我们最多二分$\frac{n}{lim}$次,每次二分的复杂度为$O(n\log n)$,因此总的复杂度为$O(n\times lim+\frac{n^2\log n}{lim})$。当$lim$取$\sqrt{n\log n}$时,理论复杂度最优,为$O(n\sqrt{n\log n})$。 需要注意,这道题有点卡常,求解不能直接dfs,需要预处理出$dfn$,然后用数组模拟求解。 ```cpp #include<bits/stdc++.h> using namespace std; int n, block; vector<int> a[100005]; int dfn[100005], cnt; int f[100005], s[100005]; int ans[100005]; inline int read() { int x = 0, f = 1; char c = getchar(); while (!isdigit(c)) {if (c == '-') f = -1; c = getchar();} while (isdigit(c)) {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();} return x * f; } void dfs(int fa, int x) { for (int i = 0; i < a[x].size(); i++) { int y = a[x][i]; if (y == fa) continue; f[y] = x; dfs(x, y); } dfn[++cnt] = x; }//预处理 int check(int k) {//求出ans[k] int ans = 0; for (int i = 1; i <= n; i++) s[i] = 1; for (int i = 1; i <= n; i++) { int u = dfn[i]; if (f[u] && s[u] != -1 && s[f[u]] != -1) if (s[f[u]] + s[u] >= k) ans++, s[f[u]] = -1;//能拼就拼 else s[f[u]] = max(s[f[u]], s[u] + 1); } return ans; } int main() { n = read(); block = sqrt(n * log2(n)); for (int i = 1; i < n; i++) { int x = read(), y = read(); a[x].push_back(y); a[y].push_back(x); } dfs(0, 1); printf("%d\n", n); for (int i = 2; i <= block; i++) printf("%d\n", check(i)); for (int i = block + 1; i <= n; i++) { int x = check(i), l = i, r = n + 1; while (l + 1 < r) { int mid = l + r >> 1; if (check(mid) == x) l = mid; else r = mid; }//二分 for (; i <= l; i++) printf("%d\n", x); i--; } return 0; } ``` --- 几道习题: - [LOJ #2838. 「JOISC 2018 Day 3」比太郎的聚会](https://loj.ac/p/2838) - [P5397 [Ynoi2018] 天降之物](https://www.luogu.com.cn/problem/P5397)(相信我,这题完全可做)