第k元素log(n)算法--划分树

十一月 27th, 2009

前几天学线段树,这个经典的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]的和

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

Tags: , , , ,

54 Responses to “第k元素log(n)算法--划分树”

  1. becauseofyou说道:

    函数式线段树太给力了,五六十行就搞定了- -

    [回复]

  2. Pira说道:

    刷完这页了,这棵树算告一段落。。。:)

    [回复]

  3. vapour说道:

    第69行
    int newr = tt[idx].left + ss + s - 1;//计算出新的映射区间
    -1 如果s==k,会不会把k-th number所在的位置正好减掉了?

    [回复]

  4. Proverbs说道:

    神牛,我写了一了看上去简洁一点的划分树,终于在POJ 2104跑了800+ms
    求优化~谢神牛代码和讲解给我的帮助!

    #include
    #include
    #include
    #include
    #define M 100005
    using namespace std;
    int sorted[M],toleft[30][M],tree[30][M],m,n;
    void read()
    {
    for(int i=1;i>1;
    int same=mid-l+1;//左子树中的数字个数
    for(int i=l;i<=r;i++)
    if(tree[d][i]<sorted[mid]) same--;//此时same成为和中位数相等的位于左子树的数字个数

    int ls=l,rs=mid+1;//分别为左右区间的起点
    for(int i=l;i<=r;i++)
    {
    int fg=0;
    if((tree[d][i]0))//将符合条件的数分到左子树
    {
    fg=1;
    tree[d+1][ls++]=tree[d][i];
    if(tree[d][i]==sorted[mid]) same--;//有些与中位数相同的数分到左子树,剩下的分到右子树
    }
    else tree[d+1][rs++]=tree[d][i];//分到右子树
    toleft[d][i]=toleft[d][i-1]+fg;//toleft[d][i]表示从l到i区间中有多少数分到了其左子树中
    }
    //递归建树
    create(l,mid,d+1);
    create(mid+1,r,d+1);
    }
    int query(int ql,int qr,int k,int l,int r,int d)
    {
    if(ql==qr) return tree[d][ql];
    int mid=(l+r)>>1;
    int x=toleft[d][ql-1]-toleft[d][l-1];//位于ql(待求区间左端点)(不包括ql)左边的放于左子树中的数字个数
    int y=toleft[d][qr]-toleft[d][l-1];//位于qr(待求区间右端点)(包括qr)左边的放于左子树中的数字个数
    int ry=qr-l+1-y;//位于qr(包含qr)的在右子树中的数字个数
    int cnt=y-x;//位于区间[ql,qr]中的在左子树中的数字个数
    int rx=ql-l-x;//位于ql左边(不包含ql)的在右子树中的数字个数
    if(cnt>=k) return query(l+x,l+y-1,k,l,mid,d+1);
    else return query(mid+rx+1,mid+ry,k-cnt,mid+1,r,d+1);
    }
    void go()
    {
    create(1,m,0);
    for(int i=1,a,b,k;i<=n;i++)
    {
    scanf("%d%d%d",&a,&b,&k);
    int ans=query(a,b,k,1,m,0);
    printf("%d\n",ans);
    }
    }
    int main()
    {
    while(scanf("%d%d",&m,&n)!=EOF)
    {
    read();
    go();
    }
    return 0;
    }

    [回复]

  5. 昵称(必须)说道:

    学长 你好! 这道你所说的线段树的做法我实在想不到,划分树我看了,我起先觉得可以用线段树套SBT做 后开发现时间复杂度会很高!!!所以想问下你那种二分二分的是怎么个思路?谢谢!

    [回复]

  6. 小媛说道:

    借下LZ的图哈~~
    小媛´s last [type] ..划分树学习(poj 2104,hdu 3473)

    [回复]

    shǎ崽 回复:

    哈哈,你写的比我详细多了~

    [回复]

  7. 云卷云舒说道:

    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的减去
    }
    }
    为什么for循环是从l到r,而不是l到mid呢。

    [回复]

    云卷云舒 回复:

    懂了。呵呵

    [回复]

  8. [...] 第二类数据:这个显然是跟区间操作有关的。借鉴刚才的思想,我们的操作就是查询区间最大k个数的和。对着这个操作,我们可以想到划分树这一数据结构。划分树的操作就不讲了,需要的参考NotOnlySuccess神牛的讲解。这题只需要多记录一个ToLeft的元素的Sum就可以了。其实就是个基本的划分树。时间复杂度O()。 [...]

  9. drcrow说道:

    请解释一下,如果所有的点都在中点右边的话可能会溢出,这个怎么处理?
    比如
    1 2 3 0 4 5 6 17
    维护时取0做中点,结果7个点都放在tree[0][4]之后了
    ——————
    我是沙茶……
    drcrow´s last [type] ..关于link-cut tree

    [回复]

  10. hanhan说道:

    好棒;
    膜拜一下

    [回复]

  11. zyue1105说道:

    发觉倒着做其实是对数组下标做归并排序

    [回复]

    shǎ崽 回复:

    恩,划分树逆着做就变成了归并树

    [回复]

  12. zyue1105说道:

    划分树逆着做是归并树?我看起来觉得像快排

    [回复]

  13. 风炎说道:

    小HH牛,我将你的代码按我的理解进行了解释,不知同意否。
    http://blog.sina.com.cn/s/blog_5f5353cc0100ki2e.html

    如果不同意的话,我把文章删了,如果同意,麻烦看下是否理解错的地方。

    [回复]

    shǎ崽 回复:

    感谢解释~我加个链接~

    [回复]

    风炎 回复:

    嗯,多谢小HH牛

    [回复]

  14. FHNstephen说道:

    YMhh大牛...
    我在我的blog上贴了这文章的地址..不介意吧?不行的话我马上删掉..

    [回复]

    shǎ崽 回复:

    当然没问题啦

    [回复]

  15. BBTiger说道:

    请问大牛,您提到的3xian的Turing tree是什么来的?我怎么找不到资料呢?3xian独有绝技?

    [回复]

    shǎ崽 回复:

    不知道.....只闻其名,不见其身

    [回复]

  16. reinyel说道:

    谢谢你的程序:)
    我做了一下优化,排序的中间结果可以不用存,因为当查询区间为单位长,而结点没有到叶子时,可以用类似的方法继续向下直到叶子,这时就知道要找的数是原数组中第几大了。而且这道题没必要显建树。
    但是时间上还是没进1S。。。应该还可以优化

    query的代码:
    int query(int l, int r, int k)
    {
    int d = 0;
    int low = 1, high = nCount;
    for (; low != high; d++) {
    int ss = l == low ? 0 : f[d][l - 1];
    int s = f[d][r] - ss;
    if (s >= k)
    high = low + high >> 1;
    else {
    k -= s;
    ss = l - low - ss;
    s = r - l + 1 - s;
    low = (low + high >> 1) + 1;
    }
    l = low + ss, r = l + s - 1;
    }
    return a[low];
    }

    最后还是感谢你的文章:)

    [回复]

    shǎ崽 回复:

    嗯嗯,我上边这个模板确实写的非常的冗余~可以化简很多~

    [回复]

  17. overpower说道:

    特来YM

    [回复]

  18. qq微博说道:

    厉害,这个看的我有点晕

    [回复]

  19. abilitytao说道:

    拜读下

    [回复]

  20. share4说道:

    特来膜拜小hh

    [回复]

  21. zt5840说道:

    int b = r - l + 1 - s;
    //b表示 [tt[idx].left , l-1 ]有多少个分到右边

    这句话好像有错吧。

    [回复]

  22. Sasuke说道:

    想问大牛,如果要对中间的元素进行修改的时候,应该怎么做比较高效呢?
    我只能想到每层都进行相应修改,但是这样效率太低了,不知大牛有什么好办法?

    [回复]

    shǎ崽 回复:

    经典的几个kth number的算法都是离线处理的
    暂时还想不出可以即使更新的算法
    期待3xian的turing tree有这个功能

    [回复]

  23. TonyShaw说道:

    求教图示中val中0元素为什么排在最后面,而sroted中却在最前面。同样的在最后的0元素在划分的时候却到左子树?
    神牛能否讲一下具体思路,我的语言为pascal- -!看代码不带懂。

    [回复]

    TonyShaw 回复:

    请先无视掉第一个问题……

    [回复]

    TonyShaw 回复:

    第二个问题也无视掉好了^_^
    我想问一下样例,比如现在建好树后(就像图片一样),现在要查询区间[3,6]的第2大数,也就是原序列中[3,6]: 2 3 6 4的第2大数,请问怎么操作?能模拟一下么?您的程序我编译不过- -!

    [回复]

    TonyShaw 回复:

    Orz……我明白了。o(╯□╰)o

    [回复]

    shǎ崽 回复:

    哎呀,没有及时回复~~sorry~

    [回复]

  24. Cinderella说道:

    我非常好奇的想知道 你的博客左边侧的三维的题目分类是拿什么软件做的&&这篇blog里的图片是那什么软件做的 O(∩_∩)O 谢谢啦~ 学习中……

    [回复]

    shǎ崽 回复:

    那个三维的好像是一个插件Community Cloud
    而这图片我是用word乱画画出来的~呵呵~

    [回复]

  25. 夜雨说道:

    大牛这么多。而我依然是一只小小菜鸟~~

    [回复]

  26. [...] 因为我觉得这个题的这个思路挺赞的,于是把这个题拿到ACM_DIY群上讨论,结果发现,利用之间群里讨论过,hh写过的O(logn)求区间第K大数的方法,可以用更低复杂度解决此题。因为如果某个元素在区间中出现次数超过一半,那么这个数一定是区间第S/2大的,其中S为区间长度,因此可以直接把候选名额锁定为1个,然后再使用之前的方法来判断。 [...]

  27. 匿名说道:

    纯YM...

    [回复]

  28. wzc1989说道:

    特来膜拜小HH~

    [回复]

    shǎ崽 回复:

    回膜拜

    [回复]

  29. daxia说道:

    仰慕让大HH膜拜的小HH

    [回复]

    shǎ崽 回复:

    YM大侠

    [回复]

    daizhenyang 回复:

    daxia神牛啊

    [回复]

  30. LonelyBoy说道:

    特来膜拜小HH~

    [回复]

    shǎ崽 回复:

    我是小hh,HH是上边哪位大牛

    [回复]

  31. 杭之翼说道:

    先沙发,再拜读~hh太牛了,加油~

    [回复]

    shǎ崽 回复:

    HH太牛了.

    [回复]

Leave a Reply

CommentLuv badge
注意: 评论者允许使用'@user空格'的方式将自己的评论通知另外评论者。例如, ABC是本文的评论者之一,则使用'@ABC '(不包括单引号)将会自动将您的评论发送给ABC。使用'@all ',将会将评论发送给之前所有其它评论者。请务必注意user必须和评论者名相匹配(大小写一致)。