zoukankan      html  css  js  c++  java
  • 可持久化线段树(主席树)

    可持久化线段(主席树)详解(两个题目):

    本篇博客借鉴了此篇博客,补充了一些自己的理解
    现有这样一个题目:

    给定长为n的一个序列,q次询问,每次询问l,r区间内第k大的数是多少
    (n,q<=200,000)

    求解这个问题,我们便要用到主席树。
    首先了解一下什么叫主席树:

    主席树,即可持久化线段树,是支持查询历史版本的一种线段树的升级版
    主席树是,对于一个序列[1...n]的每一个前缀[1...i]都建立一颗线段树。我们知道线段树的每一个节点对应的是一个区间[l, r],则每一个节点对应的sum[rt是在的[1...i]中的数,在区间[l, r]的有多少个。询问时利用前缀的思想进行求解的一种树。(不理解没关系我们慢慢讲)

    下面我们详细讲一下怎么写主席树:
    先做好准备工作,因为主席树一个区间[l , r]代表的是[l , r]的数,所以我们要把数据离散化,才能用主席树表示。

    for (int i = 1; i <= n; i++) {
    		a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
    }
    

    之后便是主席树的部分了。

    我们用一组数据来讲一下:

    3 1
    3 1 2
    1 2 2
    
    • 首先建一棵空树,建树中不同于线段树的有几点:
    1. 主席树因为在建立n棵树的时候必须要利用当前树和前一棵树的相似之处,所以左右节点和根节点之间可能不满足lson = rt << 1, rson = rt << 1 | 1。所以我们在建树时直接用一个计数器++来记录当前节点是哪个节点。
    2. 在建树时,用L数组和R数组记录这个节点指向左右子树的根节点的值。
    int build(int l, int r) {//建一棵空树
    	int rt = ++cnt;
    	sum[rt] = 0;
    	if (l < r) {
    		L[rt] = build(l, mid);
    		R[rt] = build(mid + 1, r);
    	}
    	return rt;
    }
    

    建完后如图:
    在这里插入图片描述

    • 再建n棵前缀的树,我们先看图:

    第一棵
    在这里插入图片描述
    第二棵:
    在这里插入图片描述
    第三棵:
    在这里插入图片描述
    要注意的有几点:

    1. 如果对于每一棵[1...i]的树都建立一颗完整的线段树,那么空间复杂度必然爆炸。所以我们要利用第i棵线段树和第i - 1棵线段树的相同特征来建树,我们观察到第i棵树和第i - 1棵树之间仅有一条路径不同,对于这条路径建树的空间复杂度仅为log(n)级别,其他的部分我们只要复制上一棵树的就可以了,这样总复杂度为nlogn。
    2. 关于如何复制,我们之前有L和R数组,分别指向左右子树,我们在建一棵新树时,先让这棵新树的每一个结点的左右节点指向原来的树的左右节点,之后递归修改sum的时候,我们把新的路径的节点值返回给对应的L和R数组,这样就对于这条路径的每一个点都有一个正确的L, R,达到了建树的目的。
    3. 递归经过的地方必然会使这个节点对应的sum++。
    • 之后便是询问:

    我们现在有n棵线段树,每颗线段树都存了到i为止的所有区间的数的个数。我们可以很容易明白询问是满足可减性的,因为假设我要询问序列[2 , 5]内3的个数,就可以用[1 , 5]内3的个数减去[1 , 2]内3的个数。
    对于[l , r]区间求第k大,我们每次可以算出第r棵线段树内和第l - 1棵线段树内左子树sum的差值x,这个差值x就说明了左子树对应的数的区间有多少个数,假如这个数大于等于k,我们就接着向左子树找,否则我们就向右子树找k - x。即可找到对应的第k大。

    int query(int u, int v, int l, int r, int k) {
    	if (l >= r) return l;
    	int x = sum[L[v]] - sum[L[u]];
    	if (x >= k) return query(L[u], L[v], l, mid, k);
    	else return query(R[u], R[v], mid + 1, r, k - x);
    }
    

    题目地址:洛谷P3834
    最后附上完整ac代码:

    #include<bits/stdc++.h>
    #define maxn 2000050
    #define mid ((l + r) >> 1)
    using namespace std;
    
    int a[maxn], b[maxn], T[maxn];
    
    inline int getnum() {
    	char c; int ans = 0; int flag = 1;
    	while (!isdigit(c = getchar()) && c != '-');
    	if (c == '-') flag = -1; else ans = c - '0';
    	while (isdigit(c = getchar())) ans = ans * 10 + c - '0';
    	return ans * flag;
    }
    
    int L[maxn << 5], R[maxn << 5];//指向左子树和右子树的根
    int sum[maxn << 5];//离散化去重后的每个节点所包含的数的个数
    int cnt;//所有节点的计数器
    
    int build(int l, int r) {//建一棵空树
    	int rt = ++cnt;
    	sum[rt] = 0;
    	if (l < r) {
    		L[rt] = build(l, mid);
    		R[rt] = build(mid + 1, r);
    	}
    	return rt;
    }
    
    int update(int pre, int l, int r, int x) {
    	int rt = ++cnt;
    	L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + 1;
    	if (l < r) {
    		if (x <= mid) L[rt] = update(L[pre], l, mid, x);
    		else R[rt] = update(R[pre], mid + 1, r, x);
    	}
    	return rt;
    }
    
    int query(int u, int v, int l, int r, int k) {
    	if (l >= r) return l;
    	int x = sum[L[v]] - sum[L[u]];
    	if (x >= k) return query(L[u], L[v], l, mid, k);
    	else return query(R[u], R[v], mid + 1, r, k - x);
    }
    
    int main() {
    	int n = getnum(), q = getnum();
    	for (int i = 1; i <= n; i++) {
    		a[i] = getnum();
    		b[i] = a[i];
    	}
    	sort(b + 1, b + n + 1);
    	int m = unique(b + 1, b + n + 1) - b - 1;
    	T[0] = build(1, m);
    	for (int i = 1; i <= n; i++) {
    		a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
    		T[i] = update(T[i - 1], 1, m, a[i]);
    	}
    	while (q--) {
    		int x = getnum(), y = getnum(), z = getnum();
    		printf("%d
    ", b[query(T[x - 1], T[y], 1, m, z)]);
    	}
    	return 0;
    }
    

    我们再做一个主席树的题练练手:

    CSU 1981: 小M的魔术表演

    题面

    Description

    小M听说会变魔术的男生最能吸引女生注意啦~所以小M费了九牛二虎之力终于学会了一个魔术:
    首先在桌面上放N张纸片,每张纸片上都写有一个数字。小M每次请女生给出一个数字x,然后划定任意一个区间[L,R],小M就能立马告诉对方这个区间内有多少个数字比x小。
    小M当然是知道答案的啦,但是你呢?

    Input

    第一行为一个数字T(T<=10)表示数据组数
    第二行为两个数字n、m(1<=n,m<=200000)表示序列长度和询问次数
    第三行为n个数字表示原始序列A (0 < A[i] < 1000000000)
    接下来m行,每行三个数字l r x 表示询问[l,r]之间小于x的有几个(1<=l<=r<=n,0<=x<=1000000000)
    保证数据合法

    Output

    输出为m行,第i行表示第i个询问的答案

    Sample Input

    1
    10 3
    2 3 6 9 8 5 4 7 1 1
    1 3 5
    2 8 7
    3 6 4
    

    Sample Output

    2
    4
    0
    

    题解

    这个题也是主席树,我们只需改动几个小地方即可

    #include<bits/stdc++.h>
    #define maxn 200050
    #define mid ((l + r) >> 1)
    using namespace std;
    
    int a[maxn], b[maxn], T[maxn];
    
    inline int getnum() {
    	char c; int ans = 0; int flag = 1;
    	while (!isdigit(c = getchar()) && c != '-');
    	if (c == '-') flag = -1; else ans = c - '0';
    	while (isdigit(c = getchar())) ans = ans * 10 + c - '0';
    	return ans * flag;
    }
    
    int L[maxn << 5], R[maxn << 5];//指向左子树和右子树的根
    int sum[maxn << 5];//离散化去重后的每个节点所包含的数的个数
    int cnt;//所有节点的计数器
    
    int build(int l, int r) {//建一棵空树
    	int rt = ++cnt;
    	sum[rt] = 0;
    	if (l < r) {
    		L[rt] = build(l, mid);
    		R[rt] = build(mid + 1, r);
    	}
    	return rt;
    }
    
    int update(int pre, int l, int r, int x) {
    	int rt = ++cnt;
    	L[rt] = L[pre], R[rt] = R[pre], sum[rt] = sum[pre] + 1;
    	if (l < r) {
    		if (x <= mid) L[rt] = update(L[pre], l, mid, x);
    		else R[rt] = update(R[pre], mid + 1, r, x);
    	}
    	return rt;
    }
    
    int query(int u, int v, int l, int r, int k) {
    	if (l >= r) {
    		if (l != k)
    			return sum[v] - sum[u];//找到不等于k的才返回这个
    		else return 0;//因为题中让求小于k的,等于k的返回0
    	}
    	int ans = sum[L[v]] - sum[L[u]];//个数
    	if (mid >= k) return query(L[u], L[v], l, mid, k);//大于k,向左子树找
    	else return ans + query(R[u], R[v], mid + 1, r, k);//否则向右子树找,加上左子树的个数
    }
    
    int main() {
    	int t = getnum();
    	while (t--) {
    		int n = getnum(), q = getnum();
    		cnt = 0;
    		memset(L, 0, sizeof(L));
    		memset(R, 0, sizeof(R));
    		memset(sum, 0, sizeof(sum));
    		memset(T, 0, sizeof(T));
    		for (int i = 1; i <= n; i++) {
    			a[i] = getnum();
    			b[i] = a[i];
    		}
    		sort(b + 1, b + n + 1);
    		int m = unique(b + 1, b + n + 1) - b - 1;
    		T[0] = build(1, m);
    		for (int i = 1; i <= n; i++) {
    			a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
    			T[i] = update(T[i - 1], 1, m, a[i]);
    		}
    		while (q--) {
    			int x = getnum(), y = getnum(), z = getnum();
    			z = lower_bound(b + 1, b + m + 1, z) - b;
    			printf("%d
    ", query(T[x - 1], T[y], 1, m, z));
    		}
    	}
    	return 0;
    }
    
  • 相关阅读:
    176. Second Highest Salary
    175. Combine Two Tables
    172. Factorial Trailing Zeroes
    171. Excel Sheet Column Number
    169. Majority Element
    168. Excel Sheet Column Title
    167. Two Sum II
    160. Intersection of Two Linked Lists
    个人博客记录
    <meta>标签
  • 原文地址:https://www.cnblogs.com/artoriax/p/10346757.html
Copyright © 2011-2022 走看看