刷题记录:牛客NC17383 A Simple Problem with Integers 线段树+循环节

传送门:牛客

题目描述:

You have N integers A1, A2, ... , AN. You are asked to write a program to receive and execute two kinds of instructions:
1. C a b means performing Ai = (Ai2 mod 2018) for all Ai such that a ≤ i ≤ b.
2. Q a b means query the sum of Aa, Aa+1, ..., Ab. Note that the sum is not taken modulo 2018.
输入:
1
8
17 239 17 239 50 234 478 43
10
Q 2 6
C 2 7
C 3 4
Q 4 7
C 5 8
Q 6 7
C 1 8
Q 2 5
Q 3 4
Q 1 8 
输出:
Case #1:
779
2507
952
6749
3486
9937

此题的出题背景是线段树维护区间自幂取模+区间和,差不多是一道结论题,但是细节较多,实现难度依旧较大

可以直接去做洛谷上的一道更为宽泛的题目[THUSC2015]平方运算,那道题没有限制模数,更为普适,所以下面的讲解和代码都是根据那道题来的,当然那道题的代码改一下便可以过此题

对于区间自幂并且取模,我们会发现肯定存在一个循环节.为什么呢,因为对于一个数来说,他一直进行平方操作,即使它每一次平方完取模的值都不一样.因为模数固定,根据抽屉原理,迟早有一次会重复的,所以必存在一个循环节.

那么对于一个进入循环节的区间来说,我们再对这个区间进行几次区间平方操作,显然我们的区间和就是在一个循环节里面循环.但是此时我们需要注意的是,当我们对儿子区间单独进行操作之后,父亲原本的循环节会因儿子区间的修改而改变.具体来讲,对于一个进入循环节的区间,我们可以准备好这个区间顺环节里的所有值,这样的话,我们在循环节里面进行循环就更为轻松的修改了.

对于没有进入循环节的区间来说,我们直接对这个区间进行暴力修改,也就是直接修改这个区间里面的每一个值.对于每一次被修改后的值,我们都进行一个判断,判断其是否进入一个循环.

对于我们的循环节来说,我们可以通过枚举暴力来直接求出对于每一个数字的循环节长度,当然,对于这种求循环节的,我们还有一种比较优雅的求法,那就是 F l o d y Flody Flody判环(龟兔赛跑算法),感兴趣的可以去学习一下,我感觉这个算法还是很妙的.给一个学习链接龟兔赛跑算法.采用龟兔赛跑算法可以很优雅的求出循环节以及需要进行几步进入到循环节中.在本题中,每一个数字的循环节可能长度并不相同,我们可以对此求一个 L C M LCM LCM,这样的话既可以保证顺环节的正确性,又可以使我们后面进行循环维护十分的方便

对于本题的模数来说,我们所有数字进入的循环节长度并不长,你会发现甚至小于等于11,所以对于本题来说,此做法复杂度时可以接受的

因为本题处理细节较多,将会在代码中加入一些注释帮助读者理解,考虑到绝大部分会有疑问的地方,因此可以将代码和上述讲解结合起来理解


