烟台CSP-S核心算法DAY 2
烟台CSP-S核心算法DAY 2
线段树日!!!(qwq)
其实树状数组就是废物——zhx
所以为什么线段树如此伟大?
特点
数据结构的90%
区间操作的KING
“种植”线段树

节点数:\(2N\)
基本思路:通过左区间和右区间的合并操作来解决父节点的操作(区间加法)
做区间操作是保证时间复杂度为\(O(logN)\)
数组开空间的时候,要记得开4倍\((MAXN << 2)\),因为它的标号方式不一定是到\(2N\)
王铭宇CODE
点击查看代码
#include <iostream>
#include <algorithm>
#include <vector>
#include <queue>
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
using namespace std;
const int N = 10000005;
typedef long long ll;
int n;
ll a[N];
ll sum[N << 2];
ll mx[N << 2];
ll tag[N];
int ls(int x)
{
return x << 1;
}
int rs(int x)
{
return x << 1 | 1;
}
void pushup(int x)
{
sum[x] = sum[ls(x)] + sum[rs(x)];
mx[x] = max(mx[ls(x)], mx[rs(x)]);
}
void add(int x, int l, int r, ll k)
{
sum[x] += k * (r - l + 1);
mx[x] += k;
tag[x] += k;
}
void pushdown(int x, int l, int r)
{
int mid = (l + r) >> 1;
if (tag[x] != 0)
{
add(ls(x), l, mid, tag[x]);
add(rs(x), mid + 1, r, tag[x]);
tag[x] = 0;
}
}
void build(int x, int l, int r)
{
if (l == r)
{
sum[x] = mx[x] = a[l];
return ;
}
int mid = (l + r) >> 1;
build(ls(x), l, mid);
build(rs(x), mid + 1, r);
pushup(x);
}
ll query_sum(int x, int l, int r, int L, int R)
{
if (L <= l && r <= R)
return sum[x];
pushdown(x, l, r);
ll ret = 0;
int mid = (l + r) >> 1;
if (L <= mid)
{
ret += query_sum(ls(x), l, mid, L, R);
}
if (mid < R)
{
ret += query_sum(rs(x), mid + 1, r, L, R);
}
return ret;
}
ll query_max(int x, int l, int r, int L, int R)
{
if (L <= l && r <= R)
return mx[x];
pushdown(x, l, r);
ll ret = -2e9;
int mid = (l + r) >> 1;
if (L <= mid)
{
ret = max(ret, query_max(ls(x), l, mid, L, R));
}
if (mid < R)
{
ret = max(ret, query_max(rs(x), mid + 1, r, L, R));
}
return ret;
}
void add_one(int x, int l, int r, int p, int k)
{
if (l == r)
{
sum[x] += k;
mx[x] += k;
return;
}
pushdown(x, l, r);
int mid = (l + r) >> 1;
if (p <= mid)
add_one(ls(x), l, mid, p, k);
else
add_one(rs(x), mid + 1, r, p, k);
pushup(x);
}
void add_many(int x, int l, int r, int L, int R, ll k)
{
if (L <= l && R >= r)
{
add(x, l, r, k);
return ;
}
pushdown(x, l, r);
int mid = (l + r) >> 1;
if (L <= mid)
add_many(ls(x), l, mid, L, R, k);
if (mid < R)
add_many(rs(x), mid + 1, r, L, R, k);
pushup(x);
}
int main()
{
int m;
cin >> n >> m;
for (int i = 1; i <= n; ++i)
{
cin >> a[i];
}
build(1, 1, n);
while (m--)
{
int opt;
cin >> opt;
if (opt == 1)
{
int x, y;
ll k;
cin >> x >> y >> k;
add_many(1, 1, n, x, y, k);
}
if (opt == 2)
{
int x, y;
cin >> x >> y;
cout << query_sum(1, 1, n, x, y) << '\n';
}
}
return 0;
}
zhx CODE(理论较优)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 7;
int read()
{
int x = 0, w = 1;
char ch = getchar();
while(ch > '9' || ch < '0')
{
if(ch == '-')
{
w = -1;
ch = getchar();
}
}
while(ch <= '9' && ch >= '0')
{
x = x * 10 + ch - '0';
ch = getchar();
}
return x * w;
}
long long Qmi(int a, int b, int p)
{
if(b == 0)
{
return 1%p;
}
if(b == 1)
{
return a%p;
}
long long ans = Qmi(a, b/2, p);
ans = ans*ans%p;
if(b % 2)
{
ans = ans*a%p;
}
return ans % p;
}
bool isprime(long long x)
{
if(x <= 1)
{
return false;
}
if(x == 2)
{
return true;
}
if(x % 2 == 0)
{
return false;
}
for(int i = 3; i <= sqrt(x); i += 2)
{
if(x % i == 0)
{
return false;
}
}
return true;
}
#define root 1,n,1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int maxn=100010;
int n,m,a[maxn];
struct node//一个线段树节点
{
int sum;//代表区间和
int size;//代表区间长度
int add;//这段区间被整体加了多少
node()
{
sum = size = add = 0;
}
void init(int v)//用一个数初始化
{
sum = v;
size = 1;
}
}z[maxn<<2];//z[i]就代表线段树的第i个节点
node operator+(const node &l,const node &r)
{
node res;
res.sum = l.sum + r.sum;
res.size = l.size + r.size;
return res;
}
void color(int l,int r,int rt,int v)//给l,r,rt这个节点打一个+v的懒标记
{
z[rt].add += v;
z[rt].sum += z[rt].size * v;
}
void push_col(int l,int r,int rt)//标记下放 把标记告诉儿子
{
if (z[rt].add == 0) return; //没标记 不需要下放 可以不要这句话 但会慢些
int m=(l+r)>>1;
color(lson,z[rt].add);
color(rson,z[rt].add);
z[rt].add=0;
}
void build(int l,int r,int rt)//建树 初始化l,r,rt这个节点
//编号为rt的线段树节点 所对应的区间是l~r
{
if (l==r)
{
z[rt].init(a[l]);
return;
}
int m=(l+r) >> 1;
build(lson);
build(rson);
z[rt] = z[rt<<1] + z[rt<<1|1];
}
node query(int l,int r,int rt,int nowl,int nowr)
//l,r,rt描述了一个线段树节点
//nowl nowr代表了询问的区间的左端点和右端点
{
if (nowl <= l && r <= nowr) return z[rt];
push_col(l,r,rt);
int m=(l+r)>>1;
if (nowl<=m)
{
if (m<nowr) return query(lson,nowl,nowr) + query(rson,nowl,nowr);
else return query(lson,nowl,nowr);
}
else return query(rson,nowl,nowr);
}
void modify(int l,int r,int rt,int nowl,int nowr,int v)
//把nowl~nowr这段区间全部整体+v
{
if (nowl<=l && r<=nowr)//当前线段树节点被修改区间整体包含
{
color(l,r,rt,v);//给l,r,rt这个节点打一个+v的懒标记
return;
}
push_col(l,r,rt);
int m=(l+r)>>1;
if (nowl<=m) modify(lson,nowl,nowr,v);
if (m<nowr) modify(rson,nowl,nowr,v);
z[rt] = z[rt<<1] + z[rt<<1|1];
}
int main()
{
cin >> n;
for (int i=1;i<=n;i++)
cin >> a[i];
build(root);
cin >> m;
for (int i=1;i<=m;i++)
{
int opt;
cin >> opt;
if (opt==1)//询问
{
int l,r;
cin >> l >> r;
cout << query(root,l,r).sum << "\n";
}
else
{
int l,r,v;
cin >> l >> r >> v;
modify(root,l,r,v);
}
}
return 0;
zhx CODE(理论较优)
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 7;
int read()
{
int x = 0, w = 1;
char ch = getchar();
while(ch > '9' || ch < '0')
{
if(ch == '-')
{
w = -1;
ch = getchar();
}
}
while(ch <= '9' && ch >= '0')
{
x = x * 10 + ch - '0';
ch = getchar();
}
return x * w;
}
long long Qmi(int a, int b, int p)
{
if(b == 0)
{
return 1%p;
}
if(b == 1)
{
return a%p;
}
long long ans = Qmi(a, b/2, p);
ans = ans*ans%p;
if(b % 2)
{
ans = ans*a%p;
}
return ans % p;
}
bool isprime(long long x)
{
if(x <= 1)
{
return false;
}
if(x == 2)
{
return true;
}
if(x % 2 == 0)
{
return false;
}
for(int i = 3; i <= sqrt(x); i += 2)
{
if(x % i == 0)
{
return false;
}
}
return true;
}
#define root 1,n,1
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
const int maxn=100010;
int n,m,a[maxn];
struct node//一个线段树节点
{
int sum;//代表区间和
int size;//代表区间长度
int add;//这段区间被整体加了多少
node()
{
sum = size = add = 0;
}
void init(int v)//用一个数初始化
{
sum = v;
size = 1;
}
}z[maxn<<2];//z[i]就代表线段树的第i个节点
node operator+(const node &l,const node &r)
{
node res;
res.sum = l.sum + r.sum;
res.size = l.size + r.size;
return res;
}
void color(int l,int r,int rt,int v)//给l,r,rt这个节点打一个+v的懒标记
{
z[rt].add += v;
z[rt].sum += z[rt].size * v;
}
void push_col(int l,int r,int rt)//标记下放 把标记告诉儿子
{
if (z[rt].add == 0) return; //没标记 不需要下放 可以不要这句话 但会慢些
int m=(l+r)>>1;
color(lson,z[rt].add);
color(rson,z[rt].add);
z[rt].add=0;
}
void build(int l,int r,int rt)//建树 初始化l,r,rt这个节点
//编号为rt的线段树节点 所对应的区间是l~r
{
if (l==r)
{
z[rt].init(a[l]);
return;
}
int m=(l+r) >> 1;
build(lson);
build(rson);
z[rt] = z[rt<<1] + z[rt<<1|1];
}
node query(int l,int r,int rt,int nowl,int nowr)
//l,r,rt描述了一个线段树节点
//nowl nowr代表了询问的区间的左端点和右端点
{
if (nowl <= l && r <= nowr) return z[rt];
push_col(l,r,rt);
int m=(l+r)>>1;
if (nowl<=m)
{
if (m<nowr) return query(lson,nowl,nowr) + query(rson,nowl,nowr);
else return query(lson,nowl,nowr);
}
else return query(rson,nowl,nowr);
}
void modify(int l,int r,int rt,int nowl,int nowr,int v)
//把nowl~nowr这段区间全部整体+v
{
if (nowl<=l && r<=nowr)//当前线段树节点被修改区间整体包含
{
color(l,r,rt,v);//给l,r,rt这个节点打一个+v的懒标记
return;
}
push_col(l,r,rt);
int m=(l+r)>>1;
if (nowl<=m) modify(lson,nowl,nowr,v);
if (m<nowr) modify(rson,nowl,nowr,v);
z[rt] = z[rt<<1] + z[rt<<1|1];
}
int main()
{
cin >> n;
for (int i=1;i<=n;i++)
cin >> a[i];
build(root);
cin >> m;
for (int i=1;i<=m;i++)
{
int opt;
cin >> opt;
if (opt==1)//询问
{
int l,r;
cin >> l >> r;
cout << query(root,l,r).sum << "\n";
}
else
{
int l,r,v;
cin >> l >> r >> v;
modify(root,l,r,v);
}
}
return 0;
}
我们认为,zhx写法更加令人愉悦,毕竟有很大的灵活性和可读性,可以更容易根据题意更改线段树函数
我们对于线段树的修改与查询操作的修改,我们一般只需更改结构体内维护的变量,构造函数,左右儿子之间的合并,以及懒标记的下方,而另外的建树、修改、询问则大部分题不需要更改
如果一道线段树题有多种修改方式(对于修改与询问的更改),一定要规划好不同修改之间的顺序
对于更改线段树函数的理解,见DAY 2代码中xianduanshu1~7的问题及代码,这很重要
伟大の取模——zhx
这些取模的一般规律在DP卡常中十分重要——zhx
加法的取模规律

我们可以将\(a += b\)的取模方式写成函数ordefine
减法的取模规律

同理,我们可以将\(a -= b\)的取模方式写成函数ordefine
这些可以伟大的优化我们的常数(运算速度)
例CODE
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int mo = 1000000007;
inline void inc(int &a,int b)
{
a+=b;if (a>=mo) a-=mo;
}
#define inc(a,b) {a+=b;if (a>=mo) a-=mo;}
int main()
{
res.sum = (1ll * res.sum * mul + 1ll * add * res.size) % mo;
//a = b+c*d;
a = (b + 1ll * c * d) % mo;
//a = b*c*d + e*f + g;
a = (1ll * b * c % mo * d + 1ll * e * f + g) % mo;
//a = a + b;
a += b; if (a >= mo) a-=mo;
//a = b*c-d*e
a = ((1ll * b * c - 1ll * d * e) % mo + mo) % mo;
//a = a - b
a -= b; if (a<0) a += mo;
return 0;
}
线段树再变异
GSS系列
GSS1
GSS3
GSS4
GSS5
QTREE系列
COT系列
上述为上午所有可做题,除QTREE & COT系列,重点xianduanshu1~7代码
例题

我们只需要处理左儿子的值、右儿子的值,再将两个区间中间的值进行操作即可
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define root 1, n, 1
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
const int maxn = 100010;
int n, m, a[maxn];
struct node // 一个线段树节点
{
int sum; // 代表区间相邻两数差的绝对值的和
int lv; // 最左边的数是多少
int rv; // 最右边的数是多少
int add; // 这段区间被整体加了多少
int size; // 区间长度
node()
{
sum = add = 0;
}
void init(int v) // 用一个数初始化
{
sum = 0;
lv = rv = v;
size = 1;
}
} z[maxn << 2]; // z[i]就代表线段树的第i个节点
node operator+(const node &l, const node &r)
{
node res;
res.sum = l.sum + r.sum + abs(l.rv - r.lv);
res.lv = l.lv;
res.rv = r.rv;
return res;
}
void color(int l, int r, int rt, int v) // 给l,r,rt这个节点打一个+v的懒标记
{
z[rt].add += v;
z[rt].lv += v;
z[rt].rv += v;
}
void push_col(int l, int r, int rt) // 标记下放 把标记告诉儿子
{
if (z[rt].add == 0)
return; // 没标记 不需要下放 可以不要这句话 但会慢些
int m = (l + r) >> 1;
color(lson, z[rt].add);
color(rson, z[rt].add);
z[rt].add = 0;
}
void build(int l, int r, int rt) // 建树 初始化l,r,rt这个节点
// 编号为rt的线段树节点 所对应的区间是l~r
{
if (l == r)
{
z[rt].init(a[l]);
return;
}
int m = (l + r) >> 1;
build(lson);
build(rson);
z[rt] = z[rt << 1] + z[rt << 1 | 1];
}
node query(int l, int r, int rt, int nowl, int nowr)
// l,r,rt描述了一个线段树节点
// nowl nowr代表了询问的区间的左端点和右端点
{
if (nowl <= l && r <= nowr)
return z[rt];
push_col(l, r, rt);
int m = (l + r) >> 1;
if (nowl <= m)
{
if (m < nowr)
return query(lson, nowl, nowr) + query(rson, nowl, nowr);
else
return query(lson, nowl, nowr);
}
else
return query(rson, nowl, nowr);
}
void modify(int l, int r, int rt, int nowl, int nowr, int v)
// 把nowl~nowr这段区间全部整体+v
{
if (nowl <= l && r <= nowr) // 当前线段树节点被修改区间整体包含
{
color(l, r, rt, v); // 给l,r,rt这个节点打一个+v的懒标记
return;
}
push_col(l, r, rt);
int m = (l + r) >> 1;
if (nowl <= m)
modify(lson, nowl, nowr, v);
if (m < nowr)
modify(rson, nowl, nowr, v);
z[rt] = z[rt << 1] + z[rt << 1 | 1];
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
build(root);
cin >> m;
for (int i = 1; i <= m; i++)
{
int opt;
cin >> opt;
if (opt == 1) // 询问
{
int l, r;
cin >> l >> r;
cout << query(root, l, r).sum << "\n";
}
else
{
int l, r, v;
cin >> l >> r >> v;
modify(root, l, r, v);
}
}
return 0;
}
再来一道例题
这题十分成功地告诉我们
如果一道线段树题有多种修改方式(对于修改与询问的更改),一定要规划好不同修改之间的顺序
一定要先处理好区间推平的情况,将懒标记删掉,再处理乘法,再处理加法
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define root 1, n, 1
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
const int maxn = 100010;
int n, m, a[maxn];
struct node // 一个线段树节点
{
int sum; // 代表区间和
int size; // 代表区间长度
int add;
int mul;
// x*mul+add
node()
{
sum = size = add = 0;
mul = 1;
}
void init(int v) // 用一个数初始化
{
sum = v;
size = 1;
}
} z[maxn << 2]; // z[i]就代表线段树的第i个节点
node operator+(const node &l, const node &r)
{
node res;
res.sum = l.sum + r.sum;
res.size = l.size + r.size;
return res;
}
void color(int l, int r, int rt, int mul, int add) // 给l,r,rt这个节点打一个*mul+add的懒标记
{
z[rt].mul *= mul;
z[rt].add = z[rt].add * mul + add;
z[rt].sum = z[rt].sum * mul + add * z[rt].size;
}
void push_col(int l, int r, int rt) // 标记下放 把标记告诉儿子
{
if (z[rt].mul == 1 && z[rt].add == 0)
return; // 没标记 不需要下放 可以不要这句话 但会慢些
int m = (l + r) >> 1;
color(lson, z[rt].mul, z[rt].add);
color(rson, z[rt].mul, z[rt].add);
z[rt].mul = 1;
z[rt].add = 0;
}
void build(int l, int r, int rt) // 建树 初始化l,r,rt这个节点
// 编号为rt的线段树节点 所对应的区间是l~r
{
if (l == r)
{
z[rt].init(a[l]);
return;
}
int m = (l + r) >> 1;
build(lson);
build(rson);
z[rt] = z[rt << 1] + z[rt << 1 | 1];
}
node query(int l, int r, int rt, int nowl, int nowr)
// l,r,rt描述了一个线段树节点
// nowl nowr代表了询问的区间的左端点和右端点
{
if (nowl <= l && r <= nowr)
return z[rt];
push_col(l, r, rt);
int m = (l + r) >> 1;
if (nowl <= m)
{
if (m < nowr)
return query(lson, nowl, nowr) + query(rson, nowl, nowr);
else
return query(lson, nowl, nowr);
}
else
return query(rson, nowl, nowr);
}
void modify(int l, int r, int rt, int nowl, int nowr, int mul, int add)
// 把nowl~nowr这段区间全部整体+v
{
if (nowl <= l && r <= nowr) // 当前线段树节点被修改区间整体包含
{
color(l, r, rt, mul, add); // 给l,r,rt这个节点打一个+v的懒标记
return;
}
push_col(l, r, rt);
int m = (l + r) >> 1;
if (nowl <= m)
modify(lson, nowl, nowr, mul, add);
if (m < nowr)
modify(rson, nowl, nowr, mul, add);
z[rt] = z[rt << 1] + z[rt << 1 | 1];
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
build(root);
cin >> m;
for (int i = 1; i <= m; i++)
{
int opt;
cin >> opt;
if (opt == 1) // 询问
{
int l, r;
cin >> l >> r;
cout << query(root, l, r).sum << "\n";
}
else
{
int l, r, add, mul;
cin >> l >> r >> add >> mul;
modify(root, l, r, add, mul);
}
}
return 0;
}
加题!加题!
给区间加上一个等差数列
首先,我们可以想到,给一个区间加上多个等差数列,那么他还是被加上了一个等差数列
所以,我们只需要维护好首项和公差即可
点击查看代码
#include <bits/stdc++.h>
using namespace std;
#define root 1, n, 1
#define lson l, m, rt << 1
#define rson m + 1, r, rt << 1 | 1
const int maxn = 100010;
int n, m, a[maxn];
struct node // 一个线段树节点
{
int sum; // 代表区间和
int size; // 代表区间长度
int x, y; // 给这段区间加上了一个首项为x 公差为y的等差数列
node()
{
sum = size = x = y = 0;
}
void init(int v) // 用一个数初始化
{
sum = v;
size = 1;
}
} z[maxn << 2]; // z[i]就代表线段树的第i个节点
node operator+(const node &l, const node &r)
{
node res;
res.sum = l.sum + r.sum;
res.size = l.size + r.size;
return res;
}
void color(int l, int r, int rt, int x, int y) // 给l,r,rt这个节点加上一个首项为x公差为y的等差数列
{
z[rt].x += x;
z[rt].y += y;
z[rt].sum += (x + x + (z[rt].size - 1) * y) * z[rt].size / 2;
}
void push_col(int l, int r, int rt) // 标记下放 把标记告诉儿子
{
if (z[rt].x == 0 && z[rt].y == 0)
return; // 没标记 不需要下放 可以不要这句话 但会慢些
int m = (l + r) >> 1;
color(lson, z[rt].x, z[rt].y);
color(rson, z[rt].x + z[rt << 1].size * z[rt].y, z[rt].y);
z[rt].x = z[rt].y = 0;
}
void build(int l, int r, int rt) // 建树 初始化l,r,rt这个节点
// 编号为rt的线段树节点 所对应的区间是l~r
{
if (l == r)
{
z[rt].init(a[l]);
return;
}
int m = (l + r) >> 1;
build(lson);
build(rson);
z[rt] = z[rt << 1] + z[rt << 1 | 1];
}
node query(int l, int r, int rt, int nowl, int nowr)
// l,r,rt描述了一个线段树节点
// nowl nowr代表了询问的区间的左端点和右端点
{
if (nowl <= l && r <= nowr)
return z[rt];
push_col(l, r, rt);
int m = (l + r) >> 1;
if (nowl <= m)
{
if (m < nowr)
return query(lson, nowl, nowr) + query(rson, nowl, nowr);
else
return query(lson, nowl, nowr);
}
else
return query(rson, nowl, nowr);
}
void modify(int l, int r, int rt, int nowl, int nowr, int x, int y)
// 把nowl~nowr这段区间全部整体+v
{
if (nowl <= l && r <= nowr) // 当前线段树节点被修改区间整体包含
{
color(l, r, rt, x, y); // 给l,r,rt这个节点打一个+v的懒标记
return;
}
push_col(l, r, rt);
int m = (l + r) >> 1;
if (nowl <= m)
modify(lson, nowl, nowr, x, y);
if (m < nowr)
modify(rson, nowl, nowr, x, y);
z[rt] = z[rt << 1] + z[rt << 1 | 1];
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
cin >> a[i];
build(root);
cin >> m;
for (int i = 1; i <= m; i++)
{
int opt;
cin >> opt;
if (opt == 1) // 询问
{
int l, r;
cin >> l >> r;
cout << query(root, l, r).sum << "\n";
}
else
{
int l, r, x, y;
cin >> l >> r >> x >> y;
modify(root, l, r, x, y);
}
}
return 0;
}
又是伟大的中午
好耶农大里面有球场
下午力
可持久化数据结构
因为一个数据结构时刻都发生变化
可持久化就是可以访问历史版本的数据结构且强制在线
我们存在许多数据结构有可持久化的模式
可持久化线段树
我们在i+1情况下会对比i的情况下做出改变
于是,我们将改变的节点新建出新的节点
注意,因为我们一定要建成可访问历史版本的线段树
所以我们做出的操作一定是新建出改变过的节点,而不是覆盖掉
我们将未作出改变的节点连到新建的节点上,就构成了一棵可持久化线段树
打个比喻:1s动画由20帧左右的静态画面连续播放而成, 每两幅相邻画面之间的差别很少, 如果用计算机制作动画 , 若每一帧的画面都重新制作,会很浪费空间, 为了降低成本, 让每一帧画面只记录与前一帧的不同处;(又如红绿灯的秒数倒计时)
模板CODE
点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 7;
int read()
{
int x = 0, w = 1;
char ch = getchar();
while(ch > '9' || ch < '0')
{
if(ch == '-')
{
w = -1;
ch = getchar();
}
}
while(ch <= '9' && ch >= '0')
{
x = x * 10 + ch - '0';
ch = getchar();
}
return x * w;
}
long long Qmi(int a, int b, int p)
{
if(b == 0)
{
return 1%p;
}
if(b == 1)
{
return a%p;
}
long long ans = Qmi(a, b/2, p);
ans = ans*ans%p;
if(b % 2)
{
ans = ans*a%p;
}
return ans % p;
}
bool isprime(long long x)
{
if(x <= 1)
{
return false;
}
if(x == 2)
{
return true;
}
if(x % 2 == 0)
{
return false;
}
for(int i = 3; i <= sqrt(x); i += 2)
{
if(x % i == 0)
{
return false;
}
}
return true;
}
int cnt;
int a[MAXN], root[MAXN];
struct node
{
int l, r;
int sum;
node()
{
l = r = sum = 0;
}
}z[MAXN*20];//应是MAXN*logn,这里logn取20
void update(int p)
{
z[p].sum = z[z[p].l].sum + z[z[p].r].sum;
}
int build(int l, int r)
{
cnt++;
int p = cnt;
if(l == r)
{
z[p].sum = a[l];
return p;
}
int m = (l + r) >> 1;
z[p].l = build(l, m);
z[p].r = build(m + 1, r);
update(p);
return p;
}
int query(int l, int r, int rt, int nowl, int nowr)
{
if(nowl <= l && r <= nowr)
{
return z[rt].sum;
}
int m = (l + r) >> 1;
if(nowl <= m)
{
if(m < nowr)
{
return query(l, m, z[rt].l, nowl, nowr) + query(m+1, r, z[rt].r, nowl, nowr);
}
else
{
return query(l, m, z[rt].l, nowl, nowr);
}
}
else
{
return query(m+1, r, z[rt].r, nowl, nowr);
}
}
int modify(int l, int r, int rt, int p, int v)
{
cnt++;
int q = cnt;
z[q] = z[rt];
if(l == r)
{
z[q].sum += v;
return q;
}
int m = (l + r) >> 1;
if(p <= m)
{
z[q].l = modify(l, m, z[q].l, p, v);
}
else
{
z[q].r = modify(m+1, r, z[q].r, p, v);
}
update(q);
return q;
}
int n, m;
int main()
{
cin>>n;
for(int i = 1; i <= n; i++)
{
cin>>a[i];
}
cin>>m;
root[0] = build(1, n);
for(int i = 1; i <= m; i++)
{
int op;
cin>>op;
if(op == 1)
{
int p, v;
cin>>p>>v;
root[i] = modify(1, n, root[i-1], p, v);
}
else
{
int k, l, r;
cin>>k>>l>>r;
cout<<query(1, n, root[k], l, r)<<'\n';
root[i] = root[i-1];
}
}
return 0;
}
可持久化数组
方法1:
每个位置开一个 \(vector\),存每一个修改的时间戳、值
查询的时候二分查找
方法2:
开一个 \(map<pair<int, int>, int>\)
这三个 \(int\) 分别表示位置、时间、值
查询直接 \(lower\)_\(bound\)
前缀值域可持久化线段树(主席树)
like this

点击查看代码
#include <iostream>
using std::cin;
using std::cout;
const int N = 2e5 + 10;
struct Node
{
int l, r; // 左儿子,右儿子
int sum; // 区间和
Node()
{
l = r = sum = 0;
}
} z[N * 30];
int n;
int cnt; // 节点数
int a[N];
int root[N]; // 第 i 个前缀应的值域线段树的根
void update(int p)
{
z[p].sum = z[z[p].l].sum + z[z[p].r].sum;
}
int build(int l, int r) // 返回这段区间对应的节点编号
{
cnt++;
int p = cnt;
if (l == r)
{
z[p].sum = a[l];
return p;
}
int m = (l + r) >> 1;
z[p].l = build(l, m);
z[p].r = build(m + 1, r);
update(p);
return p;
}
int query(int p1, int p2, int l, int r, int k)
// 当前对应的值域范围 l ~ r
// 要询问第 k 小的数
// 需要 p1, p2 两棵线段树来询问
{
if (l == r)
return l;
int m = (l + r) >> 1;
if (z[z[p2].l].sum - z[z[p1].l].sum >= k)
return query(z[p1].l, z[p2].l, l, m, k);
else
return query(z[p1].r, z[p2].r, m + 1, r, k - (z[z[p2].l].sum - z[z[p1].l].sum));
}
int modify(int l, int r, int rt, int p, int v)
{
cnt++;
int q = cnt; // 新的节点 q 用于修改
z[q] = z[rt];
if (l == r)
{
z[q].sum += v;
return q;
}
int m = (l + r) >> 1;
if (p <= m)
z[q].l = modify(l, m, z[q].l, p, v);
else
z[q].r = modify(m + 1, r, z[q].r, p, v);
update(q);
return q;
}
int main()
{
cin >> n;
int MAXV = 0;
for (int i = 1; i <= n; ++i)
cin >> a[i], MAXV = std::max(MAXV, a[i]);
int m;
cin >> m;
root[0] = 0;
for (int i = 1; i <= n; ++i)
root[i] = modify(1, MAXV, root[i - 1], a[i], 1);
// 主席树就建好了
for (int i = 1; i <= m; ++i)
{
int l, r, k;
cin >> l >> r >> k;
cout << query(root[l - 1], root[r], 1, MAXV, k);
}
return 0;
}
吃掉例题

可可爱爱的思考题捏
我最爱做题了

我们可以很容易的想到两种暴力的做法
一种为修改\(O(1)\),查询\(O(n)\)的
一种为修改\(O(n)\),查询\(O(1)\)的
我们应该思考把这两种想法综合一下,找出一种复杂度更加合理的做法,因为这种暴力的最坏复杂度是\(O(n^2)\)的
于是
根号分治
以 \(\sqrt n\)
为界限,如果一个点周围连了超过
\(\sqrt n\)个点,就称它为大点,否则称它为小点。
如果修改的一个点是小点,那么就直接暴力。
否则,如果这个点连的是大点,显然大点的数量不会超过
\(\sqrt n\)个;如果连的是小点的话,就处理出每个大点连的白小点和黑小点的数量
博客中的最后一题
思考了114min,竟然是Fibonacci斐波那契数列
先用 LCA 求出 l,r 之间的路径
fib 数列任意三项都不能组成三角形
树上三角形:每个值都 \(\leq\) 1e9
做 LCA
如果树上的点 $\geq $ 40,直接输出 \(Yes\)
\((∵fib_40 ≈ 1e9)\)
否则直接暴力
题单
https://www.luogu.com.cn/problem/P4513
https://www.luogu.com.cn/problem/P3372
https://www.luogu.com.cn/problem/P3373
https://www.luogu.com.cn/problem/P1253
https://www.luogu.com.cn/problem/SP1043
https://www.luogu.com.cn/problem/SP1716
https://www.luogu.com.cn/problem/SP2916
https://www.luogu.com.cn/problem/SP2713
http://cdqz.openjudge.cn/ds/1003/
http://cdqz.openjudge.cn/ds/1004/
http://cdqz.openjudge.cn/ds/1005/
https://vjudge.net/problem/HDU-3954#author=GPT_zh
https://hydro.ac/p/bzoj-P3333
https://www.luogu.com.cn/problem/P3919
https://www.luogu.com.cn/problem/P3834
http://cdqz.openjudge.cn/ds/1011/
http://cdqz.openjudge.cn/ds/1001/
http://cdqz.openjudge.cn/ds/1012/
https://vjudge.net/problem/HDU-3333#author=GPT_zh
https://vjudge.net/problem/HDU-4467#author=GPT_zh
https://hydro.ac/p/bzoj-P3251
(完成情况见luogu题单)

浙公网安备 33010602011771号