嗯……主要为了帮助自己整理做题的思路,于是决定写写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;
}