下面是具体的代码部分(为洛谷上的AC代码):

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define root 1,n,1
#define ls rt<<1
#define rs rt<<1|1
#define lson l,mid,rt<<1
#define rson mid+1,r,rt<<1|1
inline ll read() {
	ll x=0,w=1;char ch=getchar();
	for(;ch>'9'||ch<'0';ch=getchar()) if(ch=='-') w=-1;
	for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
	return x*w;
}
#define maxn 100100
const double eps=1e-8;
#define	int_INF 0x3f3f3f3f
#define ll_INF 0x3f3f3f3f3f3f3f3f
struct Segment_tree{
	int l,r,lazy,flag,sum[80];
	//lazy用来记录区间进行过几次平方修改,对于没有进入循环的区间来说,我们是直接暴力修改单点的,所以此时不累加lazy
	//flag用来记录这个区间是否已经进入循环
	//sum[]用来记录一个区间接下来所有循环节的数字,sum[0]表示当前和,sum[1]表示当前区间进行一次操作之后的区间和
}tree[maxn*4];
int cirlen[maxn];int check[maxn];int vis[maxn];
int n,m,p;int a[maxn];int LCM;
inline int gcd(int a,int b) {
	if(a%b==0) return b;
	else return gcd(b,a%b);
} 
inline int lcm(int a,int b) {
	return a*b/gcd(a,b);
}
void pushup(int rt) {
//对于pushup来说,只有当左右的儿子都已经进入循环节时,此时父亲才进入循环节
	tree[rt].flag=tree[ls].flag&&tree[rs].flag;
	if(tree[rt].flag) {
		for(int i=0;i<LCM;i++) {
		//这里是因为儿子区间可以已经进行过改变,所以此时父亲的循环节也相应的发生了变化
			tree[rt].sum[i]=tree[ls].sum[i]+tree[rs].sum[i];
		}
	}
	else {
		tree[rt].sum[0]=tree[ls].sum[0]+tree[rs].sum[0];
	}
}
void build(int l,int r,int rt) {
	tree[rt].l=l;tree[rt].r=r;
	if(l==r) {
		tree[rt].sum[0]=a[l];
		if(check[a[l]]) {//可能这个数字刚开始便是进入循环节的,比如0,1等等
			tree[rt].flag=1;
			for(int i=1;i<LCM;i++) {
				tree[rt].sum[i]=tree[rt].sum[i-1]*tree[rt].sum[i-1]%p;
			}
		}
		return ;
	}
	int mid=(l+r)>>1;
	build(lson);build(rson);
	pushup(rt);
}
int num[maxn];
void change(int rt,int v) {
	tree[rt].lazy+=v;
	//此时表示我们的区间进行改变v次,就意味着我们的区间和向右循环v轮
	for(int i=0;i<LCM;i++) num[i]=tree[rt].sum[(i+v)%LCM];
	for(int i=0;i<LCM;i++) tree[rt].sum[i]=num[i];
}
void pushdown(int rt) {
//可能有人会对下面操作产生疑问,为什么我们不需要对左右儿子进行判断是否已经进入循环节呢
//这是因为当我们的儿子需要继承lazy时,父亲区间已经有lazy了,而我们的父亲区间有lazy的前提是已经进入到循环节中中
//这就意味着我们的儿子区间肯定也已经进入到循环节中了
	change(ls,tree[rt].lazy);change(rs,tree[rt].lazy);
	tree[rt].lazy=0;
}
void update(int l,int r,int rt,int v) {
	if(tree[rt].l==l&&tree[rt].r==r) {
		if(tree[rt].flag) {
			change(rt,v);
			return ;
		}
		else if(l==r) {//没有进入循环节,那就进行暴力单点修改
			tree[rt].sum[0]=tree[rt].sum[0]*tree[rt].sum[0]%p;
			if(check[tree[rt].sum[0]]) {
				tree[rt].flag=1;
				for(int i=1;i<LCM;i++) {
					tree[rt].sum[i]=tree[rt].sum[i-1]*tree[rt].sum[i-1]%p;
				}
			}
			return ;
		}
	}
	if(tree[rt].lazy) pushdown(rt);
	int mid=(tree[rt].l+tree[rt].r)>>1;
	if(r<=mid) update(l,r,ls,v);
	else if(l>mid) update(l,r,rs,v);
	else update(l,mid,ls,v),update(mid+1,r,rs,v);
	pushup(rt);
}
int query(int l,int r,int rt) {//简单的返回区间和,不解释
	if(tree[rt].l==l&&tree[rt].r==r) {
		return tree[rt].sum[0];
	}
	if(tree[rt].lazy) pushdown(rt);
	int mid=(tree[rt].l+tree[rt].r)>>1;
	if(r<=mid) return query(l,r,ls);
	else if(l>mid) return query(l,r,rs);
	else return query(l,mid,ls)+query(mid+1,r,rs);
}
void get_loop(int x) {//求出每一个数字的循环节的长度
	int len=0;int xx=x;
	while(1) {
		len++;
		if(vis[xx]) {
			cirlen[x]=len-vis[xx];
			break;
		}
		else {
			vis[xx]=len;
		}
		xx=xx*xx%p;
	}
	while(vis[x]) {//清空我们的vis数组,不用memset是因为需要多次调用,减少花费的时间
		vis[x]=0;
		x=x*x%p;
	}
}
void init() {//对于每一个数字,我们预处理出当前数字是否已经进入到循环节中
	for(int i=0;i<p;i++) {
		int x=i;
		for(int j=1;j<=LCM;j++) {
			x=x*x%p;
		}
		if(x==i) check[i]=1;
	}
}
int main() {
	n=read();m=read();p=read();
	for(int i=0;i<p;i++) get_loop(i);
	LCM=cirlen[0];
	for(int i=1;i<p;i++) {
		LCM=lcm(LCM,cirlen[i]);
	}
	for(int i=1;i<=n;i++) {
		a[i]=read();
	}
	init();
	build(root);
	for(int i=1;i<=m;i++) {
		int opt=read();
		if(opt==1) {
			int l=read(),r=read();
			update(l,r,1,1);
		}
		else {
			int l=read(),r=read();
			printf("%d\n",query(l,r,1));
		}
	}
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值