根号科技概览Chapter1 -- 前言与小技巧(alpha版)


参考资料

《根号算法--不只是分块》 王悦同 ———— 2014集训队论文
《lxl的大分块》———— nzhtl1477
《根号算法》 ———— ckw


前言

近一年 lxl 题开始更频繁地进入大众视野了orz。
然后我就想学啦。
不想定目标, 最后发出来的是 Chapter几 就算 Chapter几 吧。


自然整除分块

其实就是一个结论, 对于整数 \(n,k\), 整除运算 \(\lfloor \frac{n}{k} \rfloor\) 的不同的值的个数是 \(O(\sqrt n)\) 的。

证明很简单, 先不考虑整除等于 0 的情况, 对于 \(k\) 落在 \([1,n]\) 的情况, 分 \(k \in [1, \sqrt n]\)\(k \in (\sqrt n, n]\) 两种即可。

具体应用上主要是用于求和, 求这样一类式子:

\[\sum_{i=1}^{min\{ n_1 \dots n_k \}} \lfloor \frac{n_1}{i} \rfloor \lfloor \frac{n_2}{i} \rfloor \cdots \lfloor \frac{n_k}{i} \rfloor f(i) \]

其中, \(\sum_{i=l}^r f(i)\) 可以快速求。
这种问题用的是这样一个相关结论:最大的使得 \(\lfloor \frac{n}{s} \rfloor = q\)\(s\)\(\lfloor \frac{n}{q} \rfloor\)
这样就可以把像 \(\lfloor \frac{n_k}{i} \rfloor\) 的形式分别划分成 \(O(\sqrt n_k)\) 段, 然后将这些段 “并起来”, 这样对于每段里的每个 \(i\) 来说, \(\lfloor \frac{n_1}{i} \rfloor \lfloor \frac{n_2}{i} \rfloor \cdots \lfloor \frac{n_k}{i} \rfloor\) 都是相同的, 就可以这样计算这一段的和:

\[\lfloor \frac{n_1}{l} \rfloor \lfloor \frac{n_2}{l} \rfloor \cdots \lfloor \frac{n_k}{l} \rfloor \sum_{i=l}^{r} f(i) \]

这样计算的复杂度是 \(O(\sum_k \sqrt n_k)\)(段并起来的段数上界, 实际上还要乘上计算每一段的复杂度, 但是被我当成常数了), 在 \(k\) 较小的时候比较快, 比如这个例题, \(k=1\):[清华集训2012]模积和, 我的远古题解
再比如这个例题, \(k=2\) : YY的GCD, 我的题解

数据分治与根号平衡

大数阶乘取模
打一个表, 相邻两个数间隔一个固定的长度 \(T\), 可以以表中的一个数为起点, 空间复杂度低, 时间复杂度就降到 \(O(T)\) 了。(大概)

另一道例题
每次操作要把 \(q = O(\lceil \frac{n}{p} \rceil)\) 个数加起来, 这 \(q\) 个数是可以用 \(O(q)\) 的时间找到并加起来的, 暴力的总复杂度就是 \(O(\sum_{i=1}^m q_i)\), 要是 \(q_i\) 都很小该多好啊orz。
最终的算法就是:设定一个阈值 \(T\), 对于 \(q \le T\) 的询问暴力做, 对于 \(q \in (T,n]\) 的询问, 记录答案; 每个修改都暴力修改就行。
思考 \(q\) 很神必, 改为思考 \(p\) : 设立一个阈值 \(T\), 对于 \(p \in [1,T]\), 开一个数组 \(f[1 \le i \le T][0 \le j < T]\) 表示 膜 \(i\)\(j\) 的答案, 剩下的情况暴力算; 修改的时候在这个数组中枚举 \(i\), 维护数组。
时间复杂度如此:
询问: \(p \in [1,T], \; O(1)\)\(else, \; O(\lfloor \frac{n}{T} \rfloor)\)
修改: \(O(T)\)
这是一个可以自行平衡 查-改 复杂度的关系, 题目并没有对于 查-改 数量差的偏好, 所以规定 \(O(T) = O(\lfloor \frac{n}{T} \rfloor)\), 解得 \(T = O(\sqrt n)\)
(下一道例题会更详细地讲这类复杂度平衡的方法, 虽然只是详细了一点点, 不过更有逻辑性)

代码就可以写出来了。

using namespace std;
const int N = 150003;
const int B = 391;

int n,m,a[N];
int T, f[B][B];

int calc(int x,int y) {
    int res = 0;
    while(y <= n) {
        res += a[y];
        y += x;
    }
    return res;
}

