您的位置:首页 > 其它

树状数组与线段树入门

2015-01-16 10:46 288 查看
最近开始接触到了线段树和树状数组的题目 (refer to acm
116系类)

线段树和树状数组本身的组织都是二叉树的一种实现方式,从目前被我用到的地方来看,主要作用在于对于顺序的连续实体的资源和的查找十分迅速,线段树的表达能力比树状数组强很多,还能用在别的地方

结合题目来谈:

士兵杀敌(一)

时间限制:1000 ms
| 内存限制:65535
KB
难度:3

描述

南将军手下有N个士兵,分别编号1到N,这些士兵的杀敌数都是已知的。

小工是南将军手下的军师,南将军现在想知道第m号到第n号士兵的总杀敌数,请你帮助小工来回答南将军吧。

注意,南将军可能会问很多次问题。

输入
只有一组测试数据

第一行是两个整数N,M,其中N表示士兵的个数(1

随后的一行是N个整数,ai表示第i号士兵杀敌数目。(0<=ai<=100)

随后的M行每行有两个整数m,n,表示南将军想知道第m号到第n号士兵的总杀敌数(1<=m,n<=N)。

输出
对于每一个询问,输出总杀敌数

每个输出占一行
样例输入

5 2


1 2 3 4 5


1 3


2 4

样例输出

6
9

比如上述题目,如果采用一般的方法,直接用一个大小为N的列表去存士兵的杀敌数,每次命令之后都动态的去把m
- n 的数据区加一次,那么不用想这样的操作是肯定有问题的,最后的时间复杂度为
NM

这样肯定不能满足性能需求,那么现在怎么办呢,此时树状数组和线段树的作用就体现出来了,

树状数组的解决方案是修改原始数据的存储方式

树状数组使用长度仍为N的数组去记录数据,但是数据点的含义发生了变化,树状数组的原始数据满足一下规律:

奇数节点记录着原始数据,

偶数节点记录着包含本身在内的前n个数据的和(n满足n为能被该偶数的最大的2的倍数)

e.g.

1
2
3
4
5
6
7

8

1
1+2 3
1+2+3+4 5
5+6 7
1+2+3+4+5+6+7+8

上面举出了长度为8的树状数组的数据结构,第一个存第一个数据的值,第二节点存 第一和第二的数据和

这样的结构带来了怎么样的影响,我们就可以发现现在查询一条记录的时间被积聚所见了,如查询m-n的数据和,那么这样的时间复杂度为 log2(n)
-log2(m),也就是最后的时间复杂度为 2M*log2(N),空间复杂度并没有改变

实现的代码

import
java.util.Scanner;

//acm 108 树状数组版

public class SoldierKillEnemyV2 {

public
static void main(String[] args) {

SoldierKillEnemyV2 soldierKillEnemyV2 = new
SoldierKillEnemyV2();

soldierKillEnemyV2.solution();

}

public void
solution() {

in = new
Scanner(System.in);

data = new
int[1000001];

getData();

System.out.println();

String cmd =
"";

int x =
0;

int y =
0;

for (int i =
0; i < times; i++) {

x =
in.nextInt();

y =
in.nextInt();

System.out.println(getSum(y) - getSum(x - 1));

}

}

Scanner
in;

int[]
data;

int
soilders;

int
times;

private void
getData() {

soilders =
in.nextInt();

times =
in.nextInt();

for (int i =
1; i <= soilders; i++) {

addTodata(i,
in.nextInt());

}

}

private int
getMod2Max(int n) {

return n
& (-n);

}

//
向Data中指定位置增加数字

private void
addTodata(int n, int x) {

while (n
<= soilders) {

data
+=
x;

n +=
getMod2Max(n);

}

}

//
返回前N项的和

private int
getSum(int n) {

int result =
0;

while (n
> 0) {

result +=
data
;

n -=
getMod2Max(n);

}

return
result;

}

}

线段数的解决方案是我采取额外的空间去记录额外的数据来辅助求和

线段树是对2叉树的一个映射,它将2叉树投射到直线上

(画的丑别介意)

对于任何一颗完全2叉树来说 我们都可以采用广度遍历将树的节点编上号,这样就存在一个关系,任何一颗节点N的子节点的编号=
2N ||(2N+1)

这就是线段树用来将二叉树投射到直线上的策略,(一个完全没有优化的版本,因为并不是每个二叉树都是完全二叉树,对于不完全二叉树 这样的数据结构还是会把那部分空间预留出来,对用部分数据 存在着很大的空间浪费)

线段树的数据组织模式

1
2
3
4
5
6
7

1+2+3+4
1+2
3+4
1
2
3
4

也就是线段树都是用叶节点存取数据值,所有的节点都保存着子节点的数据之和

下面用代码来阐述线段树

//线段树 用来存取1-n 这条线段中
整数点出现的次数,已经一段中所有点出现的次数

//扩展 可以把整数点映射成n个实体,
每个实体会存放着一点资源, 线段树在大规模读取连续数的时候存在着优势

public class LineTree {

int[]
tree;

int
size;

public
LineTree(int n) {

tree = new
int[n * 4 + 1]; // 抛弃掉0点 方便index的扩张 e.g 1的子节点为2, 3 2的子节点为3 4

size =
n;

}

public
static void main(String[] args){

//test
case acm_116

LineTree
lineTree = new LineTree(5);

lineTree.Update(1, 1);

lineTree.Update(2, 2);

lineTree.Update(3, 3);

lineTree.Update(4, 4);

lineTree.Update(5, 5);

System.out.println(lineTree.Query(1, 3));

lineTree.Update(1, 2);

System.out.println(lineTree.Query(1, 3));

lineTree.Update(2, 3);

System.out.println(lineTree.Query(1, 2));

System.out.println(lineTree.Query(1, 5));

}

public void
init() throws Exception {

if (tree ==
null) {

throw new
Exception("tree 未初始化");

}

for (int i =
0; i < tree.length; i++) {

tree[i] =
0;

}

size =
0;

}

//
在这条线段中的n节点 插入权值value;

public void
Update(int n, int value) {

Update(1, 1,
size, n, value);

}

private void
Update(int index, int left, int right, int n, int value) {

tree[index]
+= value;

if (left ==
right) {

return;

} else
{

int mid =
(left + right) >> 1;

if (mid
>= n) {

Update(index
<< 1, left, mid, n, value);

} else
{

Update(index
<< 1 | 1, mid + 1, right, n, value);

}

}

}

//
差选线段起止之间的权值和

public int
Query(int start, int end) {

return
Query(1, 1, size, start, end);

}

private int
Query(int index, int left, int right, int start, int end) {

//

System.out.println(left +" "+ right);

int sum =
0;

if (left
>= start && right <= end) {

return
tree[index];

}

int mid =
(left + right) >> 1;

if (mid
>= start && left<= end) {

sum +=
Query(index << 1, left, mid, start, end);

}

if (mid
<= end && right >= start) {

sum +=
Query(index << 1 | 1, mid + 1, right, start, end);

}

return
sum;

}

}

线段树采用递归的方式解决的查询m-n的数据之和,查询的时间复杂度为 M*log2(N)

以上是对线段树和树状数组的结构分析

从是实现方式上来看,线段树的空间复杂度和插入复杂度都比树状数组差,查询的时间复杂度属于同一个数量级,但是线段树的数据信息比较大,线段树数组的信息构成的闭包要包含树状数组的闭包。线段树可以根据的现实需求变更节点和含义,而不仅仅是查询连续的实体和。之后如果遇到具体问题,我会结合问题来讲
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: