线段树高阶学习指南

典题合集

题目 思路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]

第一行两个整数,MD,其中 M 表示操作的个数,D 如上文中所述。

接下来的 M 行,每行一个字符串,描述一个具体的操作。语法如上文所述。

[output]

对于每一个查询操作,你应该按照顺序依次输出结果,每个结果占一行。

1M2×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]

您需要写一种数据结构(可参考题目标题),来维护一些数,其中需要提供以下操作:

  1. 插入 \(x\)
  2. 删除 \(x\) 数(若有多个相同的数,应只删除一个)
  3. 查询 \(x\) 数的排名(排名定义为比当前数小的数的个数 \(+1\) )
  4. 查询排名为 \(x\) 的数
  5. \(x\) 的前驱(前驱定义为小于 \(x\),且最大的数)
  6. \(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;
		}
	}
}
posted @ 2023-10-14 08:18  White_Sheep  阅读(8)  评论(0编辑  收藏  举报