int main() {
    scanf("%d%d", &n,&m);
    for(int i=1;i<=n;++i) scanf("%d", &a[i]);
    T = sqrt(n*1.0);
    for(int i=1;i<=T;++i)
        for(int j=1;j<=n;++j)
            f[i][j%i] += a[j];
    while(m--) {
        char cmd[3];
        int x,y;
        scanf("%s%d%d", cmd, &x, &y);
        if(cmd[0] == 'A') {
            if(x <= T) cout << f[x][y] << '\n';
            else cout << calc(x,y) << '\n';
        } else {
            for(int i=1;i<=T;++i)
                f[i][x%i] -= a[x], f[i][x%i] += y;
            a[x] = y;
        }
    }
    return 0;
}

另一道例题

题意简述:
给定节点数为 \(N \in [1,2e5]\) 的有根树, 节点有颜色 \(\in [1,R]\)\(R\) 给定; 有 \(Q \in [1,2e5]\) 次询问, 每次询问形如 \(r_1,r_2 \in [1,R]\), 要求输出满足 \(e_1\) 颜色为 \(r_1\)\(e_2\) 颜色为 \(r_2\)\(e_1\)\(e_2\) 祖先的二元组 \((e_1,e_2)\) 的个数。

\(col[x]\) 表示节点 \(x\) 的颜色
\(num[r]\) 表示颜色为 \(r\) 的节点数。

