线段树的鬼畜操作
一 线段树维护区间取模
题目大意是让你维护区间求和,区间取模,单点修改。
如果我们去掉区间取模的这个操作话,就会发现这就是一个线段树单点修改区间求和的板子。
那么我们只需要考虑的就只有区间取模这个操作。
我们可以联想到模对加法是封闭的。
即 \(a \% p + b \% p + c \% p + d \% p\) = \((a+b+c+d) \% p\);
所以我们可以正常维护区间和,取模后,区间的和 = \(区间修改之前的和 \% p\)
模法的另一个性质
当模数大于一个数时,他取不取模都一样。这就可以看做一个剪枝。
我们维护一个区间最大值,当这个数比模数要小时,直接\(return\) 就行了。
这就是本题比较重要的一个优化。
对于大于模数的数,我们可以直接暴力修改。(暴力出奇迹)
然后本题没了。。。。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define LL long long
const int N = 1e5+10;
int n,m,opt,l,r,x;
LL a[N];
struct tree{
#define l(o) tr[o].lc
#define r(o) tr[o].rc
#define sum(o) tr[o].sum
#define maxn(o) tr[o].maxn
struct node{
int lc,rc;
LL sum,maxn;
}tr[N<<2];
void up(int o)
{
sum(o) = sum(o<<1) + sum(o<<1|1);//区间和
maxn(o) = max(maxn(o<<1),maxn(o<<1|1));//维护区间最大值
}
void build(int o,int L,int R)//正常建树
{
l(o) = L, r(o) = R;
if(L == R)
{
sum(o) = maxn(o) = a[L];
return ;
}
int mid = (L + R)>>1;
build(o<<1,L,mid);
build(o<<1|1,mid+1,R);
up(o);
}
void change(int o,int x,int val)//单点修改
{
if(l(o) == r(o))
{
sum(o) = maxn(o) = val;
return ;
}
int mid = (l(o) + r(o))>>1;
if(x <= mid) change(o<<1,x,val);
if(x > mid) change(o<<1|1,x,val);
up(o);
}
void chenge(int o,int L,int R,int mod)//区间取模
{
if(maxn(o) < mod) return;//当区间最大值都小于模数时,直接return
if(l(o) == r(o))//单点暴力修改
{
sum(o) = sum(o) % mod;
maxn(o) = maxn(o) % mod;
return ;
}
int mid = (l(o) + r(o))>>1;
if(L <= mid) chenge(o<<1,L,R,mod);
if(R > mid) chenge(o<<1|1,L,R,mod);
up(o);
}
LL ask(int o,int L,int R)//正常查询
{
LL ans = 0;
if(L <= l(o) && R >= r(o))
{
return sum(o);
}
int mid = (l(o) + r(o))>>1;
if(L <= mid) ans += ask(o<<1,L,R);
if(R > mid) ans += ask(o<<1|1,L,R);
return ans;
}
}tree;
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++) scanf("%lld",&a[i]);
tree.build(1,1,n);
for(int i = 1; i <= m; i++)
{
scanf("%d",&opt);
if(opt == 1)
{
scanf("%d%d",&l,&r);
printf("%lld\n",tree.ask(1,l,r));//区间求和
}
if(opt == 2)
{
scanf("%d%d%d",&l,&r,&x);
tree.chenge(1,l,r,x);//区间取模
// for(int i = 1; i <= n; i++) cout<<tree.ask(1,i,i)<<endl;
}
if(opt == 3)
{
scanf("%d%d",&l,&x);
tree.change(1,l,x);//单点修改
}
}
return 0;
}
二:线段树维护等差数列
题目大意就是让线段树维护区间加等差数列,和单点查询。
前置芝士
差分数组
定义:对于已知有n个元素的数列d,建立记录它每项与前一项差值的差分数组d 即 \(d[i]\) = \(a[i] - a[i-1]\)
性质:计算数列各项的值 a[x] = 原数列a[x]的值 + \(\displaystyle\sum_{i=1}^{x} d[i]\)
我们考虑到等差数列每一项的差值是一定的,所以我们可以考虑线段树维护一个差分数组
。
查询某个节点的值时,就是用原数列 \(a[x]\) 的值加上\(1-x\)的区间和。
这样查询操作就可以解决了。那么修改操作呢???
1.\(L\)与\(L-1\)的差值为\(k\),所以我们可以在\(L\)的差分数组上加\(K\),用线段树来单点修改
2.从\(L+1\)到\(R\) 其中每一项与前一项的差值都为\(d\),所以我们在\(L+1-R\)的差分数组都加上\(d\),用线段树来维护区间修改
3.\(R+1\)与\(R\)这一项的差值为\(-(k + (R-L) * d)\),其实就是等差数列的和,直接线段树单点修改就可以了。
线段树和普通的线段树一样,直接维护区间修改,区间求和就可以了。(才不会告诉你我懒得写单点修改呢)
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 1e5+10;
int k,d,x;
int a[N],n,m,opt,l,r;
struct tree{//普通的线段树
#define l(o) tr[o].lc
#define r(o) tr[o].rc
#define add(o) tr[o].add
#define sum(o) tr[o].sum
struct node{
int lc,rc;
int add,sum;
}tr[N<<2];
void up(int o){
sum(o) = sum(o*2) + sum(o*2+1);
}
void down(int o){
if(add(o)){
add(o*2) += add(o);
add(o*2+1) += add(o);
sum(o*2) += add(o) *(r(o*2) - l(o*2) +1);
sum(o*2+1) += add(o) * (r(o*2+1) - l(o*2+1) +1);
add(o) = 0;
}
}
void build(int o,int L,int R){
l(o) = L; r(o) = R;
if(L == R){
sum(o) = 0;//一开始差分数组都定义为0
return ;
}
int mid = (L + R) / 2;
build(o*2,L,mid);
build(o*2+1,mid+1,R);
up(o);
}
void chenge(int o,int L,int R,int val){
if(L <= l(o) && R >= r(o)){
add(o) += val;
sum(o) += val * (r(o) - l(o) + 1);
return ;
}
down(o);
int mid = (l(o) + r(o)) / 2;
if(L <= mid) chenge(o*2,L,R,val);
if(R > mid) chenge(o*2+1,L,R,val);
up(o);
}
int ask(int o,int L,int R){
int ans = 0;
if(L <= l(o) && R >= r(o)){
return sum(o);
}
down(o);
int mid = (l(o) + r(o)) / 2;
if(L <= mid) ans += ask(o*2,L,R);
if(R > mid) ans += ask(o*2+1,L,R);
return ans;
}
}tree;
int main(){
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++) scanf("%d",&a[i]);
tree.build(1,1,n);
for(int i = 1; i <= m; i++){
scanf("%d",&opt);
if(opt == 1){
scanf("%d%d%d%d",&l,&r,&k,&d);
tree.chenge(1,l,l,k);
if(r > l) tree.chenge(1,l+1,r,d);
if(r < n)tree.chenge(1,r+1,r+1, -(k + (r-l) * d));
}
else{
scanf("%d",&x);
printf("%d\n",a[x] + tree.ask(1,1,x));//差分数组的性质
}
}
return 0;
}
突然觉得自己以前的代码写的好丑,没写位运算QAQ。(down函数也贼丑)
三 线段树维护区间排序
题目大意是让我们维护每次区间排序后的结果,并询问排完序后第\(pos\)位置的值。
看到这道题,我们第一眼可能不会想到线段树,而是暴力快排。
然而,这道题可以转换为线段树(当然你也可以用珂朵莉树)
我们可以将这个序列转换为01序列在进行排序。
当我们转换为01序列后,我们可以用线段树来维护,记录每个区间1的个数为\(cnt\)。
1.对于升序排序,我们可以将 \(L\) 到 \(R-cnt\)这一段区间全部变为0,并用线段树维护。
再将 \(R-cnt+1\) 到 \(R\)这一段区间全部变为1,、同理用线段树维护。因为在升序排序中
0肯定要排在1前面
2.同理,对于降序排序,我们将 \(L\) 到 \(L+cnt-1\)这一段区间变为1,将\(L+cnt\) 到 \(R\)
这一段区间全部变为0,并用线段树来维护。
那么怎么转换为01序列呢????
题目中给出的序列为\(1-n\)的全排列,那么我们可以二分一个答案 mid.
我们将大于等于 mid 的数变为1,小于的则变为0。
并用线段树来模拟排序。当我们要查询的pos这个位置的数字为1代表我们的答案
是大于等于mid 的,所以我们上扩大二分下限。反之减小上限。
坑点
1.tag 标记要为 0,1,-1, 三种状态。-1表示没有标记。
0为区间变为0,1的话则为区间变为1.
2.要注意线段树模拟排序的区间范围。
代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define l(o) tr[o].lc
#define r(o) tr[o].rc
#define sum(o) tr[o].sum
#define add(o) tr[o].add
const int N = 2e5+10;
int n,m,ans,pos;
int a[N],b[N],add[N];
struct question{
int opt,l,r;
}q[N];
struct tree
{
struct node{
int lc,rc;
int add,sum;
}tr[N<<2];
void Add(int o,int val)
{
add(o) = val;
sum(o) = val * (r(o) - l(o) + 1);
}
void up(int o)
{
sum(o) = sum(o<<1) + sum(o<<1|1);
}
void down(int o)
{
if(add(o) == -1) return;//没有标记直接返回
Add(o<<1 , add(o)); Add(o<<1|1 , add(o));//下传
add(o) = -1;
}
void build(int o,int L,int R)
{
l(o) = L, r(o) = R;
add(o) = -1;//清空标记
if(L == R)
{
sum(o) = b[L];
return;
}
int mid = (L + R)>>1;
build(o<<1 , L , mid);
build(o<<1|1 , mid+1 ,R);
up(o);
}
void chenge(int o,int L,int R,int val)//区间赋值
{
if(L <= l(o) && R >= r(o))
{
Add(o , val); return;
}
down(o);
int mid = (l(o) + r(o))>>1;
if(L <= mid) chenge(o<<1 , L , R , val);
if(R > mid) chenge(o<<1|1 , L , R , val);
up(o);
}
int ask(int o,int L,int R)
{
int ans = 0;
if(L <= l(o) && R >= r(o))
{
return sum(o);
}
down(o);
int mid = (l(o) + r(o))>>1;
if(L <= mid) ans += ask(o<<1 , L , R);
if(R > mid) ans += ask(o<<1|1 , L , R);
return ans;
}
}tree;
bool judge(int k)
{
for(int i = 1; i <= n; i++)//把大于等于mid的变为1,反之变为0
{
if(a[i] >= k) b[i] = 1;
else b[i] = 0;
}
tree.build(1,1,n);
for(int i = 1; i <= m; i++)//线段树模拟排序
{
int tot = tree.ask(1,q[i].l,q[i].r);//区间中1的个数
if(q[i].opt == 0)//升序排列
{
tree.chenge(1, q[i].l , q[i].r - tot , 0);//区间变为0
tree.chenge(1, q[i].r - tot + 1 , q[i].r , 1);//区间赋1
}
if(q[i].opt == 1)//降序排序
{
tree.chenge(1 , q[i].l , q[i].l + tot - 1 , 1);//区间赋1
tree.chenge(1 , q[i].l + tot ,q[i].r ,0);//区间赋0
}
}
return tree.ask(1,pos,pos);//查询pos这个位置为0或1
}
int main()
{
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i++) scanf("%d",&a[i]);
for(int i = 1; i <= m; i++) scanf("%d%d%d",&q[i].opt,&q[i].l,&q[i].r);//开个结构体储存每个询问的信息
scanf("%d",&pos);
int L = 1,R = n;
while(L <= R)//二分答案
{
int mid = (L + R)>>1;
if(judge(mid))
{
ans = mid;
L = mid + 1;
}
else R = mid - 1;
}
printf("%d\n",ans);
fclose(stdin); fclose(stdout);
return 0;
}
四 线段树维护约数个数
一句话题意:让你维护一个数据结构支持区间求和以及单点修改。
这都是线段树的常规操作,但每次修改操作都单点修改一次复杂度是不能接受的。
但当一个数小于等于 \(2\) 的时候,无论再怎么修改,他的值也不会再改变。
因此我们可以在维护一个区间最大值,当最大值大于 \(2\) 的时候直接单点修改,反之 return
复杂度 O(能过)
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define LL long long
const int N = 3e5+10;
const int M = 1e6+10;
LL n,m,opt,l,r,tot;
int f[M],g[M],a[N],prime[M];
bool check[M];
inline LL read()
{
LL s = 0,w = 1; char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') w = -1; ch = getchar();}
while(ch >= '0' && ch <= '9'){s = s * 10 + ch - '0'; ch = getchar();}
return s * w;
}
struct node
{
int lc,rc;
LL sum,maxn;
}tr[N<<2];
#define l(o) tr[o].lc
#define r(o) tr[o].rc
#define sum(o) tr[o].sum
#define maxn(o) tr[o].maxn
void up(int o)
{
sum(o) = sum(o<<1) + sum(o<<1|1);
maxn(o) = max(maxn(o<<1),maxn(o<<1|1));
}
void build(int o,int L,int R)
{
l(o) = L, r(o) = R;
if(L == R)
{
sum(o) = maxn(o) = a[L];
return;
}
int mid = (L + R)>>1;
build(o<<1,L,mid);
build(o<<1|1,mid+1,R);
up(o);
}
void chenge(int o,int L,int R)
{
if(maxn(o) <= 2) return;
if(l(o) == r(o))
{
sum(o) = maxn(o) = f[sum(o)];
return;
}
int mid = (l(o) + r(o))>>1;
if(L <= mid) chenge(o<<1,L,R);
if(R > mid) chenge(o<<1|1,L,R);
up(o);
}
LL query(int o,int L,int R)
{
LL res = 0;
if(L <= l(o) && R >= r(o)) return sum(o);
int mid = (l(o) + r(o))>>1;
if(L <= mid) res += query(o<<1,L,R);
if(R > mid) res += query(o<<1|1,L,R);
return res;
}
void YYCH()
{
g[1] = f[1] = 1;
for(int i = 2; i <= M-5; i++)
{
if(!check[i])
{
prime[++tot] = i;
g[i] = 1;
f[i] = 2;
}
for(int j = 1; j <= tot && i * prime[j] <= M-5; j++)
{
check[i * prime[j]] = 1;
if(i % prime[j] == 0)
{
g[i * prime[j]] = g[i] + 1;
f[i * prime[j]] = f[i] * (g[i * prime[j]] + 1) / (g[i] + 1);
}
else
{
g[i * prime[j]] = 1;
f[i * prime[j]] = f[i] * f[prime[j]];
}
}
}
}
int main()
{
n = read(); m = read(); YYCH();
for(int i = 1; i <= n; i++) a[i] = read();
build(1,1,n);
for(int i = 1; i <= m; i++)
{
opt = read(); l = read(); r = read();
if(opt == 1) chenge(1,l,r);
else printf("%lld\n",query(1,l,r));
}
return 0;
}
五 线段树维护区间GCD
不会,先把坑占上,后期再补