• 主席树学习笔记


    概述

    这篇文章是我对学习主席树的一个小结。

    当然,笔者很菜,除了模板就什么都不会打,也会有一些知识理解得不够深刻。

    因此,这篇文章以后还会不定期更新的。

    主席树

    主席树,全名称为可持久化线段树,也就是把每个阶段的线段树保存起来,每次可以直接访问一个阶段的状态,是神犇(HJT)发明出来的一种数据结构。

    先放出一个经典模板题:静态区间第(k)

    题目描述

    如题,给定(N)个整数构成的序列,将对于指定的闭区间查询其区间内的第K小值。

    输入输出格式

    输入格式

    第一行包含两个正整数(N)(M),分别表示序列的长度和查询的个数。

    第二行包含(N)个整数,表示这个序列各项的数字。

    接下来(M)行每行包含三个整数(l, r, k) , 表示查询区间([l, r])内的第(k)小值。

    输出格式

    输出包含(k)行,每行(1)个整数,依次表示每一次查询的结果

    输入输出样例

    输入样例#1

    5 5
    25957 6405 15770 26287 26465 
    2 2 1
    3 4 1
    4 5 1
    1 2 2
    4 4 1
    

    输出样例#1

    6405
    15770
    26287
    25957
    26287
    

    说明

    数据范围

    对于(20\%)的数据满足:(1 leq N, M leq 10)

    对于(50\%)的数据满足:(1 leq N, M leq 10^3)

    对于(80\%)的数据满足:(1 leq N, M leq 10^5)

    对于(100\%)的数据满足:(1 leq N, M leq 2cdot 10^5)

    对于数列中的所有数(a_i)​,均满足(-{10}^9 leq a_i leq {10}^9)

    样例数据说明

    (N=5),数列长度为(5),数列从第一项开始依次为([25957, 6405, 15770, 26287, 26465 ])

    第一次查询为([2, 2])区间内的第一小值,即为(6405)

    第二次查询为([3, 4])区间内的第一小值,即为(15770)

    第三次查询为([4, 5])区间内的第一小值,即为(26287)

    第四次查询为([1, 2])区间内的第二小值,即为(25957)

    第五次查询为([4, 4])区间内的第一小值,即为(26287)

    题解

    经典的主席树题。

    由于本题中的(a_i)数据范围很大,因此我们需要将数据进行离散化处理。

    然后就开始执行主席树的操作。

    先来看看插入操作的代码:

    void add(int &num, int &x, int l, int r)
    {
        T[cnt++] = T[x], x = cnt - 1, ++T[x].sum;
        if (l == r) return;
        int mid = (l + r) >> 1;
        if (num <= mid) add(num, T[x].L, l, mid);
        else add(num, T[x].R, mid + 1, r);
    }
    

    和线段树很相似,不是吗?(主席树不就是可持久化的线段树吗)

    接着看寻找答案的代码:

    int getans(int i, int j, int k, int l, int r)
    {
        if (l == r) return l;
        int t = T[T[j].L].sum - T[T[i].L].sum, mid = (r + l) >> 1;
        if (k <= t) return getans(T[i].L, T[j].L, k, l, mid);
        else return getans(T[i].R, T[j].R, k - t, mid + 1, r);
    }
    

    和线段树的思想一样,分开去找左右两子树。

    代码就这么一点儿,并不难记。

    不难得出最后的代码:

    #include <iostream>
    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <cmath>
    #include <algorithm>
    #include <cctype>
    #include <vector>
    #include <set>
    #include <map>
    #include <string>
    
    using namespace std;
    
    inline int gi()
    {
    	int f = 1, x = 0; char c = getchar();
    	while (c < '0' || c > '9') { if (c == '-')f = -1; c = getchar();}
    	while (c >= '0' && c <= '9') { x = x * 10 + c - '0'; c = getchar();}
    	return f * x;
    }
    
    struct Node
    {
        int L, R, sum;
    } T[4000003];
    struct lisan
    {
        int x, idx;
        bool operator < (const lisan &rhs) const
    	{
            return x < rhs.x;
        }
    } a[4000003];
    int r[2000003], t[2000003], n, m, cnt;
    
    void add(int &num, int &x, int l, int r)
    {
        T[cnt++] = T[x], x = cnt - 1, ++T[x].sum;
        if (l == r) return;
        int mid = (l + r) >> 1;
        if (num <= mid) add(num, T[x].L, l, mid);
        else add(num, T[x].R, mid + 1, r);
    }
    
    int getans(int i, int j, int k, int l, int r)
    {
        if (l == r) return l;
        int t = T[T[j].L].sum - T[T[i].L].sum, mid = (r + l) >> 1;
        if (k <= t) return getans(T[i].L, T[j].L, k, l, mid);
        else return getans(T[i].R, T[j].R, k - t, mid + 1, r);
    }
    
    int main()
    {
        n = gi(), m = gi();
    	for (int i = 1; i <= n; i++)
        {
            a[i].x = gi(), a[i].idx = i;
        }
        sort(a + 1, a + n + 1);//离散化
        for (int i = 1; i <= n; i++) r[a[i].idx] = i;
        cnt = 1;
        for (int i = 1; i <= n; i++)
    	{
            t[i] = t[i - 1], add(r[i], t[i], 1, n);//建树
        }
        while (m--)
    	{
            int x = gi(), y = gi(), z = gi();
            printf("%d
    ", a[getans(t[x - 1], t[y], z, 1, n)].x);//输出答案
        }
    	return 0;
    }
    

    可持久化数组

    可持久化数组属于主席树的一种变形,它是带修改的。

    即:可持久化数组是一种支持单点修改、单点查询的可持久化数据结构。

    我怎么感觉可持久化数组比主席树还简单啊

    还是来边看代码边理解思路:

    定义一个结构体,存储每个节点的信息:左儿子、右儿子、权值。

    struct Node
    {
    	int l, r, id;
    } q[20000005];
    

    新建一个节点:

    inline int newnode(int x)
    {
    	q[++tot] = q[x];
    	return tot;
    }
    

    来看建树的代码(其实就是新建节点的过程):

    int jianshu(int node, int beg, int en)
    {
    	node = ++tot;
    	if (beg == en)
    	{
    		q[node].id = a[beg];
    		return tot;
    	}
    	int mid = (beg + en) >> 1;
    	q[node].l = jianshu(q[node].l, beg, mid);//左儿子
    	q[node].r = jianshu(q[node].r, mid + 1, en);//右儿子
    	return node;
    }
    

    更新数值:

    int modify(int node, int beg, int en, int x, int y)
    {
    	node = newnode(node);//新建一个节点
    	if (beg == en)
    		q[node].id = y;//更新完了
    	else
    	{
    		int mid = (beg + en) >> 1;
    		if (x <= mid)
    			q[node].l = modify(q[node].l, beg, mid, x, y);//更新左子树
    		else
    			q[node].r = modify(q[node].r, mid + 1, en, x, y);//更新右子树
    	}
    	return node;
    }
    

    寻找答案:

    int getans(int node, int beg, int en, int x)
    {
    	if (beg == en)
    		return q[node].id;//已经找到答案了
    	else
    	{
    		int mid = (beg + en) >> 1;
    		if (x <= mid)
    			return getans(q[node].l, beg, mid, x);//向左寻找
    		else
    			return getans(q[node].r, mid + 1, en, x);//向右寻找
    	}
    }
    

    不难得出全部代码:

    #include <iostream>
    #include <cstdio>
    #include <cstdlib>
    #include <cstring>
    #include <algorithm>
    #include <cmath>
    #include <cctype>
    
    using namespace std;
    
    inline int gi()
    {
    	int f = 1, x = 0;
    	char c = getchar();
    	while (c < '0' || c > '9')
    	{
    		if (c == '-')
    			f = -1;
    		c = getchar();
    	}
    	while (c >= '0' && c <= '9')
    	{
    		x = x * 10 + c - '0';
    		c = getchar();
    	}
    	return f * x;
    }
    
    int n, m, ans, a[1000005], tot, root[1000005];
    struct Node
    {
    	int l, r, id;
    } q[20000005];
    
    inline int newnode(int x)
    {
    	q[++tot] = q[x];
    	return tot;
    }
    
    int jianshu(int node, int beg, int en)
    {
    	node = ++tot;
    	if (beg == en)
    	{
    		q[node].id = a[beg];
    		return tot;
    	}
    	int mid = (beg + en) >> 1;
    	q[node].l = jianshu(q[node].l, beg, mid);
    	q[node].r = jianshu(q[node].r, mid + 1, en);
    	return node;
    }
    
    int modify(int node, int beg, int en, int x, int y)
    {
    	node = newnode(node);
    	if (beg == en)
    		q[node].id = y;
    	else
    	{
    		int mid = (beg + en) >> 1;
    		if (x <= mid)
    			q[node].l = modify(q[node].l, beg, mid, x, y);
    		else
    			q[node].r = modify(q[node].r, mid + 1, en, x, y);
    	}
    	return node;
    }
    
    int getans(int node, int beg, int en, int x)
    {
    	if (beg == en)
    		return q[node].id;
    	else
    	{
    		int mid = (beg + en) >> 1;
    		if (x <= mid)
    			return getans(q[node].l, beg, mid, x);
    		else
    			return getans(q[node].r, mid + 1, en, x);
    	}
    }
    
    int main()
    {
    	n = gi(), m = gi();
    	for (int i = 1; i <= n; i++)
    		a[i] = gi();
    	root[0] = jianshu(0, 1, n);//建树
    	for (int i = 1; i <= m; i++)
    	{
    		int rf = gi(), fl = gi(), x = gi();
    		if (fl == 1)
    		{
    			int y = gi();
    			root[i] = modify(root[rf], 1, n, x, y);//保存状态+修改
    		}
    		else
    		{
    			printf("%d
    ", getans(root[rf], 1, n, x));//输出答案
    			root[i] = root[rf];//保存状态
    		}
    	}
    	return 0;
    }
    

    总结

    主席树和可持久化数组的代码很短,但是,理解思路才是最重要的,它们的思想也很重要。

    The End.

  • 相关阅读:
    sql server 阻塞与锁
    JVM监控
    Java性能分析之线程栈详解与性能分析
    MySQL监控调优
    JVM(上)
    性能测试流程介绍
    性能测试分析调优思路
    监控分析——Web中间件
    Linux性能测试工具安装全集
    Java基础(二十)集合(2)Collection接口
  • 原文地址:https://www.cnblogs.com/xsl19/p/11099885.html
Copyright © 2020-2023  润新知