对于一个询问 \((r_1,r_2)\), 有两种计算方式:

  1. 枚举 \(e_1\) 满足 \(col[e_1] = r_1\), 对每个 \(e_1\) 分别求其子树里(自然不包括 \(e_1\)\(col[x] = r_2\) 的节点 \(x\) 的数量, 最后全加起来。
  2. 枚举 \(e_2\) 满足 \(col[e_2] = r_2\), 对每个 \(e_2\) 分别求其到根的路径上 (自然不包括 \(e_2\)\(col[x] = r_2\) 的节点 \(x\) 的数量, 最后全加起来。

由于 \(R\) 很小, 可以开一个桶, 配合 dfs 就可以实现 \(O(1)\) 回答节点 \(x\) 到根的路径上有多少个节点的颜色为 \(r\); 当然也可以实现 \(O(1)\) 回答节点 \(x\) 的子树内有多少个节点的颜色为 \(r\) (这个在实现上还要容斥一下)。

两种方法都需要明确 \(r\), 所以对于一个询问 \((r_1,r_2)\):
1.用第一种计算方式, 给颜色 \(r_1\) 挂上 \(r_2\), 每次 dfs 到 \(col[x] = r_1\)\(x\) 都查询一次, 结果加到到对应询问的答案数组里。
2.用第一种计算方式, 给颜色 \(r_2\) 挂上 \(r_1\), 每次 dfs 到 \(col[x] = r_2\)\(x\) 都查询一次, 结果加到到对应询问的答案数组里。

所以处理一个询问 \((r_1,r_2)\), 用第一种计算方式的时间复杂度是 \(O(num[r_1])\), 第二种则是 \(O(num[r_2])\)

对于一个询问, 一个自然的想法是哪种复杂度小就交给哪种。
这个策略的局限性是不能处理两种复杂度都大的情况。
然而其实并不需要考虑处理两种复杂度都大的情况。

设立一个阈值 \(T\), 对于 \(num[r_2] \le T\) 的询问, 用第二种算法, 复杂度是 \(O(qT)\)\(q\)\(n\) 同阶, 复杂度可看成 \(O(nT)\) ; 剩下的都交给第一种算法, 复杂度看上去是 \(O(qn)\)\(O(n^2)\) 的, 但是由于这样的询问的数量是 \(O(\lfloor \frac{n}{T} \rfloor)\) 的 ,如果对询问去重, 就能保证每种这样的询问(按 \(r_2\) 分类)最多会被挂到 \(O(n)\) 个节点上, 这样, 复杂度就是 \(O(n \lfloor \frac{n}{T} \rfloor)\) 的了。
为分析方便, 把这两种复杂度加起来, 得到一个不太紧的上界 \(O(nT + n\lfloor \frac{n}{T} \rfloor)\), 对 \(T\) 的升降都会使得加号的两边一个升一个降, 故加号两边相等时, 渐进上界最低, 为 \(O(n\sqrt n)\)
一般来说, 这样的复杂度式子 \(O(aT + \lfloor \frac{b}{T} \rfloor)\) 中, 若 \(a、b\) 不可自选而 \(T\) 可以自选, 那么取 \(aT = \lfloor \frac{b}{T} \rfloor\) 解出 \(T\) 来就可以让渐进上界最低, 这样的式子, 或者说这样的思想, 就叫 【根号平衡】。(大概)
话说这道题好像不去重也能过

#include<bits/stdc++.h>
using namespace std;
const int B = 453;
const int N = 200003;
const int R = 25003;

int n,q,r, color[N], T;
int ct, hd[N], nt[N<<1], vr[N<<1];
int num[R];
int r1[N], r2[N], Ans[N];

vector<int> ques[R], ques2[R];
int t1[R], t2[R];

void dfs1(int x) {
	for(int i=0;i<(int)ques[color[x]].size();++i) {
		int id = ques[color[x]][i];
		Ans[id] += t1[r1[id]];
	}
	++t1[color[x]];
	for(int i=hd[x];i;i=nt[i]) {
		int y = vr[i];
		dfs1(y);
	}
	--t1[color[x]];
}

void dfs2(int x) {
	for(int i=0;i<(int)ques2[color[x]].size();++i) {
		int id = ques2[color[x]][i];
		Ans[id] -= t2[r2[id]];
	}
	for(int i=hd[x];i;i=nt[i]) {
		int y = vr[i];
		dfs2(y);
	}
	for(int i=0;i<(int)ques2[color[x]].size();++i) {
		int id = ques2[color[x]][i];
		Ans[id] += t2[r2[id]];
	}
	++t2[color[x]];
}

void ad(int x,int y) { vr[++ct] = y; nt[ct] = hd[x]; hd[x] = ct; }
int main() {
	scanf("%d%d%d", &n, &r, &q);
	T = sqrt(n*1.0);
	
	scanf("%d", &color[1]);
	++num[color[1]];
	for(int i=2;i<=n;++i) {
		int fa;
		scanf("%d%d", &fa, &color[i]);
		ad(fa, i);
		++num[color[i]];
	}
	
	for(int i=1; i<=q; ++i) {
		scanf("%d%d", &r1[i], &r2[i]);
		if(num[r2[i]] <= T) ques[r2[i]].push_back(i);
		else ques2[r1[i]].push_back(i);
	}
	
	dfs1(1);
	dfs2(1);
	
	for(int i=1; i<=q; ++i) cout << Ans[i] << '\n';
	
	return 0;
}

再一道例题

题意简述:
给出数列 a[1~n], \(n \in [1, 3e5]\) 和若干次询问, 每次询问形如 \((x,y)\), 要求输出 \(a[x] + a[x+y] + a[x+2y] + \cdots + a[x+ky]\), 满足 \(x+ky \le n\)\(x+(k+1)y > n\), 询问总数不超过 \(3e5\)
时空限制 \(4s - 70mb\)

跟上面那题差不多, 不做了。

再一道例题
推荐用 \(\text{virtual judge}\) 交。(这里

题意简述:
给一段长度为 \(n \in [1, 300]\)\(0/1\) 串和一个正整数 \(M\), 规定一次操作可以将一个位置取反或者将一段长度为 \(M\) 倍数的前缀取反, 问最少需要多少次操作才能使这个串变成最小循环节长度为 \(M\) 的循环串。
最小循环节长度为 \(M\) 的循环串:对于任意 \(i\), 如果 \(i+M \in [1,n]\), 那么 \(i\)\(i+M\) 位置上的字符应该相等。
(例子: \(00100100\) 是个最小循环节为 \(3\) 的循环串)

解题的关键是 \(循环节长度 * 循环节个数 \approx n\), 那么这两个数值总有一个不超过 \(\sqrt n\), 分别对两种情况设计算法。
具体见参考资料《根号算法—-不只是分块》。

更多根号!

其实这节只有1道例题, 还是王悦同论文里的题orz

[JSOI2013互测 烧桥计划 BZOJ5424]
然而 bzoj 没了, 只能口胡啦。

题目描述:
给一个长度为 \(N \in [1,100000]\) 的序列 \(\{A_i\}\), 其中 \(A_i \in [1000,2000]\), 再给定一个 \(M \in [0,2e8]\)
可以选若干个数(那自然可以不选), 记为 \(A_{p_1} \cdots A_{p_k}\), 满足 \(\{p_i\}\) 是个递增序列, 产生 \(A_{p_1} + 2A_{P_2} + 3A_{p_3} + \cdots + kA_{p_k}\) 的代价; 去掉选出来的数, 剩下的数下标不变, 可以看到剩下的数组成了若干连续的段, 定义每段的权 \(T\) 为这段所有 \(A\) 的和, 每个 \(T > M\) 的段都会产生 \(T\) 的代价。
求代价最小的选数方案所产生的代价。
时限 \(7s\)

可以动态规划, 还能单调队列优化, 最终动态规划的时空复杂度为 \(O(N^2)\), 不细述。
解题的关键就是抓住 \(A_i\) 的下界, 分析最多选多少个数才可能成为最优解(最优解上界是一个数都不选的情况), 分析出来最多选 \(O(\sqrt N)\), 动态规划的时空复杂度就降为 \(O(N\sqrt N)\) 了。

posted @ 2020-08-21 09:50  xwmwr  阅读(334)  评论(0编辑  收藏  举报