前几天学线段树,这个经典的K-th number一直没有做,关键是听别人说复杂度是log(n)^3,我对这个需要两次二分+一次查找的算法非常的不爽,于是一直拖着没搞
今天正准备着手这题的时候,发现PKU的Disscuss有人提到log(n)的算法,而且编程复杂度比log(n)^3的还小,于是对这种算法充满了憧憬,那个log(n)^3的写到一半也放弃了(其实log(n)^3的归并树算法化简了之后就是求n个有序数列的第k大数)
YY了很久之后,得到下边这个代码..关键部分已经很明白的加了的注释
完全看明白之后会发现一个非常有趣的现象,划分树逆着做就变成了归并树
(其实我也不知道这是不是hyerty大神所说的划分树,乱YY的)
画了一颗划分树对数列[1 5 2 3 6 4 7 3 0 0]进行划分,下图有助于理解(红色表示该数被分到左儿子)

划分树

划分树


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#define M 100001
struct Seg_Tree{
    int left,right;
    int mid() {
        return (left + right) >> 1;
    }
}tt[M*4];
int len;
int sorted[M];
int toLeft[20][M];
int val[20][M];
 
void build(int l,int r,int d,int idx) {
    tt[idx].left = l;
    tt[idx].right = r;
    if(tt[idx].left == tt[idx].right)    return ;
    int mid = tt[idx].mid();
    int lsame = mid - l + 1;//lsame表示和val_mid相等且分到左边的
    for(int i = l ; i <= r ; i ++) {
        if(val[d][i] < sorted[mid]) {
            lsame --;//先假设左边的数(mid - l + 1)个都等于val_mid,然后把实际上小于val_mid的减去
        }
    }
    int lpos = l;
    int rpos = mid+1;
    int same = 0;
    for(int i = l ; i <= r ; i ++) {
        if(i == l) {
            toLeft[d][i] = 0;//toLeft[i]表示[ tt[idx].left , i ]区域里有多少个数分到左边
        } else {
            toLeft[d][i] = toLeft[d][i-1];
        }
        if(val[d][i] < sorted[mid]) {
            toLeft[d][i] ++;
            val[d+1][lpos++] = val[d][i];
        } else if(val[d][i] > sorted[mid]) {
            val[d+1][rpos++] = val[d][i];
        } else {
            if(same < lsame) {//有lsame的数是分到左边的
                same ++;
                toLeft[d][i] ++;
                val[d+1][lpos++] = val[d][i];
            } else {
                val[d+1][rpos++] = val[d][i];
            }
        }
    }
    build(l,mid,d+1,LL(idx));
    build(mid+1,r,d+1,RR(idx));
}
 
int query(int l,int r,int k,int d,int idx) {
    if(l == r) {
        return val[d][l];
    }
    int s;//s表示[ l , r ]有多少个分到左边
    int ss;//ss表示 [tt[idx].left , l-1 ]有多少个分到左边
    if(l == tt[idx].left) {
        s = toLeft[d][r];
        ss = 0;
    } else {
        s = toLeft[d][r] - toLeft[d][l-1];
        ss = toLeft[d][l-1];
    }
    if(s >= k) {//有多于k个分到左边,显然去左儿子区间找第k个
        int newl = tt[idx].left + ss;
        int newr = tt[idx].left + ss + s - 1;//计算出新的映射区间
        return query(newl,newr,k,d+1,LL(idx));
    } else {
        int mid = tt[idx].mid();
        int bb = l - tt[idx].left - ss;//bb表示 [tt[idx].left , l-1 ]有多少个分到右边
        int b = r - l + 1 - s;//b表示 [l , r]有多少个分到右边
        int newl = mid + bb + 1;
        int newr = mid + bb + b;
        return query(newl,newr,k-s,d+1,RR(idx));
    }
}
 
int main() {
    int T;
    scanf("%d",&T);
    while(T --) {
        int n , m;
        scanf("%d%d",&n,&m);
        FOR(i,1,n+1) {
            scanf("%d",&val[0][i]);
            sorted[i] = val[0][i];
        }
        sort(sorted + 1 , sorted + n + 1);
        build(1,n,0,1);
        while(m --) {
            int l,r,k;
            scanf("%d%d%d",&l,&r,&k);
            printf("%d\n",query(l,r,k,0,1));
        }
    }
    return 0;
}

后记:写完后去PKU交了一下,原以为不是rank1也至少是前十,结果连1s都没跑进去...
看了数据后发现m<=5000很少
意味着 nlogn(建树常数较大) + mlngn
和 nlogn(建树常数小)+mlogn^3
前者没占多少优势...
在我们hduoj也找了一道,所幸这题m <= 100000很大
两题比较:
OJ nlog(n) + mlog(n) nlog(n) + mlog(n)^3
HDU 少于500MS 3000MS左右
PKU 1000MS左右 1000-2000MS

2010.7.23跟新
扩展:
Minimum Sum
找到区间中的中位数,然后确定绝对值只和
就是找区间[l,r]的第(l-r+2)/2个数,而求和的话在Query函数里找到kth number后递归上来后再处理一下,需要另开一个数组sum[deep][i]表示第deep层,区间[ tt[idx].Left , i]的和

我比较懒都没有解释什么,只写了个代码
这篇文章解释的很清楚,可以去看看
我的代码只是为了理解方便点写的这么麻烦,其实划分树可以写的很简洁很简洁的,有好多地方可以优化~~