第一篇Blog……

来源:互联网 发布:mm国家域名 编辑:程序博客网 时间:2024/06/10 11:57


嗯……主要为了帮助自己整理做题的思路,于是决定写写Blog尝试一下……

今天先总结一道去年数算实习机考的题--第K大数(POJ2104) 吧……


题目概要:

先给定一个n个数的序列,然后做m次询问,每次询问形式为(i, j, k),表示询问第a个数到第b个数之内第k小的数是几。

(1<= n <= 100000,1 <= m<= 5000, 序列中的数绝对值 <= 10^9)

(题目名说的是第k大……算了不要在意这些细节,反正题意肯定是求第k小)


该题(经室友指导)可以用可持久化线段树。这是个什么东西呢?基本上就是一棵线段树,但每次“修改”一个点的时候,并不修改原节点的值,而是把原节点复制一份,然后修改新的节点。比如说要在某个父节点下修改某个子节点,则先把父节点复制一份(因为父节点也修改了,只要有修改就复制),然后把对应子节点(不妨设为左节点)复制一份,改变其中的相关信息,然后让新父节点的左指针指向新的左子节点,右指针不变,与旧父节点相同,是指向旧的右子节点。

然后这个东西跟这个问题有啥关系呢?可以这样搞:初始的时候创建一棵线段树,每个节点维护一个size信息,表示这个节点代表的区间里面已经插入了多少个数(初始为0)。从前往后遍历整个序列,每次把这个数插入到这棵可持久化线段树当中,然后插入路径上的节点都创建了新的,并且比旧的size值+1。其中必然有新的根,把新根的id记录下来。(我们用顺序方法存储这棵树)

插入完成之后,得到一颗错综复杂的树……这怎么用来处理询问呢?比如询问是从i到j。我们可以注意到,从第j个根往下遍历所有子节点,得到各节点的size值是插入完第j个数之后的size值;对于第i-1个根,得到的是插入完第i-1个数之后的size值。于是把这两棵树对应位置(代表同一区间的)节点相减,得到的正是从插入第i个数到插入第j个数之间各节点得到的size值,也就是只有[i, j]区间的信息。这样我们只需要进行一次搜索,从根节点往下,每次把k与左节点的size值比较,如果小于则往左找,如果大于则把k减掉左节点size,并往右找。这样找到底就是我们要的区间第k小数了。


这样就可以O(logn)解决每个询问了……预处理时间是O(nlogn),于是就可以了

噢对了,还有一个麻烦事,就是虽然数只有n个,但大小可以很大,如果对-10^9到10^9维护一个线段树就炸飞天了,于是需要先离散化,具体来说就是先对序列排序(间接排序),求出序列名次 -> 位置的映射,然后反过来求一个位置 -> 名次的映射,用这个名次值去维护线段树,最后输出的时候,是用树里找出来的名次值先换回位置,在去原序列里找到原数值。

#include <iostream>#include <cstring>#include <cstdlib>#include <cstdio>#include <algorithm>using namespace std;struct Node{int size;int lc;int rc;} node[2000007]={};int root[100007]={};int nID = 0;int N,M;int arr[100007]={};int sa[100007]={};int rank[100007]={};void Insert(int o, int l, int r, int x)//预处理的插入操作 {if(l == r){return;}int m = (l+r)/2;if(x <= m){node[++nID] = node[node[o].lc];//新建左子节点 node[o].lc = nID;//更新当前节点的左子节点索引 node[nID].size++;//更新左子节点信息 Insert(nID, l, m, x);<span style="white-space:pre"></span>//向左子节点插入 }else{node[++nID] = node[node[o].rc];//新建右子节点... node[o].rc = nID;node[nID].size++;Insert(nID, m+1, r, x);}}int Query(int a, int b, int k)//查询 {int o1 = root[a-1], o2 = root[b];//o1, o2表示两棵树中的对应位置的节点, o1与o2的size作差即为待查区间的size信息 int l = 1;// 区间范围: [l, r] int r = N;while(l < r){int lSize = node[node[o2].lc].size - node[node[o1].lc].size;//计算左子节点size if(k <= lSize)//确定插入方向 {o1 = node[o1].lc;o2 = node[o2].lc;r = (l+r)/2;//更新区间范围 }else{k -= lSize;o1 = node[o1].rc;o2 = node[o2].rc;l = (l+r)/2 + 1;}}return l;}bool cmp(int i, int j)//位置之间比大小的函数 {return arr[i] < arr[j];}int main(){scanf("%d%d", &N, &M);/* 离散化 */ for(int i = 1; i <= N; i++){scanf("%d", &arr[i]);sa[i] = i;//待排序的数组, 准备得到名次 -> 位置的映射 }sort(sa+1, sa+N+1, cmp);//排序. 现在sa数组存放的位置1~N,按照在原序列中大小关系的顺序 for(int i = 1; i <= N; i++){rank[sa[i]] = i;//求反函数, 即位置 -> 名次的映射 }for(int i = 1; i <= N; i++)//依次插入序列中的值 {root[i] = ++nID;<span style="white-space:pre"></span>//保存新根ID node[root[i]] = node[root[i-1]];//维护新根节点信息... node[root[i]].size++;Insert(root[i], 1, N, rank[i]);//向新根插入需要插入的数 }for(int i = 0; i < M; i++){int a,b,k;scanf("%d%d%d", &a, &b, &k);int ans = Query(a, b, k);printf("%d\n", arr[sa[ans]]);//输出时,用名次值换回原值输出 }return 0;}

0 0