题解 [六省联考2017] 相逢是问候
题目链接
题目描述
维护一个序列,支持区间求和,以及将区间内每一个数 \(a_i\) 变成 \(c^{a_i}\) (c为给定的常数), 并要求结果对 \(p\) 取模 ( \(p\) 不一定是质数 )
数列元素个数 \(n \leq 10^5\), 操作次数 \(m \leq 10^5\), \(1 \leq c < p \leq 10^8\)
注 : 以下称 “将区间内每一个数 \(a_i\) 变成 \(c^{a_i}\)” 为 “操作”
分析
一道看着就很线段树的题目 ...
首先我们发现区间和似乎并不是那么好维护:假定我们维护了 \(a_i\) 的区间和,但我们并不能很快的推出 \(c^{a_i}\) 的区间和,因为从 \(a_i\) 到 \(c^{a_i}\), 区间内的每一个元素的变化几乎完全没有相关性,并不像区间加或区间和一样能方便的维护。
但除了线段树外,我们应该还会很自然的想到扩展欧拉定理
那么很显然的,当 \(a_i\) 对应的模数 \(\varphi(\varphi(\varphi(\varphi(...p)))) = 1\) 时,不论再怎么操作 \(a_i\) 也不会变化了.
而对一个数 \(p\) 不停地取 \(\varphi\) ,在取了 \(\log p\) 次后这个数必然会变成 \(1\).
那么对于序列中所有的数,他们的总操作次数就应该不超过 \(n \log p\).
也就是说,对于每一个区间操作,我们只需要操作其中没有操作到 \(\log p\) 次的数, 其他的数可以不管他.
那显然可以考虑预处理出每个数在前 \(\log p\) 次操作下分别会变成多少,操作时单点修改即可,复杂度就是 \(O(n\log n\log p)\).
这样做还有个的好处:我们可以用树状数组代替掉线段树.
至于如何在区间操作中快速找到哪些数的操做次数小于 \(\log p\), 我们可以考虑使用逐渐变形成并查集的单向链表(我自己瞎扯的名字)
具体而言就是,我们有一个 \(next\) 数组, \(next_i\) 表示下标大于等于 i 的数中第一个操作次数小于 \(\log p\) 的数 (即继续操作可能会改变数值的数) 的下标,并有一个 \(time\) 数组记录每个数当前的操作次数.
显然初始时 \(next_i = i + 1\).
那么查询下标大于等于 i 的第一个操作次数小于 \(\log p\) 的数可以这样实现:
int get(int i) {
if (time[i] < logp)
return i;
else
return next[i] = get(next[i]);
}
这应该比较直观的体现了他为什么是"逐渐变形成并查集的单向链表"......
代码
#include <bits/stdc++.h>
#define lowbit(x) x&-x
using namespace std;
typedef long long LL;
const int N = 50010;
const int lam = 10;
LL n, m, p, c, logcp, top, nxt[N], tree[N], a[N], tms[N], phi[N], val[lam + 10][N];
inline int read()
{
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9') { if(c == '-') f = -1; c = getchar(); }
while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
LL ksm(LL x, LL p)
{
LL cur = c, res = 1;
for(; x; x >>= 1, cur = cur * cur % p) if(x & 1) res = res * cur % p;
return res;
}
int get_phi(int x) //暴力求欧拉函数
{
LL res = 1;
for(int i = 2; i * i <= x; i++){
if(x % i == 0){
res *= (i - 1), x /= i;
while(x % i == 0) res *= i, x /= i;
}
}
return (x == 1) ? res : res * (x - 1);
}
int dfs(int x, int num, int t)//深搜扩展欧拉定理
{
if(phi[t] == 1) return 1;
if(!num) return a[x];
if(num < top) return val[num][x]; //特判不能用扩展欧拉定理的部分
return ksm(dfs(x, num - 1, t + 1), phi[t]) + phi[t];
}
void prework()//预处理出a[i]操作b次后会变成多少,即val[b][i]
{
LL o = 1;
if(c == 1) logcp = 1e9;
else while(o < p) logcp++, o = o * c;
//logcp 就是以c为底p的对数
int cur = p; phi[0] = p;
for(int i = 1; i <= lam; i++){
int now = get_phi(cur);
phi[i] = cur = now;
}
for(int j = 1; j <= n; j++){
val[0][j] = a[j];
bool flag = true;
for(int i = 1; i <= lam; i++)
if(val[i - 1][j] < logcp && flag) val[i][j] = ksm(val[i - 1][j], p); //特判不能用扩展欧拉定理的部分
else{
if(flag) flag = false, top = i;
val[i][j] = dfs(j, i, 0) % p;
}
}
}
void add(int pos, int val)
{
for(; pos < N; pos += lowbit(pos))
tree[pos] = (tree[pos] + val + p) % p;
}
LL que(int pos)
{
LL res = 0;
for(; pos; pos -= lowbit(pos)) res = (res + tree[pos]) % p;
return res;
}
int get(int now)
{
if(tms[now] < lam) return now;
else return nxt[now] = get(nxt[now]);
}
int main()
{
n = read(), m = read(), p = read(), c = read();
for(int i = 1; i <= n; i++) a[i] = read(), nxt[i] = i + 1;
for(int i = 1; i <= n; i++) add(i, a[i]);
prework();
for(int i = 1, op, l, r; i <= m; i++){
op = read(), l = read(), r = read();
if(op) printf("%lld\n", (que(r) - que(l - 1) + p) % p);
else{
int cur = get(l);
while(cur <= r){
LL x = val[tms[cur] + 1][cur], de = x - a[cur];
add(cur, de);
a[cur] = x, tms[cur] ++, cur = get(nxt[cur]);
}
}
}
return 0;
}
顺便一提,虽然说理论上每个数需要操作 \(\log p\) 次才不会变化,但这题数据比较水,每个数操作 5 次就可以不管他了.
还有就是一定要注意扩展欧拉定理需要满足指数不小于 \(\varphi(p)\).

浙公网安备 33010602011771号