Battle of the Brains
第k元素log(n)算法–划分树
前几天学线段树,这个经典的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]的和
我比较懒都没有解释什么,只写了个代码
这篇文章解释的很清楚,可以去看看
我的代码只是为了理解方便点写的这么麻烦,其实划分树可以写的很简洁很简洁的,有好多地方可以优化~~
Popularity: 26% [?]
| 打印文章 | 这篇文章由shǎ崽于2009/11/27 23:01发表在Algorithm。你可以订阅RSS 2.0 也可以发表评论或引用到你的网站。 |
大约2周前
发觉倒着做其实是对数组下标做归并排序
[回复]
shǎ崽 回复:
八月 18th, 2010 at 12:30
恩,划分树逆着做就变成了归并树
[回复]
大约2周前
划分树逆着做是归并树?我看起来觉得像快排
[回复]
大约4周前
小HH牛,我将你的代码按我的理解进行了解释,不知同意否。
http://blog.sina.com.cn/s/blog_5f5353cc0100ki2e.html
如果不同意的话,我把文章删了,如果同意,麻烦看下是否理解错的地方。
[回复]
shǎ崽 回复:
八月 10th, 2010 at 11:44
感谢解释~我加个链接~
[回复]
风炎 回复:
八月 10th, 2010 at 23:10
嗯,多谢小HH牛
[回复]
大约1月前
YMhh大牛…
我在我的blog上贴了这文章的地址..不介意吧?不行的话我马上删掉..
[回复]
shǎ崽 回复:
八月 5th, 2010 at 17:59
当然没问题啦
[回复]
大约1月前
请问大牛,您提到的3xian的Turing tree是什么来的?我怎么找不到资料呢?3xian独有绝技?
[回复]
shǎ崽 回复:
八月 5th, 2010 at 18:00
不知道…..只闻其名,不见其身
[回复]
大约1月前
谢谢你的程序:)
我做了一下优化,排序的中间结果可以不用存,因为当查询区间为单位长,而结点没有到叶子时,可以用类似的方法继续向下直到叶子,这时就知道要找的数是原数组中第几大了。而且这道题没必要显建树。
但是时间上还是没进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ǎ崽 回复:
七月 31st, 2010 at 19:10
嗯嗯,我上边这个模板确实写的非常的冗余~可以化简很多~
[回复]
大约3月前
特来YM
[回复]
大约4月前
厉害,这个看的我有点晕
[回复]
大约4月前
拜读下
[回复]
大约4月前
特来膜拜小hh
[回复]
大约4月前
int b = r – l + 1 – s;
//b表示 [tt[idx].left , l-1 ]有多少个分到右边
这句话好像有错吧。
[回复]
大约5月前
想问大牛,如果要对中间的元素进行修改的时候,应该怎么做比较高效呢?
我只能想到每层都进行相应修改,但是这样效率太低了,不知大牛有什么好办法?
[回复]
shǎ崽 回复:
四月 1st, 2010 at 13:56
经典的几个kth number的算法都是离线处理的
暂时还想不出可以即使更新的算法
期待3xian的turing tree有这个功能
[回复]
大约5月前
求教图示中val中0元素为什么排在最后面,而sroted中却在最前面。同样的在最后的0元素在划分的时候却到左子树?
神牛能否讲一下具体思路,我的语言为pascal- -!看代码不带懂。
[回复]
TonyShaw 回复:
三月 18th, 2010 at 19:33
请先无视掉第一个问题……
[回复]
TonyShaw 回复:
三月 18th, 2010 at 19:44
第二个问题也无视掉好了^_^
我想问一下样例,比如现在建好树后(就像图片一样),现在要查询区间[3,6]的第2大数,也就是原序列中[3,6]: 2 3 6 4的第2大数,请问怎么操作?能模拟一下么?您的程序我编译不过- -!
[回复]
TonyShaw 回复:
三月 19th, 2010 at 07:56
Orz……我明白了。o(╯□╰)o
[回复]
shǎ崽 回复:
三月 19th, 2010 at 11:49
哎呀,没有及时回复~~sorry~
[回复]
大约6月前
我非常好奇的想知道 你的博客左边侧的三维的题目分类是拿什么软件做的&&这篇blog里的图片是那什么软件做的 O(∩_∩)O 谢谢啦~ 学习中……
[回复]
shǎ崽 回复:
三月 4th, 2010 at 14:53
那个三维的好像是一个插件Community Cloud
而这图片我是用word乱画画出来的~呵呵~
[回复]
大约6月前
大牛这么多。而我依然是一只小小菜鸟~~
[回复]
大约9月前
纯YM…
[回复]
大约9月前
特来膜拜小HH~
[回复]
shǎ崽 回复:
十一月 28th, 2009 at 18:58
回膜拜
[回复]
大约9月前
仰慕让大HH膜拜的小HH
[回复]
shǎ崽 回复:
十一月 28th, 2009 at 18:58
YM大侠
[回复]
大约9月前
特来膜拜小HH~
[回复]
shǎ崽 回复:
十一月 28th, 2009 at 11:42
我是小hh,HH是上边哪位大牛
[回复]
大约9月前
先沙发,再拜读~hh太牛了,加油~
[回复]
shǎ崽 回复:
十一月 27th, 2009 at 23:26
HH太牛了.
[回复]