zoukankan      html  css  js  c++  java
  • 根号科技概览Chapter1 -- 前言与小技巧(alpha版)


    参考资料

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


    前言

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


    自然整除分块

    其实就是一个结论, 对于整数 (n,k), 整除运算 (lfloor frac{n}{k} floor) 的不同的值的个数是 (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} floor lfloor frac{n_2}{i} floor cdots lfloor frac{n_k}{i} floor f(i) ]

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

    [lfloor frac{n_1}{l} floor lfloor frac{n_2}{l} floor cdots lfloor frac{n_k}{l} floor 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} ceil)) 个数加起来, 这 (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} floor))
    修改: (O(T))
    这是一个可以自行平衡 查-改 复杂度的关系, 题目并没有对于 查-改 数量差的偏好, 所以规定 (O(T) = O(lfloor frac{n}{T} floor)), 解得 (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] << '
    ';
                else cout << calc(x,y) << '
    ';
            } 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} floor)) 的 ,如果对询问去重, 就能保证每种这样的询问(按 (r_2) 分类)最多会被挂到 (O(n)) 个节点上, 这样, 复杂度就是 (O(n lfloor frac{n}{T} floor)) 的了。
    为分析方便, 把这两种复杂度加起来, 得到一个不太紧的上界 (O(nT + nlfloor frac{n}{T} floor)), 对 (T) 的升降都会使得加号的两边一个升一个降, 故加号两边相等时, 渐进上界最低, 为 (O(nsqrt n))
    一般来说, 这样的复杂度式子 (O(aT + lfloor frac{b}{T} floor)) 中, 若 (a、b) 不可自选而 (T) 可以自选, 那么取 (aT = lfloor frac{b}{T} floor) 解出 (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] << '
    ';
    	
    	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)

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

    再一道例题
    推荐用 ( ext{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(Nsqrt N)) 了。

  • 相关阅读:
    poj 1789 每个字符串不同的字母数代表两个结点间的权值 (MST)
    poj 1251 poj 1258 hdu 1863 poj 1287 poj 2421 hdu 1233 最小生成树模板题
    poj 1631 最多能有多少条不交叉的线 最大非降子序列 (LIS)
    hdu 5256 最少修改多少个数 能使原数列严格递增 (LIS)
    hdu 1025 上面n个点与下面n个点对应连线 求最多能连有多少条不相交的线 (LIS)
    Gym 100512F Funny Game (博弈+数论)
    UVa 12714 Two Points Revisited (水题,计算几何)
    UVa 12717 Fiasco (BFS模拟)
    UVa 12718 Dromicpalin Substrings (暴力)
    UVa 12716 && UVaLive 6657 GCD XOR (数论)
  • 原文地址:https://www.cnblogs.com/tztqwq/p/13533007.html
Copyright © 2011-2022 走看看