CF679E 题解
题意简述
我们称 $42$ 的正整数次幂为“坏数”(正整数次幂,不包含 $1$),而其他的正整数都是“好数”。
给出长度为 $n(1\le n\le10^5)$ 的序列 $a_i(1\le a_i\le10^9)$,并进行 $q(1\le q\le 10^5)$ 次操作。操作共三种:
-
给定 $x$,求 $a_x$ 的值。
-
给定 $l,r$,将区间 $a_l,a_{l+1},\cdots,a_{r-1},a_r$ 全赋值为给定的 $x(1\le x\le10^9)$。
-
给定 $l,r$ 和 $x(1\le x\le10^9)$。先将 $a_l,a_{l+1},\cdots,a_{r-1},a_r$ 全部加上 $x$,然后不断如此操作下去一直到 $a_l,a_{l+1},\cdots,a_{r-1},a_r$ 都是“好数”为止。
题目分析
显然操作三并不能不停操作下去(操作有限次之后一定能找到两个相邻的 $42$ 的次幂使得 $a_l\sim a_r$ 都夹在其之间,证明此处不多赘述),这就表明可能涉及到的 $42$ 的次幂也是有限的。具体来说,最大的 $42$ 的次幂在 $O(qx)$ 级别,也就是 $10^{14}$ 左右,不会超过 $42^9≈4\times10^{14}$。可以直接把 $42^1,42^2,\cdots,42^9$ 即所谓“坏数”预处理一下。
考虑使用线段树维护 $a_l\sim a_r$ 之间每个 $a_i$ 到下一个“坏数”距离的最小值。操作二的区间赋值很简单,打懒标记简单处理,更新最小值直接 lower_bound
查找新的“坏数”计算就行(直接枚举也行);操作三则先实现区间加,然后不断区间加直到 $a_1\sim a_n$ 距“坏数”距离最小值不为 $0$ 即可。而单点查询,单凭一个数距“坏数”的距离不能确定其原始值,但是区间赋值标记可以解决这一问题。我们直接让 $l=r$ 的结点的区间赋值标记记录原始值即可。
至于时间复杂度,根据刚刚的分析,每个位置只会被修改 $O(\log_{42}qx)$ 次,所以总时间复杂度为 $O((n+q)\log_2n\log_{42}qx)$
代码实现
#include<bits/stdc++.h>
using namespace std;
int n,q,op,l,r;
long long pow42[12],a[100010],x;
struct node
{
int l,r;//左右端点。
long long mn,tag1,tag2;//mn:离下一个“坏数”距离的最小值;tag1 是区间加标记,tag2 是区间赋值标记。
}tr[400010];
template<typename T>void rd(T &x)
{
x=0;
char c=getchar();
for(;c>'9'||c<'0';c=getchar());
for(;c<='9'&&c>='0';c=getchar())
x=(x<<3)+(x<<1)+c-'0';
}//快读
template<typename T>void wt(T x)
{
if(x>=10)
wt(x/10);
putchar(x%10+'0');
}//快写
long long calc(long long x)
{
return (*lower_bound(pow42+1,pow42+11,x))-x;//距下一个“坏数”的距离
}
void pushup(int p)
{
tr[p].mn=min(tr[p<<1].mn,tr[p<<1|1].mn);
}//子结点更新父结点
void addtag1(int p,long long tag)
{
if(tr[p].tag2)//区间赋值过就加到标记上
{
tr[p].tag2+=tag;
tr[p].mn=calc(tr[p].tag2);
}
else//没区间赋值过就直接打标记
{
tr[p].mn-=tag;
tr[p].tag1+=tag;
}
}//区间加打标记
void addtag2(int p,long long tag)
{
tr[p].tag2=tag;
tr[p].mn=calc(tag);
tr[p].tag1=0;//区间赋值之后,之前的区间加都失效
}//区间赋值打标记
void pushdown(int p)
{
if(tr[p].tag1)
{
addtag1(p<<1,tr[p].tag1);
addtag1(p<<1|1,tr[p].tag1);//向左右子结点下传标记
tr[p].tag1=0;//清空标记
}
if(tr[p].tag2)
{
addtag2(p<<1,tr[p].tag2);
addtag2(p<<1|1,tr[p].tag2);//向左右子结点下传标记
tr[p].tag2=0;//清空标记
}
}
void build(int p,int l,int r)//建树
{
tr[p].l=l,tr[p].r=r;
if(l==r)
{
addtag2(p,a[l]);//为了方便询问,初始化就添上一个赋值标记
return;
}
int mid=l+r>>1;
build(p<<1,l,mid);
build(p<<1|1,mid+1,r);
pushup(p);
}
void change1(int p,int l,int r,int x)//区间加
{
if(tr[p].l>=l&&tr[p].r<=r/*区间全包含*/&&(tr[p].tag2||tr[p].mn>=x)/*若被区间赋值过或者区间加操作完后,没有比原来的“坏数”大,那么就可以直接打标记*/)
{
addtag1(p,x);
return;
}
//否则就递归下去算
pushdown(p);
int mid=tr[p].l+tr[p].r>>1;
if(mid>=l)
change1(p<<1,l,r,x);
if(mid<r)
change1(p<<1|1,l,r,x);
pushup(p);
}
void change2(int p,int l,int r,int x)//区间赋值
{
if(tr[p].l>=l&&tr[p].r<=r)
{
addtag2(p,x);
return;
}
pushdown(p);
int mid=tr[p].l+tr[p].r>>1;
if(mid>=l)
change2(p<<1,l,r,x);
if(mid<r)
change2(p<<1|1,l,r,x);
pushup(p);
}
long long query(int p,int x)//单点查询,也可以直接二分实现
{
if(tr[p].l==tr[p].r)
return tr[p].tag2;
int mid=tr[p].l+tr[p].r>>1;
pushdown(p);
if(mid>=x)
return query(p<<1,x);
else
return query(p<<1|1,x);
}
int main()
{
pow42[0]=1ll;
for(int i=1;i<=10;i++)
pow42[i]=42ll*pow42[i-1];//预处理“坏数”
rd(n),rd(q);
for(int i=1;i<=n;i++)
rd(a[i]);
build(1,1,n);
while(q--)
{
rd(op),rd(l);
switch(op)
{
case 1:
wt(query(1,l));
putchar('\n');
break;
case 2:
rd(r),rd(x);
change2(1,l,r,x);
break;
case 3:
rd(r),rd(x);
do
{
change1(1,l,r,x);
}while(tr[1].mn==0);//由于至少要操作一次,所以用 do-while。
}
}
return 0;
}