这题好强强啊,貌似是集训队原题?集训队原题当中值域是1e9的范围,这样各种乱搞是妥妥的不能过了,只能写正解的离线+树状数组维护前缀积。
最开始我写了几种乱搞做法,包括莫队和线段树做法。其中表现比较优秀的是线段树的做法,非常的暴力,就是每一个区间都维护vector记录区间lcm的质因数分解结果合并区间归并排序一路维护就可以了。由于51nod数据比较弱,数据值域大小有限,所以还可以跑过大部分的点,但A掉仍然是不能够。
线段树乱搞:
#include <bits/stdc++.h> using namespace std; #define maxn 1000000 #define mod 1000000007 #define LL long long int n, m, a[maxn]; int tot, rec[maxn], prime[maxn]; bool is_prime[maxn]; int read() { int x = 0, k = 1; char c; c = getchar(); while(c < '0' || c > '9') { if(c == '-') k = -1; c = getchar(); } while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar(); return x * k; } struct node { int num, cnt; node(int a = 0, int b = 0) { num = a, cnt = b; } friend bool operator <(const node& a, const node& b) { if(a.num != b.num) return a.num < b.num; return a.cnt > b.cnt; } friend bool operator ==(const node& a, const node& b) { return a.num == b.num; } }; vector <node> T[maxn], S, tem1, tem2; void Get_Prime() { for(int i = 2; i < maxn; i ++) { if(!is_prime[i]) prime[++ tot] = i, rec[i] = i; for(int j = 1; j <= tot; j ++) { if(i * prime[j] >= maxn) break; is_prime[i * prime[j]] = 1; rec[i * prime[j]] = prime[j]; if(!(i % prime[j])) break; } } } LL Qpow(LL x, int times) { LL base = 1; for(; times; times >>= 1, x = (x * x) % mod) if(times & 1) base = (base * x) % mod; return base; } void Push(vector <node> &S, node a) { if(!S.size() || a.num != S[S.size() - 1].num) S.push_back(a); } void Merge(vector <node> &M1, vector <node> &M2) { int i = 0, j = 0; tem1.resize(0), tem2.resize(0); for(int i = 0; i < M1.size(); i ++) tem1.push_back(M1[i]); for(int i = 0; i < M2.size(); i ++) tem2.push_back(M2[i]); M1.resize(0); while(i < tem1.size() && j < tem2.size()) { if(tem1[i] < tem2[j]) Push(M1, tem1[i]), i ++; else Push(M1, tem2[j]), j ++; } while(i < tem1.size()) Push(M1, tem1[i]), i ++; while(j < tem2.size()) Push(M1, tem2[j]), j ++; } void Build(int p, int l, int r) { if(l == r) { int tem = a[l], last = -1; while(tem != 1) { if(T[p].size() && rec[tem] == T[p][last].num) T[p][last].cnt ++; else T[p].push_back(node(rec[tem], 1)), last ++; tem /= rec[tem]; } return; } int mid = (l + r) >> 1; Build(p << 1, l, mid), Build(p << 1 | 1, mid + 1, r); T[p].resize(0); for(int i = 0; i < T[p << 1].size(); i ++) T[p].push_back(T[p << 1][i]); Merge(T[p], T[p << 1 | 1]); } void Query(int p, int l, int r, int L, int R) { if(L <= l && R >= r) { Merge(S, T[p]); return; } if(L > r || R < l) return; int mid = (l + r) >> 1; Query(p << 1, l, mid, L, R), Query(p << 1 | 1, mid + 1, r, L, R); } signed main() { Get_Prime(); n = read(), m = read(); for(int i = 1; i <= n; i ++) a[i] = read(); Build(1, 1, n); for(int i = 1; i <= m; i ++) { int L = read(), R = read(); S.resize(0); Query(1, 1, n, L, R); LL ret = 1; for(int i = 0; i < S.size(); i ++) ret = (ret * Qpow(S[i].num, S[i].cnt)) % mod; printf("%lld ", ret); } return 0; }
现在来介绍一下正解的做法。有一个很妙的转化:lcm为各个数质因数分解之后每一位取max之后相乘,这样不是很好做。但我们对于一个数(p^{a}),可以分析发现它对于答案的贡献正好就是(a)次。我们可以把它看做是(p^{1}, p^{2}...p^{a})这样(a)个不同的数,每个数对于答案均有(*p)的贡献(其中一个区间内相同的数只产生一次贡献)。
那么现在我们的问题转化为了问一个区间中有多少个不同的数并将它们的权值累乘起来。一个区间当中有多少个不同的数,这里有一个常见的转化,即为统计一个区间当中有多少个(last[i] < l)的数((last[i]) 表示 (i) 这个数字上一次出现的位置,没有出现可以视作 (0))。那么我们可以离线 + 树状数组,按照左端点排序依次处理。维护一个指针 (now),树状数组中维护的值为所有范围在 (now --> tot) 的数字之间 (last[i] < now) 的数字权值积。如何维护?只需要在一个数 (< now) 被从树状数组中删去的时候向树状数组中加入它的后继元素即可。
#include <bits/stdc++.h> using namespace std; #define maxn 50005 #define mod 1000000007 #define LL long long #define lowbit(i) (i & (-i)) int n, m, tot, a[maxn << 4], b[maxn << 4]; int C[maxn << 4], nxt[maxn << 4], last[maxn << 4]; int l[maxn], r[maxn], rec[maxn], ans[maxn]; int read() { int x = 0, k = 1; char c; c = getchar(); while(c < '0' || c > '9') { if(c == '-') k = -1; c = getchar(); } while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar(); return x * k; } struct node { int l, r, id; node(int a = 0, int b = 0, int c = 0) { l = a, r = b, id = c; } friend bool operator <(const node& x, const node& y) { return x.l < y.l; } }Q[maxn]; int Qpow(int x, int times) { int base = 1; for(; times; times >>= 1, x = ((LL) x * x) % mod) if(times & 1) base = ((LL) base * x) % mod; return base; } void update(int x, int y) { for(; x <= tot; x += lowbit(x)) C[x] = (LL) C[x] * y % mod; } int query(int x) { int ret = 1; for(; x; x -= lowbit(x)) ret = (LL) ret * C[x] % mod; return ret; } int main() { n = read(), m = read(); for(int i = 1; i <= n; i ++) { int x = read(), t = sqrt(x); if(x == 1) continue; l[i] = tot + 1; for(int j = 2; j <= t; j ++) { if(!(x % j)) { int k = j; while(!(x % j)) { x /= j; a[++ tot] = k; b[tot] = j; k *= j; } } } if(x > 1) ++ tot, a[tot] = b[tot] = x; r[i] = tot; } for(int i = 1; i <= tot; i ++) C[i] = 1; for(int i = 1; i <= tot; i ++) { if(rec[a[i]]) nxt[rec[a[i]]] = i; else update(i, b[i]); rec[a[i]] = i; } for(int i = 1; i <= m; i ++) { int x = read(), y = read(); Q[i] = node(l[x], r[y], i); } sort(Q + 1, Q + 1 + m); for(int i = 1, j = 0; i <= tot; i ++) { while(Q[j + 1].l == i) ++ j, ans[Q[j].id] = query(Q[j].r); update(i, Qpow(b[i], mod - 2)); if(nxt[i]) update(nxt[i], b[nxt[i]]); } for(int i = 1; i <= m; i ++) printf("%d ", ans[i]); return 0; }
其实感觉这么多的题目做下来这个转化还是比较常见的。当一个数(p)对于它的(k)倍 (k) 次方产生为 (k) 的贡献时,我们就可以考虑将其看做 (p, 2 * p, 3 * p...) 或 (p^{1}, p^{2}, p^{3}...) 这么些不同的数字分别对它产生了为 (1) 的贡献,累加或累乘(贡献为乘积形式时)即可。如 Test 4 的 第一题、对于 (N!) 分解质因数等。
而这个 (last[i] < l) 对于处理“在区间中只出现了一次”也是一个很棒也很常见的思路。好题!好好记录一下( • ̀ω•́ )✧