Kth number 主席树(可持久化线段树)
Kth number
HDU - 2665InputThe first line is the number of the test cases.
For each test case, the first line contain two integer n and m (n, m <= 100000), indicates the number of integers in the sequence and the number of the quaere.
The second line contains n integers, describe the sequence.
Each of following m lines contains three integers s, t, k.
s,ts,t indicates the interval and k indicates the kth big number in interval s,ts,tOutputFor each test case, output m lines. Each line contains the kth big number.Sample Input
1 10 1 1 4 2 3 5 6 7 8 9 0 1 3 2
Sample Output
2
题意很简单,就是给你n个数,再给你m组询问,问区间[l,r]中第k小的数字是多少。
我们可以先考虑简单情况,假设l和r固定是1和n,要怎么做。不考虑离线版本,如果强制在线的话我们可以建一棵权值线段树,就是维护数字数量的线段树,比如说,有五个数字1,1,2,2,2,那么Tree[1]=2,Tree[2]=3,这样假设我要查询第k小的数,可以选择二分区域,找到sum[T[1~pos]]>=k,那pos就是第k小的数字了。这种方法复杂度是logn*logn,我们可以再优化一下。可以从线段树的根节点开始找,每次向下查找,如果左节点的数字总数大于等于k,说明第k小的数字一定在左节点,我们就往左节点走,否则往右节点走,同时k要减去左节点数字总数,因为线段树维护的不是前缀和,右节点数字总数不包括左节点数字总数。这样不断递归下去,当l==r的时候返回l就行了。这种查询方式复杂度是logn。注意,当数字太大时要进行离散化再建树,否则内存会爆炸。
接下来考虑进化版,l和r是任意数字的时候,我们可以考虑建n棵线段树,SegTree[i]代表插入第i个数字的时候对应的线段树,假设我能得到这n棵线段树,那我查询[l,r]这区间的第k小数字可以利用前缀和用第r棵线段树减去第l-1棵线段树,此时可以得到[l,r]这部分的线段树,然后我们再查询第k小的时候可以用上面讲过的方法查询。关键在于,如果真的建了n棵线段树,内存会爆炸。其实仔细思考我们会发现,当我们添加了一个点后,会影响到线段树的节点只有logn个,我们只需要新建这logn个节点就好,这样空间复杂度就大大降低了。我们用root[i]代表SegTree[i]的根节点,在更新的时候只更新被影响到的logn个节点就好。具体实现看代码,有注释。
#include <iostream> #include <cstdio> #include <cstring> #include <cmath> #include <algorithm> #include <vector> #include <queue> #include <string> #include <stack> #include <map> #include <set> #include <bitset> #define X first #define Y second #define clr(u,v); memset(u,v,sizeof(u)); #define in() freopen("data","r",stdin); #define out() freopen("ans","w",stdout); #define Clear(Q); while (!Q.empty()) Q.pop(); #define pb push_back using namespace std; typedef long long ll; typedef pair<int, int> pii; const int maxn = 1e5 + 10; const int INF = 0x3f3f3f3f; struct Tree { int l, r, sum; //l,r指向左右子树,sum该节点对应的数字的总数量 } T[maxn<<5]; //这是一颗权值线段树,就是根据数字建树,维护数字的个数 int cnt;//线段树总节点数 int N[maxn]; int root[maxn];//root[i]代表第i棵线段树所在位置 vector <int> V; void init() { cnt = 0; V.clear(); T[0].sum = 0; } int id(int x)//用于离散 { return lower_bound(V.begin(), V.end(), x) - V.begin() + 1; } void update(int l, int r, int &x, int y, int pos) { T[++cnt] = T[y]; //新建节点,并继承他的上一个版本 T[cnt].sum++;//新加了一个数字,sum++ x = cnt;//将这节点和他的父节点连起来 if (l == r) return ; int mid = (l + r) >> 1; if (mid >= pos) update(l, mid, T[x].l, T[y].l, pos);//折半查找找到pos的位置进行更新,线段树的基本操作 else update(mid + 1, r, T[x].r, T[y].r, pos); } int query(int l, int r, int x, int y, int k) //查询第k大 { if (l == r) return l; int mid = (l + r) >> 1; int sum = T[T[y].l].sum - T[T[x].l].sum;//利用前缀和求出[x,y]这部分的线段树 //T[T[y].l].sum - T[T[x].l].sum可以得出[x,y]这线段树左子树的sum if (sum >= k) return query(l, mid, T[x].l, T[y].l, k);//如果sum>=k说明要找的值在左子树 else return query(mid + 1, r, T[x].r, T[y].r, k - sum);//否则到右子树去找 } int main() { int T; scanf("%d", &T); while (T--) { init(); int n, m, l, r, k; scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &N[i]); V.pb(N[i]); } sort(V.begin(), V.end()); V.erase(unique(V.begin(), V.end()), V.end()); for (int i = 1; i <= n; i++) update(1, n, root[i], root[i-1], id(N[i]));//根据1~n建立n棵线段树,root[i]代表前i棵线段树的根节点,root[i-1]则是上一个版本的线段树 while (m--) { scanf("%d%d%d", &l, &r, &k); printf("%d\n", V[query(1, n, root[l-1], root[r], k) - 1]); //查询[l,r]这棵线段树的第k小数 } } return 0; }