线段树高阶学习指南
典题合集
题目 | 思路1 | col3 |
---|---|---|
动态维护连通块内第k小(永无乡) | 线段树合并+并查集+权值线段树 | |
前置芝士
区间加等差数列
原始序列与差分序列转化
6 2
1 2 3 4 5 6
2 5 1 2
首项:1 公差:2
区间:[2,5]
原始序列:1 2 3 4 5 6 差分序列:1 1 1 1 1 1
等差数列:1 3 5 7
加后序列:1 3 6 9 12 6 然后差分:1 2 3 3 3 -6
设差分序列为a,等差数列的首项为s,末项为e,公差为d
将区间[l,r]加上等差数列,等价于\(a_l+s\),\([a_{l+1},a_r]+d\),\(a_{r+1}-e\),这里的\(e=s+d*(r-l)\)。
线段树基本框架
线段树开4n空间
线段树一定是平衡二叉树
区间求和
//#define mid ((l+r)>>1)
//#define lc u<<1
//#define rc u<<1|1
const int N = 100010;
ll a[N];//初始化数组
struct SegTree {
ll sum[N * 4], add[N * 4];
void pushup(int u) {//向上传
sum[u] = sum[lc] + sum[rc];
}
void build(int u, int l, int r) {//初始化
sum[u] = a[l];
if (l == r) {
return;
}
build(lc, l, mid);
build(rc, mid + 1, r);
pushup(u);
}
void pushdown(int u, int l, int r) { //向下传
if (add[u]) {
sum[lc] += add[u] * (mid - l + 1);
sum[rc] += add[u] * (r - mid);
add[lc] += add[u];
add[rc] += add[u];
add[u] = 0;
}
}
void change(int u, int l, int r, int x, int y, int k) {//区间修改
if (x > r || y < l) return;
if (x <= l && y >= r) {
sum[u] += (r - l + 1) * k;
add[u] += k;
return;
}
pushdown(u, l, r);
change(lc, l, mid, x, y, k);
change(rc, mid + 1, r, x, y, k);
pushup(u);
}
ll query(int u, int l, int r, int x, int y) {//区间查询
if (x > r || y < l) return 0;
if (x <= l && r <= y) return sum[u];
pushdown(u, l, r);
return query(lc, l, mid, x, y) + query(rc, mid + 1, r, x, y);
}
} tr;
区间最值
[python]
class Tree():
def __init__(self):
self.l=0
self.r=0
self.lazy=0
self.val=0
tree=[Tree() for i in range(10*4)]
def build(p,l,r):
if l>r:
return
tree[p].l, tree[p].r, tree[p].lazy, tree[p].val = l, r, 0, 0
if l<r:
mid=(l+r)>>1
build(p<<1,l,mid)
build(p<<1|1,mid+1,r)
def pushUp(p):
tree[p].val=max(tree[p<<1].val,tree[p<<1|1].val)
#单点修改,添加值
def add(p,i,v):
if tree[p].l==tree[p].r:
tree[p].val+=v
else:
mid=(tree[p].l+tree[p].r)>>1
if i>mid:
add(p<<1|1,i,v)
else:
add(p<<1,i,v)
pushUp(p)
def pushdown(p):
if tree[p].lazy:
lazy=tree[p].lazy
tree[p<<1].lazy+=lazy
tree[p<<1|1].lazy+=lazy
tree[p<<1].val+=lazy
tree[p<<1|1].val+=lazy
tree[p].lazy=0
def update(p,l,r, v):
if l<=tree[p].l and r>=tree[p].r:
tree[p].lazy+=v
tree[p].val+=v
return
if r<tree[p].l or l>tree[p].r:
return
if tree[p].lazy:
pushdown(p)
update(p<<1,l,r,v)
update(p<<1|1,l,r,v)
pushUp(p)
def query(p,l,r):
if l<=tree[p].l and r>=tree[p].r:
return tree[p].val
if tree[p].l>r or tree[p].r<l:
return 0
if tree[p].lazy:
pushdown(p)
return max(query(p<<1,l,r),max(p<<1|1,l,r))
build(1,1,10)
update(1,1,5,1)
update(1,7,10,1)
update(1,2,8,1)
update(1,3,4,1)
update(1,9,10,1)
print(query(1,1,10))
[java]
class SegTree{
static final int INF=0x3f3f3f3f;
final int n;
final int[] min,lazy;
SegTree(int n) {
this.n=n;
min=new int[n<<2];
lazy=new int[n<<2];
Arrays.fill(min,INF);
Arrays.fill(lazy,INF);
}
void build(int[] a){
build(1,0,n-1,a);
}
void build(int p,int l,int r,int[] a){
// if(l>r) return;
if(l==r){
min[p]=a[l];
return;
}
int mid=(l+r)>>1;
build(p<<1,l,mid,a);
build(p<<1|1,mid+1,r,a);
min[p]=Math.min(min[p<<1],min[p<<1|1]);
}
void update(int i,int j,int val){
if(i>j) return;
update(i,j,val,0,n-1,1);
}
void update(int i,int j,int val,int st,int ed,int p){
if(i<=st && j>=ed){
min[p]=Math.min(min[p],val);
lazy[p]=Math.min(lazy[p],val);
return;
}
pushDown(p);
int mid=(st+ed)>>1;
if(i<=mid) update(i,j,val,st,mid,p<<1);
if(j>mid) update(i,j,val,mid+1,ed,p<<1|1);
pushUp(p);
}
int query(int i){
return query(0,i,0,n-1,1);
}
int query(int i,int j){
return query(i,j,0,n-1,1);
}
int query(int i,int j,int st,int ed,int p){
if(i<=st && j>=ed){
return min[p];
}
pushDown(p);
int ans=INF;
int mid=(st+ed)>>1;
if(i<=mid) ans=Math.min(ans,query(i,j,st,mid,p<<1));
if(j>mid) ans=Math.min(ans,query(i,j,mid+1,ed,p<<1|1));
return ans;
}
void pushUp(int p){
min[p]=Math.min(min[p<<1],min[p<<1|1]);
}
void pushDown(int p){
if(lazy[p]==INF) return;
min[p<<1]=Math.min(min[p<<1],lazy[p]);
min[p<<1|1]=Math.min(min[p<<1|1],lazy[p]);
lazy[p<<1]=Math.min(lazy[p<<1],lazy[p]);
lazy[p<<1|1]=Math.min(lazy[p<<1|1],lazy[p]);
lazy[p]=INF;
}
}
权值线段树
维护区间内数出现的次数,区间范围是数据的值域
值域:[1,10^6]
const int N = 100010;
struct weight_Segment_Tree{
int sum[N*4];
void pushup(int u) {
sum[u] = sum[lc] + sum[rc];
}
void change(int u, int l, int r, int x,int k) {//点修
if (l == r) {sum[u]+=k; return;}
if (x <= mid) change(lc, l, mid, x,k);
else change(rc, mid + 1, r, x,k);
pushup(u);
}
int query(int u, int l, int r, int x, int y) {//区查
if (x <= l && r <= y) return sum[u];
int s = 0;
if (x <= mid) s += query(lc, l, mid, x, y);
if (y > mid) s += query(rc, mid + 1,r, x, y);
return s;
}
}tr;
动态开点
普通线段树的空间复杂度为O(4n),\(n=10^9\)很大,直接爆内存。
我们不需要使用所有的节点,不用一次性建好树,可以动态开点,一边修改,查询,一边建树
我们不再\(u*2\)和\(u*2+1\)表示左右儿子,而是用两个数组ls[u]和rs[u]记录左右儿子的编号。
[空间复杂度分析]
设修改次数为m,分裂开点时,最坏的情况会走两个分支,到叶子节点。
所以单次修改最坏开点数为2logn,所以,空间复杂度为O(m*2logn)。
我们在实际创建空间时,不要考虑最坏的情况,可以缩小到符合要求的情况即可。
离散开点
可持久化线段树
线段树合并
同时维护乘法和加法
[problem presecption]
[支持的操作]
已知一个数列,你需要进行下面三种操作:
- 将某区间每一个数乘上 x
- 将某区间每一个数加上 x
- 求出某区间每一个数的和
[input]
第一行包含三个整数 n,m,p,分别表示该数列数字的个数、操作的总个数和模数。
第二行包含 n 个用空格分隔的整数,其中第 i 个数字表示数列第 i 项的初始值。
接下来 m 行每行包含若干个整数,表示一个操作,具体如下:
操作 1: 格式:1 x y k
含义:将区间 [x,y] 内每个数乘上 k
操作 2: 格式:2 x y k
含义:将区间[x,y] 内每个数加上 k
操作 3: 格式:3 x y
含义:输出区间 [x,y] 内每个数的和对 p 取模所得的结果
n=100000,q=100000
[solved]
/*
pushup
calc
pushdown
build
change
query
*/
const int N=100010;
ll n,p,q;
ll w[N];
struct Tree{
ll l,r,sum,mul,add;
}tr[N*4];
void pushup(ll u){
tr[u].sum=(tr[lc].sum+tr[rc].sum)%p;
}
//更新节点权值,并且更新懒标记
void calc(Tree& t,ll m,ll a){
t.sum=(t.sum*m+(t.r-t.l+1)*a)%p;
t.mul=t.mul*m%p;
t.add=(t.add*m+a)%p;
}
void pushdown(ll u){
calc(tr[lc],tr[u].mul,tr[u].add);
calc(tr[rc],tr[u].mul,tr[u].add);
tr[u].add=0;
tr[u].mul=1;
}
void build(ll u,ll l,ll r){
tr[u]={l,r,w[l],1,0};//节点初始化
if(l==r) return;
ll mid=(l+r)/2;
build(lc,l,mid);
build(rc,mid+1,r);
pushup(u);
}
void change(ll u,ll l,ll r,ll m,ll a){
if(l>tr[u].r||r<tr[u].l) return;
if(l<=tr[u].l&&tr[u].r<=r) {calc(tr[u],m,a);return;}
pushdown(u);
change(lc,l,r,m,a);
change(rc,l,r,m,a);
pushup(u);
}
ll query(ll u,ll l,ll r){
if(l>tr[u].r|| r<tr[u].l) return 0;
if(l<=tr[u].l&&tr[u].r<=r) return tr[u].sum;
pushdown(u);
return (query(lc,l,r)+query(rc,l,r))%p;
}
单点修改查询区间最大数
[problem presecetion]
维护一个数列,要求提供以下两种操作:
语法:Q L
功能:查询当前数列中末尾L个数中的最大的数,并输出这个数的值。
语法:A n
功能:将n加上t,其中t是最近一次查询操作的答案(如果还未执行过查询操作,则t=0),并将所得结果对一个固定的常数D取模,将所得答案插入到数列的末尾。
限制:n是整数(可能为负数)并且在长整范围内。
注意:初始时数列是空的,没有一个数。
[input]
第一行两个整数,M 和D,其中 M 表示操作的个数,D 如上文中所述。
接下来的 M 行,每行一个字符串,描述一个具体的操作。语法如上文所述。
[output]
对于每一个查询操作,你应该按照顺序依次输出结果,每个结果占一行。
1≤M≤2×10**5**,**1**≤**D**≤**2**×**1**09
[solved]
const int N = 200010;
int m, p;
struct Tree {
int l, r, mx;
} tr[N * 4];
void pushup(int u) {
tr[u].mx = max(tr[lc].mx, tr[rc].mx);
}
void build(int u, int l, int r) {
tr[u] = {l, r};
if (l == r) return ;
int m = l + r >> 1;
build(lc, l, m); build(rc, m + 1, r);
}
void change(int u, int x, int v) {
if (tr[u].l == x && tr[u].r == x) {tr[u].mx = v; return;}
int m = tr[u].l + tr[u].r >> 1;
if (x <= m) change(lc, x, v);
else change(rc, x, v);
pushup(u);
}
int query(int u, int l, int r) {
if (l <= tr[u].l && r >= tr[u].r) return tr[u].mx;
int m = tr[u].l + tr[u].r >> 1;
int res = -2e9;
if (l <= m) res = max(res, query(lc, l, r));
if (r > m) res = max(res, query(rc, l, r));
return res;
}
void solve() {
cin >> m >> p;
//先为m次操作预留空间
build(1, 1, m);
char c;
ll x;
//单点修改
ll n = 0, t = 0;
while (m--) {
cin >> c >> x;
if (c == 'A') change(1, ++n, (x + t) % p);
else {t = query(1, n - x + 1, n); cout << t << endl;}
}
}
数学计算
[problem presection]
现在有一个数 x,初始值为 1。小豆有Q 次操作,操作有两种类型:
1 m
:将 x 变为 x×m,并输出 xmodM。
2 pos
:将 x 变为 x 除以第 pos 次操作所乘的数(保证第 pos 次操作一定为类型 1,对于每一个类型 1 的操作至多会被除一次),并输出xmodM。
[input]
一共有 t 组输入。
对于每一组输入,第一行是两个数字 Q,M。
接下来Q 行,每一行为操作类型op,操作编号或所乘的数字m(保证所有的输入都是合法的)。
[output]
对于每一个操作,输出一行,包含操作执行后的xmodM 的值。
\(1≤Q≤10^5,t≤5,M≤10^9,0<m≤10^9\)
[solved]
/*
1.对Q次操作,建一颗叶子节点的值均为1的线段树。
2.对Q次操作看做对Q个叶子节点的单点修改,操作1把第i个叶子节点修改为m,
操作2把pos位置的叶子节点修改为。
3.线段树维护区间积,每次操作后,根节点的值即答案。
*/
const int N = 100005;
int M;
struct Tree {
int l, r;
int mul;
} tr[N * 4];
void pushup(int u) {
tr[u].mul = (1ll * tr[lc].mul * tr[rc].mul) % M;
}
void build(int u, int l, int r) {
tr[u] = {l, r, 1};
if (l == r) return;
int mid = (l + r) >> 1;
build(lc, l, mid);
build(rc, mid + 1, r);
pushup(u);
}
void change(int u, int x, int c) { //点修
if (x == tr[u].l && x == tr[u].r) {
tr[u].mul = c; return;
}
int mid = tr[u].l + tr[u].r >> 1;
if (x <= mid) change(lc, x, c);
if (x > mid) change(rc, x, c);
pushup(u);
}
void solve() {
int t; cin >> t;
for (int i = 1; i <= t; i++) {
int q, op, m, pos;
cin >> q >> M;
build(1, 1, q);
for (int i = 1; i <= q; i++) {
cin >> op;
if (op == 1) {
cin >> m, change(1, i, m);
cout << tr[1].mul << endl;
} else
{cin >> pos, change(1, pos, 1); cout << tr[1].mul << endl;}
}
}
}
逆序对
[problem presection]
“逆序对”,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中 \(a_i>a_j\) 且 \(i<j\) 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。
[input]
第一行,一个数 \(n\),表示序列中有 \(n\)个数。
第二行 \(n\) 个数,表示给定的序列。序列中每个数字不超过 \(10^9\)。
[output]
输出序列中逆序对的数目。
\(n \leq 5 \times 10^5\)
[solved]
单点修改,区间查询
大范围数据离散化
const int N=500010;
int a[N],b[N];
int n;
int sum[N*4];
void pushup(int u){
sum[u]=sum[lc]+sum[rc];
}
void change(int u,int l,int r,int x){
if(l==r) {sum[u]+=1;return;}
int mid=(l+r)>>1;
if(x<=mid) change(lc,l,mid,x);
else change(rc,mid+1,r,x);
pushup(u);
}
ll query(int u,int l,int r,int x,int y){
if(x<=l&&y>=r) return sum[u];
ll res=0;
int mid=(l+r)>>1;
if(x<=mid) res+=query(lc,l,mid,x,y);
if(y>mid) res+=query(rc,mid+1,r,x,y);
return res;
}
void solve() {
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
b[i]=a[i];
}
sort(b+1,b+n+1);
ll res=0;
for(int i=1;i<=n;i++){
int id=lower_bound(b+1,b+n+1,a[i])-b;
// cout<<id<<endl;
change(1,1,n,id);
// cout<<query(1,1,n,id+1,n)<<endl;
res+=query(1,1,n,id+1,n);
}
cout<<res<<endl;
}
普通平衡树
[problem description]
您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:
- 插入 \(x\) 数
- 删除 \(x\) 数(若有多个相同的数,应只删除一个)
- 查询 \(x\) 数的排名(排名定义为比当前数小的数的个数 \(+1\) )
- 查询排名为 \(x\) 的数
- 求 \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)
- 求 \(x\) 的后继(后继定义为大于 \(x\),且最小的数)
[input]
第一行为 \(n\),表示操作的个数,下面 \(n\) 行每行有两个数 \(\text{opt}\) 和 \(x\),\(\text{opt}\) 表示操作的序号( $ 1 \leq \text{opt} \leq 6 $ )
[output]
对于操作 \(3,4,5,6\) 每行输出一个数,表示对应答案
[datas]
\(1\le n \le 10^5\),\(|x| \le 10^7\)
[solved]
权值线段树+离散化来实现普通平衡树。
优点:相比较Splay算法,相同的复杂度,同样支持动态,权值树代码量少,易于调整,优势明显。
把除操作4以为的其他操作所涉及的数,先进行离散化,再用权值线段树来维护这些数出现的次数,即可实现所有操作。
const int N = 100010;
int op[N], a[N];
int sum[N * 4];
int b[N];
int m;
int n;
void pushup(int u) {
sum[u] = sum[lc] + sum[rc];
}
void change(int u, int l, int r, int x, int k) {
if (l == r) {sum[u] += k; return;}
int mid = (l + r) >> 1;
if (x <= mid) change(lc, l, mid, x, k);
else change(rc, mid + 1, r, x, k);
pushup(u);
}
//排名即为前缀和
int q_rank(int u, int l, int r, int x, int y) {
if (x <= l && y >= r) return sum[u];
int s = 0;
int mid = (l + r) >> 1;
if (x <= mid) s += q_rank(lc, l, mid, x, y);
if (y > mid) s += q_rank(rc, mid + 1, r, x, y);
return s;
}
//查询排名为x的数
int q_num(int u, int l, int r, int x) {
if (l == r) return l;
int mid = (l + r) >> 1;
if (x <= sum[lc]) return q_num(lc, l, mid, x);
else return q_num(rc, mid + 1, r, x - sum[lc]);
}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> op[i] >> a[i];
if (op[i] == 4) continue;
b[++m] = a[i];
}
sort(b + 1, b + m + 1);
m = unique(b + 1, b + m + 1) - b - 1;
int id;
for (int i = 1; i <= n; i++) {
if (op[i] != 4) id = lower_bound(b + 1, b + m + 1, a[i]) - b;
if (op[i] == 1) change(1, 1, m, id, 1);
else if (op[i] == 2) change(1, 1, m, id, -1);
else if (op[i] == 3) {int kth = id > 1 ? q_rank(1, 1, m, 1, id - 1) + 1 : 1; cout << kth << endl;}
else if (op[i] == 4) {
int idx = q_num(1, 1, m, a[i]);
cout << b[idx] << endl;
} else if (op[i] == 5) {
int rk = q_rank(1, 1, m, 1, id - 1);
cout << b[q_num(1, 1, m, rk)] << endl;
} else {
//可能此时x还没有放入线段树中,但又是最大的。
int rk = q_rank(1, 1, m, 1, id)+1;
cout << b[q_num(1, 1, m, rk)] << endl;
}
}
}
KUR-Couriers
[problem description]
给一个长度为 \(n\) 的正整数序列 \(a\)。共有 \(m\) 组询问,每次询问一个区间 \([l,r]\) ,是否存在一个数在 \([l,r]\) 中出现的次数严格大于一半。如果存在,输出这个数,否则输出 \(0\)。
[datas]
\(1 \leq n,m \leq 5 \times 10^5\),\(1 \leq a_i \leq n\)。
[sample]
a.in
7 5
1 1 3 2 3 4 3
1 3
1 4
3 7
1 7
6 6
a.out
1
0
3
0
4
[solved]
询问区间某数的出现次数,我们用持久化线段树维护
建持久树,把每个数字依次插入持久树中,版本号就是序列的下标,用sum记录每个数字的出现次数。
查询[l,r],从r和l-1两个版本进入,同步搜索,如果左区间的数字个数满足条件,就继续搜索左区间,直到叶子节点,即走到答案。如果左区间搜索失败,再搜索右区间。如果两个都不行,就返回0.
const int N = 500010;
int ls[N * 25], rs[N * 25], sum[N * 25];
int root[N], tot;
int n, m;
void change(int& u, int v, int l, int r, int x) {
u = ++tot;
ls[u] = ls[v]; rs[u] = rs[v]; sum[u] = sum[v] + 1;
if (l == r) return;
int mid = (l + r) >> 1;
if (x <= mid) change(ls[u], ls[v], l, mid, x);
if (x > mid) change(rs[u], rs[v], mid + 1, r, x);
}
int query(int u, int v, int l, int r, int len) {
if (l == r) return l;
int mid = (l + r) >> 1;
if (sum[ls[u]] - sum[ls[v]] > len / 2) return query(ls[u], ls[v], l, mid, len);
if (sum[rs[u]] - sum[rs[v]] > len / 2) return query(rs[u], rs[v], mid + 1, r, len);
return 0;
}
void solve() {
//freopen("a.in","r",stdin);
//freopen("a.out","w",stdout);
cin >> n >> m;
for (int i = 1, x; i <= n; i++) {
cin >> x;
change(root[i], root[i - 1], 1, n, x);
}
for (int i = 1, l, r; i <= m; i++) {
cin >> l >> r;
cout << query(root[r], root[l - 1], 1, n, r - l + 1) << endl;
}
}
HH的项链
[problem description]
HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。
有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。
[input]
一行一个正整数 \(n\),表示项链长度。
第二行 \(n\) 个正整数 \(a_i\),表示项链中第 \(i\) 个贝壳的种类。
第三行一个整数 \(m\),表示 HH 询问的个数。
接下来 \(m\) 行,每行两个整数 \(l,r\),表示询问的区间。
[output]
输出 \(m\) 行,每行一个整数,依次表示询问对应的答案。
[sample]
in
6
1 2 3 4 3 5
3
1 2
3 5
2 6
out
2
2
4
[datas]
\(1\le n,m,a_i \leq 10^6\),\(1\le l \le r \le n\)
[solved]
本题可以用可持久化线段树创建序列的各个版本
查询区间[l,r]内不同数字的个数,可以转化为查询r版本的线段树的l点。
(1)空间设置:因为两次change,所以\(n*log*2=N*40\)
(2)点修:如果当前数字是第一次出现,则直接添加分支,节点记录该区间内不同数字出现的次数。
如果当前数字不是第一次出现,则先创建一个辅助版本rt,把它上一次出现的叶子位置变为0,相应分支系节点均减1,再创当前版本。
(3)每个版本只保留每个数字最后一次出现的位置为1,先前出现的位置为0。
(4)点查:查询区间[l,r]内不同的数字个数,等价于查询r版本的线段树,到达叶子[l,l],即得到答案。
右边界r是确定的,如果走左区间,要加上右区间的sum(因为每个区间记录了不同数字的个数)。如果走右区间,继续分裂即可。
int n, m, a[N], last[N];//a_i上次出现的位置
int root[N], tot;//根节点,节点个数
int ls[N * 40], rs[N * 40], sum[N * 40];
//点修
void change(int &u,int v,int l,int r,int p,int k){
u=++tot;//动态开点
ls[u]=ls[v];rs[u]=rs[v];sum[u]=sum[v]+k;
if(l==r) return;//双指针同步搜索
int mid=(l+r)>>1;
if(p<=mid) change(ls[u],ls[v],l,mid,p,k);
else change(rs[u],rs[v],mid+1,r,p,k);
// cout<<sum[u]<<endl;
}
//点查
int query(int u,int l,int r,int p){
if(l==r) return sum[u];
int mid=(l+r)>>1;
if(p<=mid) return query(ls[u],l,mid,p)+sum[rs[u]];
else return query(rs[u],mid+1,r,p);
}
void solve() {
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
for (int i=1, rt; i <= n; i++) {
if (!last[a[i]]) change(root[i], root[i - 1], 1, n, i, 1);
else {
change(rt, root[i - 1], 1, n, last[a[i]], -1);
change(root[i], rt, 1, n, i, 1);
}
last[a[i]] = i;//记录a_i出现的位置
}
cin >> m;
int l, r;
for (int i = 1; i <= m; i++) {
cin >> l >> r;
cout << query(root[r], 1, n, l) << endl;
}
}
静态区间第k小
[problem description]
这是个非常经典的可持久化权值线段树入门题——静态区间第 \(k\) 小。
如题,给定 \(n\) 个整数构成的序列 \(a\),将对于指定的闭区间 \([l, r]\) 查询其区间内的第 \(k\) 小值。
[input]
第一行包含两个整数,分别表示序列的长度 \(n\) 和查询的个数 \(m\)。
第二行包含 \(n\) 个整数,第 \(i\) 个整数表示序列的第 \(i\) 个元素 \(a_i\)。
接下来 \(m\) 行每行包含三个整数 $ l, r, k$ , 表示查询区间 \([l, r]\) 内的第 \(k\) 小值。
[output]
对于每次询问,输出一行一个整数表示答案。
[sample]
in
5 5
25957 6405 15770 26287 26465
2 2 1
3 4 1
4 5 1
1 2 2
4 4 1
out
6405
15770
26287
25957
26287
\(1 \leq n,m \leq 2\times 10^5\),\(|a_i| \leq 10^9\),\(1 \leq l \leq r \leq n\),\(1 \leq k \leq r - l + 1\)
[solved]
/*动态开点*/
/*tr[]:开log[N]*N空间*/
const int N=200010;
struct node{
//s:节点值域中有多少个数
int l,r,s;
}tr[N*20];
int root[N],idx=0;
int n,m,a[N];
vector<int> b;
void build(int &x,int l,int r){
x=++idx;
if(l==r) return;
int m=l+r>>1;
build(lc(x),l,m);
build(rc(x),m+1,r);
}
void insert(int x,int &y,int l,int r,int k){
y=++idx;tr[y]=tr[x];tr[y].s++;
if(l==r) return;
int m=l+r>>1;
if(k<=m) insert(lc(x),lc(y),l,m,k);
else insert(rc(x),rc(y),m+1,r,k);
}
int query(int x,int y,int l,int r,int k){
if(l==r) return l;
int m=l+r>>1;
int s=tr[lc(y)].s-tr[lc(x)].s;
if(k<=s) return query(lc(x),lc(y),l,m,k);
else return query(rc(x),rc(y),m+1,r,k-s);
}
void solve(){
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i],b.push_back(a[i]);
sort(b.begin(),b.end());
b.erase(unique(b.begin(),b.end()),b.end());
int bn=b.size();
build(root[0],1,bn);
for(int i=1;i<=n;i++){
int id=lower_bound(b.begin(),b.end(),a[i])-b.begin()+1;
insert(root[i-1],root[i],1,bn,id);
}
while(m--){
int l,r,k;
cin>>l>>r>>k;
int id=query(root[l-1],root[r],1,bn,k)-1;
cout<<b[id]<<endl;
}
}
雨天的尾巴
[problem description]
首先村落里的一共有 \(n\) 座房屋,并形成一个树状结构。然后救济粮分 \(m\) 次发放,每次选择两个房屋 \((x, y)\),然后对于 \(x\) 到 \(y\) 的路径上(含 \(x\) 和 \(y\))每座房子里发放一袋 \(z\) 类型的救济粮。
然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。
[input]
输入的第一行是两个用空格隔开的正整数,分别代表房屋的个数 \(n\) 和救济粮发放的次数 \(m\)。
第 \(2\) 到 第 \(n\) 行,每行有两个用空格隔开的整数 \(a, b\),代表存在一条连接房屋 \(a\) 和 \(b\) 的边。
第 \((n + 1)\) 到第 \((n + m)\) 行,每行有三个用空格隔开的整数 \(x, y, z\),代表一次救济粮的发放是从 \(x\) 到 \(y\) 路径上的每栋房子发放了一袋 \(z\) 类型的救济粮。
[output]
输出 \(n\) 行,每行一个整数,第 \(i\) 行的整数代表 \(i\) 号房屋存放最多的救济粮的种类,如果有多种救济粮都是存放最多的,输出种类编号最小的一种。
如果某座房屋没有救济粮,则输出 \(0\)。
[a.in]
5 3
1 2
3 1
3 4
5 3
2 3 3
1 5 2
3 3 3
[a.out]
2
3
3
0
2
[datas]
\(1 \leq n, m \leq 10^5\),\(1 \leq a,b,x,y \leq n\),\(1 \leq z \leq 10^5\)
[solved]
const int N = 100010;
int n, m, ans[N];
vector<int> e[N];
int fa[N][20], dep[N];
int root[N], tot;
int ls[N * 50], rs[N * 50], sum[N * 50], typ[N * 50];
//sum:某种救济粮的数量
//typ:救济粮的类型
void pushup(int u) {//上传
if (sum[ls[u]] >= sum[rs[u]]) {
sum[u] = sum[ls[u]], typ[u] = typ[ls[u]];
} else {
sum[u] = sum[rs[u]], typ[u] = typ[rs[u]];
}
}
void change(int &u, int l, int r, int p, int k) {//点修
if (!u) u = ++tot;
if (l == r) {sum[u] += k; typ[u] = p; return;}
if (p <= mid) change(ls[u], l, mid, p, k);
else change(rs[u], mid + 1, r, p, k);
pushup(u);
}
void dfs(int u, int father) {
dep[u] = dep[father] + 1;
fa[u][0] = father;
for (int i = 1; i <= 18; i++) {
fa[u][i] = fa[fa[u][i - 1]][i - 1];
}
for (auto v : e[u]) {
if (v == father) continue;
dfs(v, u);
}
}
int lca(int u, int v) {//lca板子
if (dep[u] < dep[v]) swap(u, v);
// if(u==v) return v;
for (int i = 18; i >= 0; i--) {
if (dep[fa[u][i]] >= dep[v]) u = fa[u][i];
}
if (u == v) return v;
for (int i = 18; i >= 0; i--) {
if (fa[u][i] != fa[v][i]) {
u = fa[u][i];
v = fa[v][i];
}
}
return fa[u][0];
}
int merge(int x, int y, int l, int r) {//合并
if (!x || !y) return x + y;//一个为空,就返回另一个
if (l == r) {sum[x] += sum[y]; return x;}
ls[x] = merge(ls[x], ls[y], l, mid);
rs[x] = merge(rs[x], rs[y], mid + 1, r);
pushup(x);
return x;
}
void calc(int u, int father) {//统计
for (int v : e[u]) {
if (v == father) continue;
calc(v, u);
root[u] = merge(root[u], root[v], 1, N);
}
ans[u] = sum[root[u]] ? typ[root[u]] : 0;
}
void solve() {
//freopen("a.in","r",stdin);
//freopen("a.out","w",stdout);
cin >> n >> m;
for (int i = 1, x, y; i < n; i++) {
cin >> x >> y;
e[x].push_back(y);
e[y].push_back(x);
}
dfs(1, 0);//树上倍增
for (int i = 1, x, y, z; i <= m; i++) {//差分
cin >> x >> y >> z;
change(root[x], 1, N, z, 1);
change(root[y], 1, N, z, 1);
int t = lca(x, y);
// cout<<t<<endl;
change(root[t], 1, N, z, -1);
change(root[fa[t][0]], 1, N, z, -1);
}
calc(1, 0);//统计
for (int i = 1; i <= n; i++) {
cout << ans[i] << endl;
}
}
永无乡
[problem description]
永无乡包含 \(n\) 座岛,编号从 \(1\) 到 \(n\) ,每座岛都有自己的独一无二的重要度,按照重要度可以将这 \(n\) 座岛排名,名次用 \(1\) 到 \(n\) 来表示。某些岛之间由巨大的桥连接,通过桥可以从一个岛到达另一个岛。如果从岛 \(a\) 出发经过若干座(含 \(0\) 座)桥可以 到达岛 \(b\) ,则称岛 \(a\) 和岛 \(b\) 是连通的。
现在有两种操作:
B x y
表示在岛 \(x\) 与岛 \(y\) 之间修建一座新桥。
Q x k
表示询问当前与岛 \(x\) 连通的所有岛中第 \(k\) 重要的是哪座岛,即所有与岛 \(x\) 连通的岛中重要度排名第 \(k\) 小的岛是哪座,请你输出那个岛的编号。
[input]
第一行是用空格隔开的两个整数,分别表示岛的个数 \(n\) 以及一开始存在的桥数 \(m\)。
第二行有 \(n\) 个整数,第 \(i\) 个整数表示编号为 \(i\) 的岛屿的排名 \(p_i\)。
接下来 \(m\) 行,每行两个整数 \(u, v\),表示一开始存在一座连接编号为 \(u\) 的岛屿和编号为 \(v\) 的岛屿的桥。
接下来一行有一个整数,表示操作个数 \(q\)。
接下来 \(q\) 行,每行描述一个操作。每行首先有一个字符 \(op\),表示操作类型,然后有两个整数 \(x, y\)。
- 若 \(op\) 为
Q
,则表示询问所有与岛 \(x\) 连通的岛中重要度排名第 \(y\) 小的岛是哪座,请你输出那个岛的编号。 - 若 \(op\) 为
B
,则表示在岛 \(x\) 与岛 \(y\) 之间修建一座新桥。
[output]
对于每个询问操作都要依次输出一行一个整数,表示所询问岛屿的编号。如果该岛屿不存在,则输出 \(-1\) 。
[a.in]
5 1
4 3 2 5 1
1 2
7
Q 3 2
Q 2 1
B 2 3
B 1 5
Q 2 1
Q 2 4
Q 2 3
[a.out]
-1
2
5
1
2
[datas]
\(1 \leq m \leq n \leq 10^5\), \(1 \leq q \leq 3 \times 10^5\),\(p_i\) 为一个 \(1 \sim n\) 的排列,\(op \in \{\texttt Q, \texttt B\}\),\(1 \leq u, v, x, y \leq n\)
[solved]
查询第k小值,权值线段树
合并操作,线段树合并
维护连通性,并查集维护
每个节点开一颗线段树
重要度为线段树的下标
维护出现次数sum和节点编号id
并查集的合并和线段树的合并方向要一致
并查集:f[y]=x,x做并查集的根
线段树合并:把y合并到x上
先找出并查集的根,再查询
const int N = 100010;
int n, m, q, fa[N]; //f:并查集
int root[N], tot; //根节点,开点个数
int ls[N * 20], rs[N * 20], id[N * 20], sum[N * 20];
//id:节点编号,sum:重要度的出现次数之和
int find(int x) { //找根
while (x != fa[x]) x = fa[x] = fa[fa[x]];
return x;
}
void pushup(int u) { //上传
sum[u] = sum[ls[u]] + sum[rs[u]];
}
int change(int u, int l, int r, int p, int i) { //点修
if (!u) u = ++tot;
if (l == r) {id[u] = i; sum[u]++; return u;}
if (p <= mid) ls[u] = change(ls[u], l, mid, p, i);
else rs[u] = change(rs[u], mid + 1, r, p, i);
pushup(u); return u;
}
int merge(int x, int y) { //合并
if (!x || !y) return x + y;
ls[x] = merge(ls[x], ls[y]);
rs[x] = merge(rs[x], rs[y]);
pushup(x); return x;
}
int query(int u, int l, int r, int k) { //点查
if (l == r) return id[u];
int ans = 0;
if (k <= sum[ls[u]]) ans = query(ls[u], l, mid, k);
else ans = query(rs[u], mid + 1, r, k - sum[ls[u]]);
return ans;
}
void solve() {
//freopen("a.in","r",stdin);
//freopen("a.out","w",stdout);
cin >> n >> m;
int x, y;
for (int i = 1; i <= n; i++) {
fa[i] = i;
cin >> x;
root[i] = change(root[i], 1, n, x, i);
}
for (int i = 1; i <= m; i++) {
cin >> x >> y;
x = find(x);
y = find(y);
fa[y] = x;
root[x] = merge(root[x], root[y]);
}
cin >> q;
while (q--) {
char ch;
cin >> ch;
if (ch== 'B') {
cin >> x >> y;
x = find(x); y = find(y);
if (x == y) continue;
fa[y] = x;
root[x] = merge(root[x], root[y]);
} else {
cin >> x >> y;
x = find(x);
int res = query(root[x], 1, n, y);
res = res ? res : -1;
cout << res << endl;
}
}
}