见微知著----POJ2182(线段树简介)

POJ2182 Lost Cows

Description
N (2 <= N <= 8,000) cows have unique brands in the range 1..N. In a spectacular display of poor judgment, they visited the neighborhood ‘watering hole’ and drank a few too many beers before dinner. When it was time to line up for their evening meal, they did not line up in the required ascending numerical order of their brands.

Regrettably, FJ does not have a way to sort them. Furthermore, he’s not very good at observing problems. Instead of writing down each cow’s brand, he determined a rather silly statistic: For each cow in line, he knows the number of cows that precede that cow in line that do, in fact, have smaller brands than that cow.

Given this data, tell FJ the exact ordering of the cows.
Input
Line 1: A single integer, N
Lines 2..N: These N-1 lines describe the number of cows that precede a given cow in line and have brands smaller than that cow.
Line 2 of the input describes the number of preceding cows whose brands are smaller than the cow in slot #2;
line 3 describes the number of preceding cows whose brands are smaller than the cow in slot #3; and so on.
Output
Lines 1..N: Each of the N lines of output tells the brand of a cow in line. Line #1 of the output tells the brand of the first cow in line; line 2 tells the brand of the second cow; and so on.
Sample Input
5
1
2
1
0
Sample Output
2
4
5
3
1

问题简介:
有N头牛的序号分别为1到N,将这N头牛乱序排列,我们已知这N头牛里每头牛前面排列的比他编号小的牛的个数,求从前往后数,每头牛的编号。
问题解析:
用比较容易想的办法,我们设定一个数组代表第m头牛是否被选中,若选中则其值为1,从而从后向前选出相应的编号。但这样的话程序的时间复杂度将达到O( n3 ),很容易就会超时。因此我们引入一种新的数据结构:线段树。
在Geeksforgeeks网站中,他们举了一个例子来帮助了解线段树:
假设我们有一个数组arr[0,1,..,n-1],我们希望能够完成如下的操作:
1、 将数组中l到r的部分元素之和求出来。
2、 更新一个或者多个元素。
如果采用普通的数组的话,查找其一个元素或者求和需要O(n)的操作时间。我们可以利用另外一个数组记录下从起始位置到第i个元素的总和,这样虽然求和只需要O(1)的时间,但是很不幸,更新数组的话仍然需要O(n)的时间。
因此我们引入线段树的结构来储存这一数组,这样无论是查找或是更新,其时间复杂度均为O(logn)。
这里写图片描述
线段树有如下性质:
1、用二叉树表示线段所在的区间。
2、每个结点包括数组所在区域和一个value值,这个value可以视作在这个区域内关于某个元素的一个函数。
3、根节点表示整个区间[1,N]。
4、每个叶子节点代表着一个独立的元素。
5、非叶子节点包括两个孩子节点,这两个孩子节点所包含的区间不相交,而且该非叶子节点的区域为两个孩子区域的和。
6、孩子节点的区域大约是其父亲节点的区域的一半。
7、非叶子节点储存的值不只是关于该区域的一个函数,而且还是其孩子存储的值的函数。

对于线段树而言,存在如下三种操作:
1、 建树(Construction):线段树的建立是为了将数组以更高效率的查询和更新。我们自上而下的、以递归的方式建立线段树,比如我们将根节点赋值,其包含区间为[1,N],并通过递归的方式将其两个子节点赋值,这时两个子区间为[ 1,N2 ],[ N+12,N ],同时两个自区间的值一定与父亲节点的值有一定的关联。

这里写图片描述
建树的代码如下:

void build(int v,int l,int r)
{
    tree[v].l = l;
    tree[v].r = r;
    if(l == r)
    {
        tree[v].value = a[r];
        return;
    }
    int mid = (l+r)/2;
    build(v*2,l,mid);
    build(v*2+1,mid+1,r);
    tree[v].value = tree[v*2].value + tree[v*2+1].value;
}

2、 更新(Update):更新一个线段树中一个或多个值,一般来说我们首先需要找到被更新的叶子节点,然后修改其父亲节点、祖父节点…但是不改变其他不会受到影响的节点。我们选择自上而下修改(因为value是和区间相关的函数值),若节点所包括的区间涵盖了所需更新的值,那么我们对这个区间进行更新,并且向其子节点进行进一步更新;如果节点所报获得区间没有涵盖所需更新的值,则停止对这一节点及其子节点的更新。

这里写图片描述
更新的代码可以通过LAZY方法优化,在下一篇博客中会作以介绍。

3、 查询(Query):如果查询的区间与节点的区间相同,则返回该节点的值;如果不相同的话,分别对其左右孩子节点进行查询,知道找到对应的区间,或者对应的区间的加和与查询区间相同。
这里写图片描述
查询的代码如下,若更新算法进行优化,那么查询算法也会发生些许改变。

void query(int v,int l,int r)
{
    if(tree[v].l == l && tree[v].r == r)
    {
        ans += tree[v].value;
        return;
    }
    int mid = (tree[v].l + tree[v].r)/2;
    if(r <= mid)
    {
        query(v*2,l,r);
    }
    else
    {
        if(l > mid)
            query(v*2+1,l,r);
        else
        {
            query(v*2,l,mid);
            query(v*2+1,mid+1,r);
        }
    }

}

在本题中,应用线段树可以将查询函数的时间复杂度由O(n2)减小为O(nlogn)(因为需要查询N个值),相对于数组形式的极为方便。同样的,和数组的方式相同,如果我们查询到对应的数字为a,则要找的数为第a+1个,若其左孩子区间的值大于等于a+1,则递归左孩子;否则递归右孩子。同时将该数字所出现过的区间中的值中逐一减一即可表示删除已经出现的数字。

代码

#include <iostream>
#include <cstdlib>
#include <cstdio>
using namespace std;
int small[10000], ans[10000];

struct segment
{
    int left;
    int right;
    int length;
};

segment s[30000];
void build(int root,int lc,int rc)   //构造一个线段树
{
    s[root].left = lc;
    s[root].right = rc;
    s[root].length = rc-lc+1;           //线段树的值是线段区间的长度
    if(lc == rc) return;
    build(root*2,lc,(lc+rc)/2);
    build(root*2+1,(lc+rc)/2+1,rc);
}

int query(int root,int k){
    s[root].length--;       //在递归的一开始就将节点所在的区间减一,即删除该数字k
    if(s[root].left == s[root].right)
        return s[root].left;
    else if(k <= s[root*2].length)
        return query(root*2,k);     //若左孩子的值大于等于数字k,则递归左孩子
    else
        return query(root*2+1,k-s[root*2].length);  //若左孩子的值小于数字k,则递归右孩子
}
int main()
{
    int n,i;
    scanf("%d",&n);
    for(i=2;i<=n;i++)
        scanf("%d",&small[i]);
    small[1] = 0;
    build(1,1,n);
    for(i=n;i>=1;i--)
        ans[i] = query(1,small[i]+1);   //从后往前倒着进行查找
    for(i=1;i<=n;i++)
        printf("%d\n",ans[i]);
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值