开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 3 天,点击查看活动详情
线段树是什么?
线段树是一个平衡的二叉树,它将每个长度不为1的区间划分成左右两个区间递归求解。令整个区间的长度为N,则其有N个叶节点,每个叶节点代表一个单位区间,每个内部结点代表的区间为其两个儿子代表区间的联集。这种数据结构可以方便的进行大部分的区间操作。(维基百科)
线段树常用于算法竞赛中。以序列{10,11,12,13,14}为例。
该线段树主要表达区间和的信息,也可以构造表达最大值或最小值的区间信息。线段树可以让有关区间的操作复杂度在o(logn)
线段树的基本操作
以洛谷该道题为例。www.luogu.com.cn/problem/P33…
该题分两个操作,节点x加y/输出区间[x,y]的和。
线段树的建树过程
已知序列,对序列进行线段树的构造。1<=n<=5x10^5,因此需要开辟一个4n的数组。
为什么线段树需要开辟4n的大小?
首先,对于高为h层的满二叉树,共有2^h-1个节点,最后一层有2^(h-1)个节点。也就是说除了最后一层的前面所有节点数=总节点数-最后一层节点数=(2^h-1)-2^(h-1)=2^(h-1)-1,前面所有层节点之和等于最后一层节点数
- 如果n恰好是2的k次幂m,由于线段树的叶子结点存储的是数组元素本身,节点数为n,前面所有层节点数之和是n-1,总节点数是2n-1。
- 如果n不是2的k次幂,最坏情况是n = 2^k+1,有一个元素需要开辟新的一层存储,需要n-2+2(n-1)+n-2=4n-5的大小。
对于该题,建树过程如下
// 建树过程,序号为i的节点表示区间[l,r],完善tree
func buildTree(i, l, r int) {
if l == r {
tree[i] = nums[l]
return
}
mid := (l + r) >> 1
buildTree(i+i, l, mid)
buildTree(i+i+1, mid+1, r)
tree[i] = tree[i+i] + tree[i+i+1]
}
使用buildTree(1,1,n)表示序号1开始,建立从1-n的线段树。建树的基本思路就是递归。
而对于两个操作。
线段树的其他操作
- 操作1:节点x+y,那么线段树中从根结点到x的路径都需要+y
// 对于符号k,对应区间[l,r],区间中第x个数+y
func add(k, l, r, x, y int) {
// 区间[l,r]包含x,对于x的路径都要加y
tree[k] += y
// 叶子结点
if l == r {
return
}
mid := (l + r) >> 1
if x <= mid {
add(k+k, l, mid, x, y)
} else {
add(k+k+1, mid+1, r, x, y)
}
}
- 操作2: 返回[x,y]的区间和。其实就是在线段树中寻找区间[x,y],返回对应节点的值。
// 符号为i的节点,对应区间[l,r],将[x,y]区间的和输出
func calc(i, l, r, x, y int) int {
if l == x && r == y {
return tree[i]
}
mid := (l + r) >> 1
if y <= mid {
// 所求区间在左子树
return calc(i+i, l, mid, x, y)
} else if x > mid {
// 所求区间在右子树
return calc(i+i+1, mid+1, r, x, y)
} else {
// 所求区间在左子树和右子树中间
return calc(i+i, l, mid, x, mid) + calc(i+i+1, mid+1, r, mid+1, y)
}
}
其他
我是用go语言完成该题,输入输出使用fmt.Scan/fmt.Println,提交后超时,最高70分。查看题解,获知了go语言竞赛的常用模版。
- 标准库的fmt包没有缓冲区,需要使用bufio包进行封装。
- fscan不是从标准输入中读取数据而是从
io.Reader中读取数据。
var (
in = bufio.NewReader(os.Stdin)
out = bufio.NewWriter(os.Stdout)
)
func main() {
defer out.Flush()
var n, m int
fmt.Fscan(in, &n, &m)
nums = make([]int, n+1)
for i := 1; i <= n; i++ {
fmt.Fscan(in, &nums[i])
}
buildTree(1, 1, n)
for i := 0; i < m; i++ {
fmt.Fscan(in, &flag, &x, &y)
if flag == 1 {
add(1, 1, n, x, y)
} else if flag == 2 {
fmt.Fprintln(out, calc(1, 1, n, x, y))
}
}
}
其他应用
每天有10亿人的登录,用户登出时,会输出登入时间和登出时间,平均在线时长为1小时,如何快速的求某一秒钟内的在线人数?
思路:可以用线段树来处理。在知道(loginTime,logoutTime)时,将区间(loginTime, logoutTime-1)加1。
参考
juejin.cn/post/685861…
www.bilibili.com/video/BV1qY…
pkg.go.dev/bufio#Write…
blog.csdn.net/xiexingshis…