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

    (;)
    前置知识:线段树。
    相关知识:可持久化Trie,具体可以看我的博客。其中对可持久化的概念有具体说明。
    这里先介绍一个东东————权值线段树
    (;)

    权值线段树

    (;)
    权值线段树:顾名思义,是以数值为下标建立的一颗线段树,大概类似于,如图:

    (;)
    在这个例子中,我们插入了(1,2,2,2,3,3,5,6,6)这些数,因此可以发现,权值线段树中一个节点的数值即为:在这个节点的值域中数的数量。
    (;)

    主席树

    (;)
    根据可持久化的思想:每次只记录发生变化的部分。而以区间和为例,每次修改至多会使(log(n))个区间所对应的节点发生变化。
    因此对于每个发生变化的节点(p),创建一个新版本(p'),而只要(p)不是叶子节点,它的左右儿子之一必定发生了更新。不妨设变化的是左儿子,那么我们只需把右儿子的信息拷贝过来,再向左子树(lson)递归,返回时令(p')的左儿子为更新后的(lson')。看下面的例子:
    (;)

    (;)
    我们把(4)这个位置修改了,会使([4,4],[4,5],[4,6],[1,6])(4)个节点所对应的区间发生变化,可以对照参考一下。
    由于主席树不再是一颗完全二叉树,所以我们不能用(2x,2x+1)这样的层次序编号,而是改为记录左右儿子的编号,其实与Trie中的字符指针类似。所以在插入查询操作时,我们要把(l,r)(这个区间对应的左右端点),当作函数的参数传递
    因为它每次修改只会新增(log(n))个节点,所以主席树的空间复杂度为:(O(n+m;log;n)),其中(m)为版本个数
    而对于第(i)个版本的线段树,从它的根(root[i])向下遍历,即可得到这个版本所对应的信息。
    下面看看主席树可以解决什么问题。
    (;)

    Problem 1 ————静态区间第k小

    题意

    (;)
    给定一个长度为(n)的序列,共有(m)次询问,每次询问给定(l,r,k),求([l,r])这段区间的第(k)小数。
    (n,mleq 10^5,a[i]leq 10^9)
    题目地址: https://www.luogu.com.cn/problem/P3834
    暴力显而易见的超时。相信大家也都会写,这里不多赘述。
    (;)

    整体二分

    (;)
    首先,我们不考虑(l,r)的限制,在([1,n])的区间如何找第(k)小数?
    容易想到我们可以建立一颗权值线段树。而我们在查询的过程中,只需判断([L,mid])这个值域中数的数量(cnt)(k)的大小关系:
    (cntgeq k),显然第(k)小数在([L,mid])这段值域中。
    否则说明是在([mid+1,R])这段值域中。
    而这里要注意的是,由于(a[i])很大,所以我们先将其离散化,再进行操作。
    (;)

    前缀和

    (;)
    但现在有(l,r)的限制,如何做?
    于是我们可以建立一颗主席树,在输入这个序列的时候不断新建版本,可以发现第(i)个版本的线段树维护的是([1,i])这段区间的信息。
    另外,还有一个重要的性质:主席树的每个版本的对值域的划分是相同的
    换句话来说:除了节点信息(cnt)发生变化,两颗线段树的内部结构,与和每个节点所代表的值域区间完全对应
    所以我们可以发现:第(r)版本的值域区间([L,R])(cnt)减去第(l-1)版本的值域区间([L,R])(cnt)。即为:(a[l-r])中有多少个数是落在值域([L,R])中的。那么我们只需从(root[l-1])(root[r])同时出发进行访问,利用这种前缀和的性质,即可算出答案。
    (;)

    code

    #include <cstdio>
    #include <vector>
    #include <cstring>
    #include <iostream>
    #include <algorithm>
    
    using namespace std;
    const int N = 200010;
    
    int n, m, idx, a[N], root[N];
    vector <int> G;
    
    struct Segtree
    {
    	int l, r, cnt;
    }tree[N * 25]; //注意:每次新建一个版本都要新增log(n)个节点,因此空间要开够 
    
    int Getit(int x)
    {
    	return lower_bound(G.begin(), G.end(), x) - G.begin();
    	//找到x这个数在序列中的排名 
    }
    
    int Insert(int Last,int l,int r,int x)
    {
    	int Now = ++ idx; // 新建一个节点 
    	tree[Now] = tree[Last]; //先把原先的拷贝过来 
    	if(l == r) // 若为叶子节点 
    	{
    		tree[Now].cnt ++; //这个位置上数的数量 + 1 
    		return Now; //返回这个点的编号 
    	}
    	int mid = (l + r) >> 1;
    	if(x <= mid)
    	{
    		tree[Now].l = Insert(tree[Last].l, l, mid, x);
    		// 若修改的在左子树,则往左子树递归插入新节点 
    	}
    	else
    	{
    		tree[Now].r = Insert(tree[Last].r, mid + 1, r, x);
    		//否则就在右子树 
    	}
    	tree[Now].cnt = tree[tree[Now].l].cnt + tree[tree[Now].r].cnt;
    	//push up 
    	return Now;
    }
    
    int Query(int Last,int Now,int l,int r,int k)
    {
    	if(l == r)
    	{
    		return l; //若为叶子节点,则第k小的数显然就是这个 
    	}
    	int mid = (l + r) >> 1;
    	int ch = tree[tree[Now].l].cnt - tree[tree[Last].l].cnt;
    	//[l,r]这段区间在左子树值域的数量 
    	if(k <= ch) // 若左子树的数量已至少有k个了,往左子树递归 
    		return Query(tree[Last].l, tree[Now].l, l, mid, k);
    	else //否则就往右子树递归,注意在右子树中的排名为 k - ch (左子树已有ch个了) 
    		return Query(tree[Last].r, tree[Now].r, mid + 1, r, k - ch);
    }
    int main()
    {
    	cin >> n >> m;
    	for(int i = 1; i <= n; i++)
    	{
    		scanf("%d",&a[i]);
    		G.push_back(a[i]);
    	}
    	
    	sort(G.begin(),G.end());
    	G.erase(unique(G.begin(),G.end()),G.end()); // 离散化 
    	
    	for(int i = 1; i <= n; i++)
    	{
    		root[i] = Insert(root[i - 1], 0, G.size() - 1, Getit(a[i]));
    		//每次输入插入一个新的版本 
    	}
    	 
    	while(m--)
    	{
    		int l, r, k;
    		scanf("%d%d%d",&l,&r,&k);
    		printf("%d
    ",G[Query(root[l - 1], root[r], 0, G.size() - 1, k)]);
    		//对于每次询问输出这个排名为k的数值(在G里存储的) 
    	}
    	return 0; 
    }
    

    (;)

    Problem 2 ————动态区间第k小

    题意

    (;)
    给定一个含有(n)个数的序列(a_1,a_2,cdots,a_n),需要支持两种操作
    1.(;Q;l;r;k):表示查询下标在区间([l,r])中第(k)小的数
    2.(;C;x;y):表示将(a_x)改为(y)
    (n,mleq 10^5,a_ileq 10^9)
    题目地址: https://www.luogu.com.cn/problem/P2617
    (;)
    与上道题相比只多了修改操作(废话)
    (;)

    树状数组维护

    (;)
    我们考虑把(a_x)改为(y)的影响,设(a_x)原先等于(v),显然,第(x)(n)个版本的线段树下标为(y)的值要(+1),为(v)的要(-1)
    如果只是单纯的(for(x;to;n)),然后对这些线段树进行修改的话,很明显:每次修改的复杂度是(O(n;log;n)),而(m)次操作,时间复杂度为(O(nm imes log;n)),显然不行。
    所以我们考虑如何优化这个东西。
    与上题相同我们想要知道的是(sum[r]-sum[l-1])这样的区间和
    如果仔细观察你就可以发现,这个东西可以用树状数组的思路来做。
    在这里:以(root[x])为根的线段树存储的不再是([1,x])的信息了,而是([x-lowbit(x)+1,x])中的信息。
    而根据树状数组的套路,每次修改会对(log(n))个点产生影响,查询也是同理。
    通俗点说:就是把每颗线段树当做一个点,这样就形成了一个长度为(n)序列,然后再用树状数组维护这个序列的修改查询操作。(外层是树状数组,内层是线段树)
    而每次至多修改(log(n))个点(线段树),每个线段树修改的复杂度为(log(n))
    由此可得,时间复杂度:(O(m imes log_2 n))
    而空间复杂度?由于每次修改都至多新建(log_2 n)个点,所以为(O((n+m) imes log_2 n))
    剩下的东西就和Problem 1完全一样了。
    (;)

    code

    #include <bits/stdc++.h>
    using namespace std;
    const int N = 100010;
    int n, q, a[N], len, ask_x[N], ask_y[N], ask_k[N], tmp1[100], tmp2[100]; 
    int root[N << 1], idx, cnt1, cnt2, g[N << 1];
    struct Node
    {
    	int l, r, cnt;
    }tree[N * 400]; //注意要开到O((n+m) log2 n)的大小
    int lowbit(int x)
    {
    	return x&-x;
    }
    int Getit(int x)
    {
    	return lower_bound(g + 1, g + len + 1, x) - g;
    }
    int Update(int Last,int l,int r,int x,int v)
    {
    	//函数的意义:对线段树中下标为x的值加上v 
    	int Now = ++idx; tree[Now] = tree[Last];
    	if(l == r)
    	{
    		tree[Now].cnt += v;
    		return Now;
    	}
    	int mid = (l + r) >> 1;
    	if(x <= mid)
    	{
    		tree[Now].l = Update(tree[Now].l, l, mid, x, v);
    	}
    	else tree[Now].r = Update(tree[Now].r, mid + 1, r, x, v);
    	tree[Now].cnt = tree[tree[Now].l].cnt + tree[tree[Now].r].cnt;
    	return Now;
    }
    inline void Add(int x,int val,int v)
    {
    	for(;x <= n; x += lowbit(x))
    	{
    		root[x] = Update(root[x], 1, len, val, v);
    		//修改时,所有被它影响的线段树中下标为x的值都要加上v 
    	}
    }
    int Query(int l,int r,int k)
    {
    	if(l == r)
    	{
    		return l;
    	}
    	int mid = (l + r) >> 1, sum = 0;
    	for(register int i=1;i<=cnt1;i++)
    	{
    		sum -= tree[tree[tmp1[i]].l].cnt;
    	}
    	for(register int i=1;i<=cnt2;i++)
    	{
    		sum += tree[tree[tmp2[i]].l].cnt;
    	}
    	//sum就是维护一个前缀和,只不过使用树状数组算的 
    	if(sum >= k)
    	{
    		for(register int i=1;i<=cnt1;i++)
    		{
    			tmp1[i] = tree[tmp1[i]].l;
    		}
    
    		for(register int i=1;i<=cnt2;i++)
    		{
    			tmp2[i] = tree[tmp2[i]].l;
    		}
    		return Query(l, mid, k);
    	}
    	else 
    	{
                    //跟着向下遍历
    		for(register int i=1;i<=cnt1;i++)
    		{
    			tmp1[i] = tree[tmp1[i]].r;
    		}
    		for(register int i=1;i<=cnt2;i++)
    		{
    			tmp2[i] = tree[tmp2[i]].r;
    		}
    		return Query(mid + 1, r, k - sum);
    	}
    } 
    int main()
    {
    	scanf("%d%d",&n,&q);
    	for(register int i=1;i<=n;i++)
    	{
    		scanf("%d",&a[i]);
    		g[ ++ len] = a[i];
    	} 
    	char op[2];
    	for(register int i=1;i<=q;i++)
    	{
    		scanf("%s%d%d",op,&ask_x[i],&ask_y[i]);
    		if(op[0] == 'C')
    		{
    			ask_k[i] = 0;
    			g[ ++ len] = ask_y[i];
    		}
    		else 
    		{
    			scanf("%d",&ask_k[i]);
    		}
    	}
    	sort(g + 1, g + len + 1);
    	len = unique(g + 1, g + len + 1) - g - 1;
    	for(register int i=1;i<=n;i++)
    	{
    		Add(i, Getit(a[i]), 1); //在i这个版本的线段树上a[i]离散化后的上的值加上1
    	}
    	for(register int i=1;i<=q;i++)
    	{
    		if(ask_k[i] == 0)
    		{
    			Add(ask_x[i], Getit(a[ask_x[i]]), -1);
    			a[ask_x[i]] = ask_y[i];
    			Add(ask_x[i], Getit(a[ask_x[i]]), 1);
    		}
    		else
    		{
    			cnt1 = 0, cnt2 = 0;
    			int L = ask_x[i] - 1, R = ask_y[i];
    			for(;L; L ^= lowbit(L)) //这里预处理出我们要算的是哪些版本的线段树
    			{
    				tmp1[ ++ cnt1 ] = root[L];
    			} 
    			for(;R; R ^= lowbit(R))
    			{
    				tmp2[ ++ cnt2 ] = root[R];
    			}
    			printf("%d
    ",g[Query(1, len, ask_k[i])]); //输出排名为k的值
    		}
    	}
    	return 0;
    }
  • 相关阅读:
    ZOJ 2588 Burning Bridges
    POJ 1966 ZOJ 2182 Cable TV Network
    HDU 5348 MZL's endless loop
    HDU 5352 MZL's City
    Tarjan算法求解无向连通图的割点、割边、点双连通分量和边双连通分量的模板
    ZOJ 1119 SPF
    HDU 3452 Bonsai
    HDU 1520 Anniversary party
    POJ 2239 Selecting Courses
    POJ 1144 Network
  • 原文地址:https://www.cnblogs.com/czyty114/p/12870947.html
Copyright © 2011-2022 走看看