2013 Multi-University Training Contest 8
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)分块的排序(不这样会TLE)动态的更新到每一个区间,每次讲边界的数进行更新,求出答案。
一段关于分块算法时间复杂度的分析:http://blog.csdn.net/yang_7_46/article/details/9618637
时间复杂度分析:
排完序后,对于相邻的两个查询,left值之间的差最大为sqrt(n),则相邻两个查询左端点移动的次数<=sqrt(n),总共有t个查询,则复杂度为O(t*sqrt(n))。又对于相同块内的查询,right端点单调上升,每一块所有操作,右端点最多移动O(n)次,总块数位sqrt(n),则复杂度为O(sqrt(n)*n)。right和left的复杂度是独立的,因此总的时间复杂度为O(t*sqrt(n) + n*sqrt(n))。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#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); } } // 处理每个数的因子 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; }