树状数组 通俗易懂地从头讲\(^_^)/

怎么理解树状数组?先从最最最最基本简单的开始讲起:

        首先说明树状数组并不是像优先队列那样的新的代码知识,用的还是普通的数组,只是数据在数组中存储的方法很特别,这种特别的方法对于我们来说可能会觉得又麻烦又难懂,但是计算机用起来却会特别快,快很多倍,acm的比赛中对于运行时间的要求是很苛刻的,所以学会使用花时间较小的算法就显得非常重要了。

在讲它之前,先看一张图:


        是不是很像倒着的一棵树 ?

        可以发现一个规律,每一个数下面都会连着两个数,上面连一个数,这就好像家族系谱图一样,可以把某个数上面的数看成它的父亲,下面两个数看成他的儿子,一个数都占一个节点,所以我们它给上面的数起名叫 父节点,下面的叫 子节点,父节点和子节点都是相对的,就像儿子和父亲是相对的一样。

       再进一步观察,发现父节点是两个子节点的中位数,再仔细观察还可以发现,同一层的子节点,两个兄弟的差是固定的,而且每上升一层差值都会翻一倍。

        然而这有什么用呢?

        可能用处不大,但是可以肯定他们之间必定有许多规律限制着整个树的分布。

        在二进制的世界里,需要把一个十进制的数变一下,比如15=8+4+2+1=2^3+2^2+2^1+2^0,写作1111(2),同理10能拆成8+2,这和上面的图有啥关系?经过我仔细观察,发现对于每一个子节点,把它按照上述方法拆开之后,得到的最小的那个数,比如15得到的是1,10得到的是2,这个最小的数字,就是它和它的父节点的差值,所以我现在告诉你一个数是不是立马就知道了它的父节点是几了?不是,我们只是知道了差值,却不知道父节点是加这个差值还是减这个差值,这时候就要接着分析了,接下来可以看拆开的数字中第二小的数(别问我怎么想到的,都是规律),分析所有右边的子节点,发现拆开之后最小的那两个数字,总是两倍的关系,如果不是两倍,那就是右节点,比如14=8+4+2, 4和2是两倍关系,所以它是12的右侧子节点,这样的话就可以仅仅通过得到一个数就知道它的父节点是几了,知道父节点也可以知道子节点了,怎么知道的?比如14,拆开之后最小数是2,说明它和父节点差2,也就是说和子节点差1(上面的红字是这句话的解释),所以14的两个子节点分别是14-1=13和14+1=15,然后现在如果给你起点数字,指定一条路经怎么走,是不是就可以知道终点是几了?

下面是一道例题,来自Codeforces 792D,题意大概就是上面的蓝字:

Paths in a Complete Binary Tree

 

