【数列区间询问中的分块思想】
分块算法主要用于给定序列的区间询问问题,能够以较小的时间代价暴力求解,时间复杂度一般在O(n*n^0.5)。关键在O(1)维护好某一区间在增加或者减少一个边界元素所带来的影响。需要注意的就是在更新的区间的时候要先放大在缩小,否则可能出现当前区间左右边界互换的情况,这个影响某一些题可能没有影响,但是极有可能出错。
时间复杂度:先考虑左边界的时间复杂度,由于分成了sqrt(n)块,而同一块中左标移动的范围最多是sqrt(n),那相邻块跳转的情况呢?可以虚拟出每块中有至少一个询问进行思考,那么相邻块之间的移动次数最大为2*sqrt(n)。由于共有Q次询问,因此最终时间复杂度为O(Q*sqrt(n))。再考虑右边界,对于同一块内的右边界来说,其值是单调递增的,因此时间复杂度为O(n),相邻块跳转为O(2*n),由于共有sqrt(n)块,因此最终时间复杂度为O(n*sqrt(n))。
CF86D Powerful array
题意:给定一N个元素的序列,序列长度最多为200000,现在定义若一个数s在区间[L, R]出现Ks次,那么这个串的价值就加上Ks^2*s。例如序列3,3,5,6,5,5,那么这个串的权值为:2^2*3+3^2*5+1^1*6。现有Q(与N同)次询问,每次询问都是[L, R],要出输出该子串的权值。
分析:离线处理,将所有的询问排序,第一关键字按照左端点所在块排序,第二关键字按照右端点排序。在确保能够O(1)在某一区间扩大/缩小一单位维护出答案的情况下,这样能够保证在O(Q*n^0.5+n*n^0.5)的时间复杂度计算出来。至于O(1)的处理,只需要考虑 Ks^2*s 和 (Ks-1)^2*s 的关系即可。
#include <cstdlib> #include <cstring> #include <cstdio> #include <cmath> #include <algorithm> using namespace std; typedef long long LL; struct Node { int l, r, b, No; bool operator < (const Node &t) const { if (b != t.b) return b < t.b; return r < t.r; } }; const int N = 200005; const int M = 1000005; int n, Q; int seq[N]; Node q[N]; int cnt[M]; // 统计每个数出现了多少次,如果范围过大可以考虑离散化 LL ans[N]; LL & cal(int x, int &l, int &r, LL &ret) { int L = q[x].l, R = q[x].r; while (l > L) { --l; int t = seq[l]; ret += 2LL*cnt[t]*t+t; cnt[t]++; } while (r < R) { ++r; int t = seq[r]; ret += 2LL*cnt[t]*t+t; cnt[t]++; } while (l < L) { int t = seq[l]; ret += -2LL*cnt[t]*t+t; ++l; cnt[t]--; } while (r > R) { int t = seq[r]; ret += -2LL*cnt[t]*t+t; --r; cnt[t]--; } return ret; } void solve() { int B = (int)sqrt(1.0*n); memset(cnt, 0, sizeof (cnt)); for (int i = 1; i <= Q; ++i) { scanf("%d %d", &q[i].l, &q[i].r); q[i].b = q[i].l / B; q[i].No = i; } sort(q+1, q+1+Q); int l = q[1].l, r = q[1].r; LL ret = 0; for (int i = l; i <= r; ++i) { int t = seq[i]; ret += 2LL*cnt[t]*t+t; cnt[t]++; } ans[q[1].No] = ret; for (int i = 2; i <= Q; ++i) { ans[q[i].No] = cal(i, l, r, ret); } for (int i = 1; i <= Q; ++i) { printf("%I64d\n", ans[i]); } } int main() { while (scanf("%d %d", &n, &Q) != EOF) { for (int i = 1; i <= n; ++i) { scanf("%d", &seq[i]); } solve(); } return 0; }
HDU-4638 Group
题意:给定一个序列,序列由1-N个元素全排列而成,求任意区间连续的段数。例如序列2,3,5,6,9就是三段(2, 3) (5, 6)(9)。
分析:也是离线处理,分块排好序,O(1)的维护就是每新添加一个元素 t 查看 t-1 与 t+1 是否都在区间内,如是则段数-1,如果都不在,则段数+1。
#include <cstdlib> #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> using namespace std; const int N = 100005; struct Node { int l, r, b, No; bool operator < (const Node &t) const { if (b != t.b) return b < t.b; return r < t.r; } }; Node q[N]; int seq[N]; char vis[N]; int n, m, ret; int ans[N]; void cal(int x, int &l, int &r) { int L = q[x].l, R = q[x].r; while (l > L) { // 需要现将左右区间进行放大,否则可能出现左右游标交错的情况 --l; int t = seq[l]; vis[t] = 1; if (vis[t-1] && vis[t+1]) --ret; else if (!vis[t-1] && !vis[t+1]) ++ret; } while (r < R) { ++r; int t = seq[r]; vis[t] = 1; if (vis[t-1] && vis[t+1]) --ret; else if (!vis[t-1] && !vis[t+1]) ++ret; } while (l < L) { int t = seq[l]; vis[t] = 0; if (vis[t-1] && vis[t+1]) ++ret; else if (!vis[t-1] && !vis[t+1]) --ret; ++l; } while (r > R) { int t = seq[r]; vis[t] = 0; if (vis[t-1] && vis[t+1]) ++ret; else if (!vis[t-1] && !vis[t+1]) --ret; --r; } } void solve() { memset(vis, 0, sizeof (vis)); sort(q+1, q+m+1); ret = 0; int l = q[1].l, r = q[1].r; // 需要预处理下第一个询问的边界 for (int i = l; i <= r; ++i) { int t = seq[i]; vis[t] = 1; if (vis[t-1] && vis[t+1]) --ret; else if (!vis[t-1] && !vis[t+1]) ++ret; } ans[q[1].No] = ret; for (int i = 2; i <= m; ++i) { ans[q[i].No] = (cal(i, l, r), ret); } for (int i = 1; i <= m; ++i) { printf("%d\n", ans[i]); } } int main() { int T; scanf("%d", &T); while (T--) { scanf("%d %d", &n, &m); for (int i = 1; i <= n; ++i) { scanf("%d", &seq[i]); } int B = int(sqrt(n*1.0)); for (int i = 1; i <= m; ++i) { scanf("%d %d", &q[i].l, &q[i].r); q[i].b = q[i].l / B, q[i].No = i; } solve(); } return 0; }
HDU-4676 Sum Of Gcd
题意:给定一个1-N的全排列序列,N<=20000,有Q组询问,Q<=20000,每组询问给出左右区间[l, r],问区间内的任意两个数的gcd之和为多少?
分析:早几场多校做过一道题目是问任意区间的gcd最大值为多少。使用的离线加线段树的方法。这题的解题就需要一些数论知识知识了。对于任意一段区间,任意两个数的gcd值一定会是区间内某个数的因子,因此我们从因子出发,通过得到该区间内所有数的所有因子分别出现的次数,那么就有线索来求最终的答案了。例如区间内数为[3, 6, 9, 36, 72],那么找到因子3,其出现的次数为5次,记为<3, 5>同样的有<2, 3>, <4, 2>, <6, 3>, <8,1>, <9, 3>, <18, 2>, <36, 2>, <72, 1>,如果此时以为对任意一个因子的个数 t ,取C(2, t)就错了,且不说因子个数只有一个的数不能够成为公因子(例如<8, 1>, <72, 1>等),就算成为公因子也未必是最大的公因子(例如<18, 2>等)。实际在统计的过程中不采用组合数的方式,而采用观察单个数带来的影响的方式。考虑到已知数 A 的因子 d 在区间其他数中出现了 k 次,那么如果 d 是所有gcd(A, other)的值时,那么最终的结果加上k*d,但这是不一定的,且 d 的因子肯定也是 A 的因子,当枚举到 d 的因子 d' 时显然就不能够加了。当务之急是找到某种方式使得所有的因子都被统计又不会是结果增多,没错,容斥。不过这里的容斥有点不太一样,而是给每个因子一个容斥因子f(d)使得 k*f(d) + k*f(d') + k*f(d'') + ... + k*f(1) = k*d,这样如果 d 出现 k 次,那么其因子的因子等等就也会出现 k 次,而将他们都统计起来的最终结果就是等效于 k*d。可以证明f(d) = phi(d),后者为欧拉函数。其实也即是 n = sum{phi(d) , d|n}。有了这些结论之后,把20000之内的数全部求出因子,然后对询问区间做一个起点以sqrt(n)分块的排序,做到O(1)的更新到每一个区间,每次讲边界的数进行更新,求出答案。
#include <cstdio> #include <cstring> #include <cstdlib> #include <cmath> #include <algorithm> #include <vector> using namespace std; typedef long long LL; const int N = 20005; int M; struct Node{ int l, r, No; bool operator < (const Node &t) const { int a = l/M, b = t.l/M; // 计算出端点落在以sqrt(n)分块的哪一块 if (a != b) return a < b; return r < t.r; } }; int n, seq[N]; Node q[N]; int phi[N]; vector<int>v[N]; int num[N]; LL ans[N]; void prepare() { for (int i = 1; i < N; ++i) { for (int j = i; j < N; j += i) { v[j].push_back(i); } } // 处理每个数的因子 // 依据phi[p^n] = n-1*n^(n-1)而来 for (int i = 1; i < N; ++i) phi[i] = i; for (int i = 2; i < N; ++i) { if (phi[i] == i) {// 说明 i 是一个质数 for (int j = i; j < N; j += i) { phi[j] = phi[j]/i*(i-1); } } } } LL modify(int x, int f) { LL ret = 0; for (int i = 0; i < (int)v[x].size(); ++i) { const int &d = v[x][i]; if (f == -1) ret += phi[d] * (--num[d]); else ret += phi[d] * (num[d]++); } return ret; } void solve() { int Q; scanf("%d", &Q); memset(num, 0, sizeof (num)); for (int i = 1; i <= Q; ++i) { q[i].No = i; scanf("%d %d", &q[i].l, &q[i].r); } M = (int)sqrt(n); sort(q+1, q+1+Q); LL ret = 0; int lb = q[1].l, rb = q[1].r; // 首先处理出第一个边界 for (int i = lb; i <= rb; ++i) { ret += modify(seq[i], +1); } ans[q[1].No] = ret; for (int i = 2; i <= Q; ++i) { int L = q[i].l, R = q[i].r; while (lb > L) ret += modify(seq[--lb], +1); // 首先进行区间放大 while (rb < R) ret += modify(seq[++rb], +1); while (lb < L) ret -= modify(seq[lb++], -1); while (rb > R) ret -= modify(seq[rb--], -1); ans[q[i].No] = ret; } for (int i = 1; i <= Q; ++i) { printf("%I64d\n", ans[i]); } } int main() { prepare(); int T, ca = 0; scanf("%d", &T); while (T--) { scanf("%d", &n); for (int i = 1; i <= n; ++i) { scanf("%d", &seq[i]); } printf("Case #%d:\n", ++ca); solve(); } return 0; }