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)) 了。

  • 相关阅读:
    STDMETHOD_,STDMETHOD,__declspec(novtable)和__declspec(selectany)
    __stdcall 与 __cdecl
    winows 进程通信的实例详解
    Windows 下多线程编程技术
    MFC/VC++ UI界面美化技术
    VC++中 wstring和string的互相转换实现
    VS2010项目转化为VS2008项目
    VC++ 响应回车键的2种方法
    高效 告别996,开启java高效编程之门 2-4实战:单一条件参数化
    高效 告别996,开启java高效编程之门 2-3实战:硬编码业务逻辑
  • 原文地址:https://www.cnblogs.com/tztqwq/p/13533007.html
Copyright © 2011-2022 走看看