T is a complete binary tree consisting of n vertices. It means that exactly one vertex is a root, and each vertex is either a leaf (and doesn't have children) or an inner node (and has exactly two children). All leaves of a complete binary tree have the same depth (distance from the root). So n is a number such that n + 1 is a power of 2.

In the picture you can see a complete binary tree with n = 15.

Vertices are numbered from 1 to n in a special recursive way: we recursively assign numbers to all vertices from the left subtree (if current vertex is not a leaf), then assign a number to the current vertex, and then recursively assign numbers to all vertices from the right subtree (if it exists). In the picture vertices are numbered exactly using this algorithm. It is clear that for each size of a complete binary tree exists exactly one way to give numbers to all vertices. This way of numbering is called symmetric.

You have to write a program that for given n answers q queries to the tree.

Each query consists of an integer number ui (1 ≤ ui ≤ n) and a string si, where ui is the number of vertex, and si represents the path starting from this vertex. String si doesn't contain any characters other than 'L', 'R' and 'U', which mean traverse to the left child, to the right child and to the parent, respectively. Characters from si have to be processed from left to right, considering that ui is the vertex where the path starts. If it's impossible to process a character (for example, to go to the left child of a leaf), then you have to skip it. The answer is the number of vertex where the path represented by si ends.

For example, if ui = 4 and si = «UURL», then the answer is 10.

Input

The first line contains two integer numbers n and q (1 ≤ n ≤ 1018q ≥ 1). n is such that n + 1 is a power of 2.

The next 2q lines represent queries; each query consists of two consecutive lines. The first of these two lines contains ui (1 ≤ ui ≤ n), the second contains non-empty string sisi doesn't contain any characters other than 'L', 'R' and 'U'.

It is guaranteed that the sum of lengths of si (for each i such that 1 ≤ i ≤ q) doesn't exceed 105.

Output

Print q numbers, i-th number must be the answer to the i-th query.

Example
Input
15 2
4
UURL
8
LRLLLLLLLL
Output
10
5
         
然后就可以轻而易举的写出它的代码了:

#include<iostream>
#include<cstdio>
#include<cstring>

using namespace std;

__int64 n,num,s,i,j,b;
char lu[100000];

int main() {
    cin>>n>>num;
    for(i=0;i<num;i++) {
        cin>>s;
        scanf("%s",lu);
        b=strlen(lu);

        for(j=0;j<b;j++) {
            b=s&(-s);
            if(lu[j]=='L') s-=b/2;
            if(lu[j]=='R') s+=b/2;
            if(lu[j]=='U') {
                if(s==(n+1)/2) continue;
                if(((s-b)&(b-s))==2*b) s-=b;
                else s+=b; 
            } 
        }

    printf("%lld\n",s);
    }
}

        18行到25行就是核心算法了,可以发现代码中有一种新的运算符号“&”,它的运算方法是把它左右的两个数变成二进制后再按位求合取,也就是“且”运算,也就是说只有当两个数字都是1时,值才为1,否则为0,比如:

10&(-10)

        10的二进制是1010,而负数的二进制数的值为它相反数二进制的各个位取反后再加1,比如1010取反后为0101,加1后为0110,所以0110就是-10的二进制表示方法,接下来进行&运算:


1010

0110

———

0010


        得到的答案是0010,也就是2,所以把10分解后最小的值就是2,那么


b=s&(-s);
if(lu[j]=='L') s-=b/2;
if(lu[j]=='R') s+=b/2;

        这三行代码就很好理解了,b是分解后的最小值,它和它的子节点的差值就是这个b,所以只需要在原来的基础上加b或者减b就可以了,现在难点来了,怎么找父节点呢?代码如下:

if(lu[j]=='U') {
    if(s==(n+1)/2)   continue;
    if(((s-b)&(b-s))==2*b)   s-=b;
    else   s+=b;
}

        这几行代码是怎么实现这个功能的呢?首先第二行就不用看了,这一行主要是用来跳过无效的操作,

所以重点全都在第三行和第四行了,因为b是分解后最小的数,所以s-b后,很明显再分解的话原来倒数第二小的数现在就变成了最小的,所以(s-b)&(b-s)就是原来的s分解后倒数第二小的数了,上面也分析了并得到了结论(请看上文的绿字),所以现在只要满足(s-b)&(b-s)==2*b就说明s相对于父节点来说是右边的子节点了,s-b就是父节点的值。

        到此大概的思路已经明白了,然后也要考虑一些特殊情况,比如s=4时,分解后只有一个数,还是4,怎么办?不用担心,上面的算法即使不改动也不会出错,4-4后变成0,再分解还是0,明显0!=2*4,所以它不是右子节点,观察图片发现它们刚好都是左子节点。

        虽然这种看起来像是在找规律的方法让人觉得很不靠谱,但是如果我们没办法找出一个反例证明一个规律是错的,那么认为它是对的也无妨,对和错本来就是相对于人类的世界观而言的。并且这道题我这样写也确实 Accepted 了,所以准确性是毋庸置疑的。

        这个题说到底只是小试牛刀,而且并没有用到数组,接下来的题就表现出树状数组的绝对优势了。


(待更新,打字慢,要花些时间)

猜你喜欢

转载自blog.csdn.net/littlewhitelv/article/details/80072274
今日推荐