The 2021 ICPC Asia Regionals Online Contest (II) L Euler Function (题解+代码)

题目传送门: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 pn p 2 ∣ n p^2 | n p2n,则 ϕ ( n ) = ϕ ( n / p ) ∗ p \phi(n) = \phi(n/p) * p ϕ(n)=ϕ(n/p)p
  • 设p为质数,若 p ∣ n p|n pn p 2 ∤ n p^2 \nmid n p2n,则 ϕ ( n ) = ϕ ( n / p ) ∗ ( p − 1 ) \phi(n) = \phi(n/p) *(p-1) ϕ(n)=ϕ(n/p)(p1)

通俗的来说(可能不太严谨):

  • 对于一个数val,如果乘以一个质数w,此时w为val的因子,那么 v a l ∗ w val*w valw 的欧拉函数 等于 val的欧拉函数 * w,即 ϕ ( v a l ∗ w ) = ϕ ( v a l ) ∗ w \phi(val*w) = \phi(val) * w ϕ(valw)=ϕ(val)w
  • 对于一个数val,如果乘以一个质数w,此时w不为val的因子,那么 v a l ∗ w val*w valw 的欧拉函数 等于 val的欧
    拉函数 * (w-1),即 ϕ ( v a l ∗ w ) = ϕ ( v a l ) ∗ ( w − 1 ) \phi(val*w) = \phi(val) *(w-1) ϕ(valw)=ϕ(val)(w1)

由于本题的题目数据范围只有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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值