可持久化线段树/主席树

主席树可以查询静态区间第k大,实际上是一种特殊的可持久化线段树,所以首先来说一下什么是可持久化线段树。

具体来说,可持久化线段树是可以保存所有历史版本的线段树,每次插入,都会创建一颗新树,保存这次插入后版本的线段树,返回这个版本的线段树根节点,后面想查询一个历史版本时,就使用这个版本的根节点,进行线段树查询就行了,具体查询和线段树正常查询完全一样

关键是,如何保证创建历史版本这个操作的复杂度?如果暴力创建,每次都是 O ( n l o g n ) O(nlogn) O(nlogn)的,显然太慢了。但是结合动态开点线段树的知识,我们知道,每次修改一个点,实际上只会影响一条链上的元素,最多 O ( l o g n ) O(logn) O(logn)个点,线段树中剩余的点其实完全没有变化,那其实我们可以在每次插入时,只对受到影响的点创建新的版本,然后接到未受影响的点上,这样每次插入,创建新版本的复杂度就是 O ( l o g n ) O(logn) O(logn)的了,具体可以看这个图
在这里插入图片描述
接下来再来看主席树,实际上是可持久化线段树的一个应用,只是节点权值比较特殊,具体来说:一个节点对应的区间是 [ l , r ] [l,r] [l,r]的话,那么 c n t cnt cnt保存值在 [ l , r ] [l,r] [l,r]范围内的元素的个数。

看到这里,如果熟悉线段树二分的话,你应该可以猜到主席树是怎么查询区间第 k k k小的了。首先把每个元素插入主席树,都会产生一个版本,那么第 i i i个版本保存的就是下标在 [ 1 , i ] [1,i] [1,i]的所有元素,其次,下标在 [ 1 , i ] [1,i] [1,i]内,值在 [ l , r ] [l,r] [l,r]内的元素个数,是可以前缀和的,因此我们用 [ 1 , l − 1 ] , [ 1 , r ] [1,l-1],[1,r] [1,l1],[1,r]两个版本的线段树的 c n t cnt cnt相减,就能得到下标在 [ l , r ] [l,r] [l,r]内,且值在某个范围内的的元素的个数,且这个个数随 [ 1 , i ] [1,i] [1,i] i i i的增加是不减的。

那么我们通过 [ 1 , r ] − [ 1 , l − 1 ] [1,r]-[1,l-1] [1,r][1,l1]得到了下标在 [ l , r ] [l,r] [l,r]内元素的 c n t cnt cnt,实际上就是得到了这个范围内元素的一个权值线段树,然后我们要找第 k k k大,这其实就是个线段树二分的经典问题,我们不断检查左右子树的值域内的元素个数,如果 k k k小于左子树的元素个数,就进入左子树,否则进入右子树,知道来到叶节点,就是我们要找的元素的值

总结一下,主席树就是通过可持久化线段树的历史版本来解决区间查询,然后用线段树二分,在权值线段树上找到第 k k k大的位置

具体仍然可以看下图,这里演示的是插入 [ 4 , 1 , 1 , 2 ] [4,1,1,2] [4,1,1,2]四个元素的情况
在这里插入图片描述
最后是板子,这里下标是从0开始的

class PersistentSegmentTree {
private:
    struct Node {
        Node *left, *right;
        int cnt;
        Node() : left(nullptr), right(nullptr), cnt(0) {}
        Node(Node* l, Node* r, int c) : left(l), right(r), cnt(c) {}
    };
    
    vector<Node*> roots;
    int n;
    
    Node* build(int l, int r) {
        Node* node = new Node();
        if (l == r) return node;
        
        int mid = (l + r) >> 1;
        node->left = build(l, mid);
        node->right = build(mid + 1, r);
        return node;
    }
    
    Node* update(Node* prev, int l, int r, int pos) {
        Node* curr = new Node(prev->left, prev->right, prev->cnt + 1);
        if (l == r) return curr;
        
        int mid = (l + r) >> 1;
        if (pos <= mid)
            curr->left = update(prev->left, l, mid, pos);
        else
            curr->right = update(prev->right, mid + 1, r, pos);
        return curr;
    }
    
    int query(Node* root1, Node* root2, int l, int r, int k) {
        if (l == r) return l;
        
        int mid = (l + r) >> 1;
        int leftCount = root2->left->cnt - root1->left->cnt;
        
        if (k <= leftCount)
            return query(root1->left, root2->left, l, mid, k);
        else
            return query(root1->right, root2->right, mid + 1, r, k - leftCount);
    }

public:
    // 使用数组初始化可持久化线段树
    PersistentSegmentTree(vector<int>& arr) {
        n = arr.size();
        if (n == 0) return;
        
        // 离散化
        vector<int> sorted = arr;
        sort(sorted.begin(), sorted.end());
        sorted.erase(unique(sorted.begin(), sorted.end()), sorted.end());
        
        // 建立映射关系
        unordered_map<int, int> mapping;
        for (int i = 0; i < sorted.size(); i++) {
            mapping[sorted[i]] = i + 1;
        }
        
        // 构建所有版本的线段树
        roots.push_back(build(1, sorted.size()));
        for (int i = 0; i < n; i++) {
            roots.push_back(update(roots.back(), 1, sorted.size(), mapping[arr[i]]));
        }
        
        // 保存离散化后的数组,用于还原查询结果
        sorted.insert(sorted.begin(), 0);  // 添加一个哨兵,使下标从1开始
        this->sorted = sorted;
    }
    
    // 查询区间[l,r]内的第k大值,l和r从0开始
    int queryKth(int l, int r, int k) {
        if (l < 0 || r >= n || k <= 0 || k > r - l + 1)
            return -1;  // 无效查询
        return sorted[query(roots[l], roots[r + 1], 1, sorted.size() - 1, k)];
    }
    
private:
    vector<int> sorted;  // 用于还原离散化的值
};
void solve(void){
	cin>>n>>m;
	vi a(n);
	rep(i,0,n-1)cin>>a[i];
	PersistentSegmentTree tr(a);
	rep(i,1,m){
		int l,r,k;
		cin>>l>>r>>k;
		l--,r--;
		cout<<tr.queryKth(l,r,k)<<'\n';
	}	
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值