HDU 2665 个人理解(主席树)

来源:互联网 发布:举牌软件下载 编辑:程序博客网 时间:2024/05/16 05:39

首先感谢 http://blog.csdn.net/zearot/article/details/44981711

先上原题链接 http://acm.hdu.edu.cn/showproblem.php?pid=2665

题意大概是给你一列数,给你若干个闭区间[i,j],问这个区间内第k(题意有误,第k大无法通过,第k小A过去了)的数是多少。

再上代码(一会解释)

#include <iostream>#include <cstdio>#include <algorithm>typedef long long ll;const constexpr int maxN = 1e5 + 5;const constexpr int MAXN = 40*maxN;int left[MAXN];int right[MAXN];int sum[MAXN];int Tree[MAXN];int cnt;void clear(){left[0] = 0;right[0] = 0;sum[0] = 0;cnt = 0;}void update(int& now,int begin,int end,int add)// Update the Fotile Tree (Insert new nodes into it){++cnt;// copy the origin nodeleft[cnt] = left[now];right[cnt] = right[now];sum[cnt] = sum[now]+1;now = cnt;// add new data to itif (begin == end) return;int mid = (begin+end)/2;if (add <= mid) update(left[now],begin,mid,add);else            update(right[now],mid+1,end,add);}int query(int i,int j,int begin,int end,int k){if (begin == end) return begin;int t = sum[left[j]] - sum[left[i]];int mid = (begin+end)/2;if (t >= k) return query(left[i],left[j],begin,mid,k);else return query(right[i],right[j],mid+1,end,k-t);}struct num_pos{int x,pos;num_pos(int _x=-1,int _pos=-1):x(_x),pos(_pos) {}bool operator<(const num_pos& n) const{return (x < n.x);}};num_pos pos[maxN];int T,n,m,nums[maxN],t_l,t_r,t_k;int N_rank[maxN];int main(){std::scanf("%d",&T);while (T--){std::scanf("%d %d",&n,&m);for (int i=1;i<=n;++i){std::scanf("%d",nums+i);pos[i] = {nums[i],i};}std::sort(pos+1,pos+n+1);for (int i=1;i<=n;++i) N_rank[pos[i].pos] = i;clear();// insert into empty treefor (int i=1;i<=n;++i){Tree[i] = Tree[i-1];update(Tree[i],1,n,N_rank[i]);  }  for (int i=0;i<m;++i)  {  std::scanf("%d %d %d",&t_l,&t_r,&t_k);  std::printf("%d\n",pos[query(Tree[t_l-1],Tree[t_r],1,n,t_k)].x);}}}

好,现在让我们正式开始。

首先是两个二傻子的对话:

二傻子A:你看这题还不简单,每次QSort一遍取第k个数不就结了。

二傻子B:是呀是呀,。。。

(Time Limit Exceed)


二傻子A:卧槽有n=1e5个数和m=1e5组查询。。这O(nmlogn)的复杂度。。

二傻子B:额,那就线段树吧,查询每次都logn岂不是美滋滋。

二傻子A:所言极是。

(Memory Limit Exceed)


二傻子A:卧槽你开了几颗线段树?

二傻子B:emmmmm,不多,也就十万颗左右吧。

二傻子A:。。。。。。。

然后二傻子A经过一晚上的思考

二傻子A:嘿B,你每次新建线段树时需要更新几个节点?

二傻子B:emmmmmm,大概logn个吧。

二傻子A:你能不能把这些线段树合并一下,比如说那些相同的节点就不要新建了?


二傻子B:emmmmmm。。。。

于是二傻子B开始合并线段树。鉴于二傻子B的码力实在是欠佳,他花了大概三小时才写出如上的代码。

(Accept!)


于是俩二傻子特别高兴,他们决定让二傻子C来写这篇blog。

(以上对话发生在二傻子C的脑海中,二傻子AB现实中并不存在,真实存在的是二傻子C,所有动作也由二傻子C来完成)

好!废话就到这里,现在让二傻子C来为各位分析一下代码。

#include <iostream>#include <cstdio>#include <algorithm>typedef long long ll;const constexpr int maxN = 1e5 + 5;const constexpr int MAXN = 40*maxN;int left[MAXN];int right[MAXN];int sum[MAXN];int Tree[MAXN];int cnt;

先看这几行。

前6行不解释,自己想。

left---某个节点的左儿子

right---某个节点的右儿子

