日常: 算法、数据结构、分布式系统、日常思考感悟等

[Data Structure] Sparse Table

2021.05.13

问题

我们先来看这样一个问题:

给定数组A [2,3,51,32,56,1,1,3,….] 如何高效的查询 [start, end] 范围内的最小值 或者 最大值?

分析

暴力解法自然是遍历[start, end]范围内的数,取最小值;但如果查询非常频繁或者范围本身非常大,这个方法并不高效; Sparse Table 就是解决这个问题的一种数据结构,其定义如下:

st[i][j] 表示下标为i的元素开始,长度为 2^j的子数组的和; 其中, i+2^j < len(A)

下图展示了sparse table计算过程中,如何利用前一轮的计算结果快速计算当前范围的极值

我们直接看代码

func main() {
	nums := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
	spew.Dump(rangeMinQuery(nums, 4, 7))
}

func rangeMinQuery(nums []int, start int, end int) int {
	max := int(math.Log2(float64(end - start + 1)))
	st := buildSparseTable(nums)
	// 注意这里的比较,start 和 end-max+1 之间是可能存在overlap的,但是这个对计算极值不影响
	// 这也是为什么sparse table在这种场景下极为高效的原因
	return min(st[start][max], st[end-max+1][max])
}

func buildSparseTable(nums []int) [][]int {
	ans := [][]int{}
	// 最大可能的幂次 即 2^maxpower <= len(nums)
	maxpower := int(math.Log2(float64(len(nums))))
	// 初始化
	for i := 0; i < len(nums); i++ {
		ans = append(ans, make([]int, maxpower+1))
	}
	for j := 1; j <= maxpower; j++ {
		for i := 0; i+1<<j-1 < len(nums); i++ {
			if j == 1 {
				// 长度为2时,比较相邻元素即可
				ans[i][j] = min(nums[i], nums[i+1])
			} else {
				// 长度为4, 8 ... 等时,利用上一轮计算的结果
				ans[i][j] = min(ans[i][j-1], ans[i+1<<(j-1)-1][j-1])
			}
		}
	}
	return ans
}

func min(a int, b int) int {
	if a < b {
		return a
	}
	return b
}

注意

  • 示例代码中,先进行sparse table的建立,然后进行查询,这只是为了方便展示; 实际问题中,一般需要预先建立sparse table,然后不断调用query去应对大量查询
发表评论