题目传送门:https://pintia.cn/market/item/1442013218528759808
题意:
简要概述下题目:
对于长度为n的区间执行以下两个操作:
- 区间[l, r]范围的所有数字都乘以w
- 求区间[l, r]范围内所有 数字的欧拉函数 的总和
题解: 首先,由于涉及到区间乘,所以需要想到线段树。
但是对应的查询操作的结果是欧拉函数值,而不是原数值。
所以这里涉及到势能线段树(一种可以支持优雅的暴力的线段树)。
这里提供几道题目来学习势能线段树:
洛谷 P4145 上帝造题的七分钟 2 / 花神游历各国
CF 438D The Child and Sequence
那么,对于本题而言,我们需要知道欧拉函数的两个性质。
- 设p为质数,若 p ∣ n p|n p∣n 且 p 2 ∣ n p^2 | n p2∣n,则 ϕ ( n ) = ϕ ( n / p ) ∗ p \phi(n) = \phi(n/p) * p ϕ(n)=ϕ(n/p)∗p
- 设p为质数,若 p ∣ n p|n p∣n 且 p 2 ∤ n p^2 \nmid n p2∤n,则 ϕ ( n ) = ϕ ( n / p ) ∗ ( p − 1 ) \phi(n) = \phi(n/p) *(p-1) ϕ(n)=ϕ(n/p)∗(p−1)
通俗的来说(可能不太严谨):
- 对于一个数val,如果乘以一个质数w,此时w为val的因子,那么 v a l ∗ w val*w val∗w 的欧拉函数 等于 val的欧拉函数 * w,即 ϕ ( v a l ∗ w ) = ϕ ( v a l ) ∗ w \phi(val*w) = \phi(val) * w ϕ(val∗w)=ϕ(val)∗w
- 对于一个数val,如果乘以一个质数w,此时w不为val的因子,那么
v
a
l
∗
w
val*w
val∗w 的欧拉函数 等于 val的欧
拉函数 * (w-1),即 ϕ ( v a l ∗ w ) = ϕ ( v a l ) ∗ ( w − 1 ) \phi(val*w) = \phi(val) *(w-1) ϕ(val∗w)=ϕ(val)∗(w−1)
由于本题的题目数据范围只有100,那么我们可能先预处理所有数的所有质因子以及其出现次数。
那么对于每个区间乘操作,也就是说 [l, r] 区间 *w 的时候(此时w不一定为质数),我们把w分解成若干个质数相乘(算术基本定理,本题数据较小,也可暴力算)。
也就是说,我们把一个区间乘w的操作,分解成若干个区间乘 p 的操作(p为w的质因子)。
又由于100范围的质数只有25个,所以实际上分解了一定次数之后,每个位置的都包含了大多数质因子,此时只需要对区间 *w即可,无需分解(性质1)
代码及注释如下:
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<vector>
#include<string>
#include<sstream>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<utility>
#include<bitset>
#include<algorithm>
#define fast ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
#define ll long long
#define INF 0x3f3f3f3f
#define LINF 0x3f3f3f3f3f3f3f3f
#define pii pair<int,int>
#define fi first
#define se second
#define eps 1e-6
#define lson rt<<1
#define rson rt<<1|1
using namespace std;
const int MAXN = 1e5+5;
const int MOD = 998244353;
const double pi = acos(double(-1));
struct node {
int l, r;
ll val, lazy; //val为当前节点值(区间和)
bitset<30> mark; //记录是否包含对应位置的质因子(如果为1表示包含)
}tree[MAXN<<2];
bitset<30> all[101]; //记录所有数字的质因子包含情况(方便线段树初始化)
int a[MAXN]; //原数据
int prim[30], vis[101], num[101][30], phi[101];//prim记录质数,vis表示当前数是否为质数
//num记录每个值对应位置质数的出现次数,phi记录对应欧拉函数值(方便线段树初始化)
int cnt; //记录质数数量
int cal_phi(int n) { //计算n的欧拉函数值
int m = int(sqrt(n + 0.5));
int ans = n;
for (int i = 2; i <= m; i++)
if (n % i == 0) {
ans = ans / i * (i - 1);
while (n % i == 0) n /= i;
}
if (n > 1) ans = ans / n * (n - 1);
return ans;
}
void init() { //初始化对应信息
for(int i = 2;i <= 100;i++) { //计算100范围内的质数
if(vis[i]==0) {
prim[++cnt] = i;
}
for(int j = i + i;j <= 100;j += i) vis[j] = 1;
}//cnt为25
for(int i = 2;i <= 100;i++) { //遍历所有值
for(int j = 1;j <= cnt;j++) { //遍历所有的质数
int tmp = i;
while(tmp%prim[j]==0) num[i][j]++, tmp /= prim[j];
all[i][j] = num[i][j]; //记录i值中j质数出现的次数
}
}
for(int i = 1;i <= 100;i++) phi[i] = cal_phi(i);//计算所有欧拉函数
}
void push_up(int rt) { //更新值
tree[rt].val = (tree[lson].val + tree[rson].val)%MOD;
//左右子区间都需要包含对应质数,所以取 &
tree[rt].mark = tree[lson].mark & tree[rson].mark;
}
//rt为当前节点编号,l为左区间,mid中间值,r为右区间
void push_down(int rt, int l, int mid, int r) {//传递标记
if(tree[rt].lazy!=1) {
//标记下传
tree[lson].lazy = (tree[lson].lazy * tree[rt].lazy)%MOD;
tree[rson].lazy = (tree[rson].lazy * tree[rt].lazy)%MOD;
//计算标记对子区间的贡献
tree[lson].val = (tree[lson].val * tree[rt].lazy)%MOD;
tree[rson].val = (tree[rson].val * tree[rt].lazy)%MOD;
tree[rt].lazy = 1;//标记还原为1
}
}
//rt为当前节点,[l, r]为初始化区间范围
void build(int rt, int l, int r) { //初始化线段树
tree[rt].l = l, tree[rt].r = r; //当前节点信息
tree[rt].lazy = 1;
if(l==r) { //叶子节点
tree[rt].val = phi[a[l]];
tree[rt].mark = all[a[l]];
return;
}
int mid = (l+r)>>1;
build(lson, l, mid); //分别建立左右子树
build(rson, mid+1, r);
push_up(rt);
}
//rt为当前节点,[l, r]为查询区间
ll query(int rt, int l, int r) { //查询区间[l, r]的值
int L = tree[rt].l, R = tree[rt].r;
if(l <= L && R <= r) {//包含当前节点,直接返回节点值
return tree[rt].val%MOD;
}
ll ans = 0;
int mid = (L+R)>>1;
push_down(rt, L, mid, R); //下传标记
//查询时, [l, r]区间大小不要变化
if(l <= mid) ans = (ans + query(lson, l, r))%MOD;
if(r > mid) ans = (ans + query(rson, l, r))%MOD;
return ans;
}
//rt为当前节点,[l, r]为操作区间,pos为对应的质数位置,num为该质数需要操作的次数
void update(int rt, int l, int r, int pos, int num) {//修改区间[l, r]
int L = tree[rt].l, R = tree[rt].r;
//如果包含该节点区间,并且该区间所有位置包含了当前质数,区间更新
if(l <= L && R <= r && tree[rt].mark[pos]) {
for(int i = 1;i <= num;i++) { //操作num次
tree[rt].val = (tree[rt].val * prim[pos]) % MOD;
tree[rt].lazy = (tree[rt].lazy * prim[pos]) % MOD;
}
return;
}
//否则需要逐个修改(遍历到叶节点位置)
if(L==R) {
//第一次不包括当前质数
tree[rt].val = (tree[rt].val * (prim[pos]-1))%MOD;
tree[rt].mark[pos] = 1; //记得标记!!!
//num-1次操作时,包含了当前质数
for(int i = 1;i < num;i++) {
tree[rt].val = (tree[rt].val * prim[pos])%MOD;
}
return;
}
int mid = (L+R)>>1;
push_down(rt, L, mid, R); //下传标记
//分别判断左右区间,若包括则继续更新
if(l <= mid) update(lson, l, r, pos, num);
if(r > mid) update(rson, l, r, pos, num);
push_up(rt); //更新值
}
int main() {
fast;
init();//初始化对应信息(质数,欧拉函数等)
int n, m;
cin>>n>>m;
for(int i = 1;i <= n;i++) {
cin>>a[i];
}
build(1, 1, n);//初始化建树
while(m--) {
int op, l, r, w;
cin>>op>>l>>r;
if(op==0) {
cin>>w;
for(int i = 1;i <= cnt;i++) { //遍历所有的质数(共cnt个)
if(num[w][i]) {//若当前w包含了i位置的质数,则需要操作
update(1, l, r, i, num[w][i]);
}
}
}
else {
cout<<query(1, l, r)<<"\n";
}
}
return 0;
}