sum---某个节点的大小(初始时空树大小为0),即该节点内出现了多少个数。

Tree---第i颗树的根(为啥不叫root?因为二傻子C实在是太傻了,傻到写这篇博客时才明白这个是树根)

cnt---目前有多少个节点

恩,相信明白人都看懂了,这实际上是一棵可持久化线段树(主席树),不过二傻子C并不是明白人,他决定继续往下写。

好,我们知道线段树每次单点修改需要自上而下修改logn个节点,n个节点中的绝大部分并没有修改。二傻子C考虑这样一个过程:

1.建立一颗空树。

2.按一定顺序向空树中添加节点,添加时按照普通线段树单点修改的方式进行,但是不修改原有节点,而是建立新节点。每个节点存储区间内的值出现了多少次(sum数组)。

3.查询时,查询某两次添加的过程中发生了什么。

现在,问题变成了应该按什么顺序添加节点,又该查询哪两次添加的过程中发生了什么。

考虑到我们需要查询区间第k小,我们可以先离散化所有的点(连二傻子C都知道该怎么离散化,不知道的同学请自行百度),然后按照原顺序从左向右依次插入每个节点的rank(相同的节点值需要有不同的rank,比如序列3 3 1 2 1的rank可以是4 5 2 3 1,也可以是5 4 1 3 2,等等)。考虑前i-1次插入和前j次插入,如果将二者的结果相减,我们正好可以得到[i,j]区间内的值的rank。


换言之,考虑第j次插入后,全区间的左半部分(左儿子)与第i-1次插入后,全区间的左半部分(左儿子)之差(设其为t,则t表示[i,j]内有多少个数落在总区间的左半部分),若t>=k,则区间第k小一定出现在左子区间内(线段树左子区间维护的值一定比右子区间要小),我们只需查询左子区间内第k小数即可。若t<k,同理可知区间第k小一定出现在右子区间内,而左子区间已经出现了t个数,我们只需查询右子区间的第(k-t)小数即可。

明白了原理,现在看查询的代码,是不是感觉很简单呢?(main内以query(Tree[t_l-1],Tree[t_r],1,n,t_k)的格式调用query)

int query(int i,int j,int begin,int end,int k){if (begin == end) return begin;int t = sum[left[j]] - sum[left[i]];int mid = (begin+end)/2;if (t >= k) return query(left[i],left[j],begin,mid,k);else return query(right[i],right[j],mid+1,end,k-t);}
相比查询,插入(update)就显得比较复杂了,我们一行行来分析。

首先注意到,main内调用update时有一句Tree[i] = Tree[i-1]; 这是干啥用的?


开始的开始,我们都是孩子(啊呸)开始的时候我们需要一颗空树(总不能啥都没有凭空建树吧)so,我们定义空树的左右孩子都是0,sum(节点个数)也是0,cnt(总节点数)自然也是0。既然有这么多0,干脆就把他放在0的位置上吧,于是就有了clear()函数。

void clear(){left[0] = 0;right[0] = 0;sum[0] = 0;cnt = 0;}

建新树的时候,每棵树的根节点都需要从上一棵树继承,但是这一条并没有在update里体现(递归建树时不需要继承根节点),所以我们需要这么一句Tree[i] = Tree[i-1]; ,表示第i棵树的根节点继承自第i-1棵树(所以树的编号是从1到n(0是空树),而非从0到n-1)。既然要继承,自然左、右儿子都要继承(更新一会再说),同时由于新节点要加一个数进来,所以sum(区间内有了多少个值)要+1 ;前面说到这些操作都不修改原有节点,而是新建一个节点,我们自然要把新建的节点连接到这棵树上(当now为根节点时,now=cnt表明新建的点为新的根节点;当now为左/右子节点时,now=cnt表明新建的点是新的左/右子节点),接下来就是喜闻乐见的线段树更新,不再赘述(二傻子C前几天刚学会怎么更新线段树)。

讲到这里其实就差不多了,剩下的东西都很简单了。num_pos和N_rank用来离散化,每次把树推倒重建时记得clear,查询得到的是离散化后的下标,要从下标得到真正的值。。。恩都是一些细节问题,不再赘述。(查询区间第k大只需把num_pos的operator<更改一下即可return (x < n.x); -> return (x > n.x),有兴趣的同学可以自己尝试。

可喜可贺,写完收工!


最后表达一下怨念,与本文无关。(七夕什么的都去**吧)

原创粉丝点击