Skip to content

线段树

参考链接:https://juejin.cn/post/7024450888727003143

前言

首先提出一个问题:对于一个长度为n的序列,进行以下操作:

给下标为x的位置的数加上c,简称 modify 求区间 [ L, R ] 的和 ,简称 query

有下列几种方法可以实现

利用数组实现操作,modify的时间复杂度为O(1),query的时间复杂度为O(N) 利用前缀和数组,modify的时间复杂度为O(N),query的时间复杂度为O(1)

如果题中要求进行m次modify或者query操作,那么时间复杂度就会变成O(n*m),很多情况下会超时;于是,树状数组站出来了; 树状数组对这两种操作的时间复杂度进行了折中处理

modify复杂度为O(log n) query 复杂度为O(log n)

原理

image.png query
拿求区间 sum=[1,12] 的和举个例子;由图例可知 sum= c[12] + c[8] 又因为8=12-lowbit(12),所以 sum= c[12] + c[12-lowbit(12)] 于是推出区间求和操作 sum=[1,R]= c[R] + c[R-lowbit(R)] + c [ temp - lowbit(temp)]+.... (temp=R-lowbit(R))

js
function query(x)//查询区间1~x的区间和;
{
	let res=0;
	for(let i=x;i>=1;i-=lowbit(i)) res+=tr[i];
	return res; 
}

modify
那么更改位置x的数值以后,怎样更新区间的数值呢? eg:更新a[5]位置 的值,执行操作a[5]+=c;

首先a[5]这个位置要加上c,也就是 c[5]+=c 然后找子节点和父节点对应关系:c[5]对应的父节点为 c[6] ,c[6]对应的父节点为c[8], c[8]对应的父节点为c[16] 也就是 5 ——> 6 ——> 8 ——> 16 转化关系为 6 =5+lowbit(5) ; 8 =6+lowbit(6) ; 16=8+lowbit(8)

所以子节点更新父节点的方式就是,每次加上自身的lowbit

js
function modify(x,c)//修改树状数组x位置的值
{
	for(let i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}

上述就是树状数组的基本原理,对于由父节点找子节点,由子节点找父节点的二进制原理,以及lowbit的基本原理,这里没有介绍;感兴趣的同学可以去b站找视频或者csdn等了解下; 模板

js
function lowbit(x)
{
	return x&-x;
}
function modify(x,c)//修改树状数组x位置的值
{
	for(let i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
}
function query(x)//查询区间1~x的区间和;
{
	int res=0;
	for(let i=x;i>=1;i-=lowbit(i)) res+=tr[i];
	return res; 
}

实际作用

简单说下树状数组能解决的问题:

单点修改,区间求和(利用模板即可) 区间修改,单点查询(做差分树状数组) 区间修改,区间求和(做差分树状数组,做前缀树状数组)

(能用树状数组解决的问题都能用线段树解决,树状数组原理难理解,但代码简单;线段树原理简单,代码长)

leetcode例题

2179.统计数组中好三元组数目(困难)
题目:
给你两个下标从 0 开始且长度为 n 的整数数组 nums1 和 nums2 ,两者都是 [0, 1, ..., n - 1] 的 排列 。

好三元组 指的是 3 个 互不相同 的值,且它们在数组 nums1 和 nums2 中出现顺序保持一致。换句话说,如果我们将 pos1v 记为值 v 在 nums1 中出现的位置,pos2v 为值 v 在 nums2 中的位置,那么一个好三元组定义为 0 <= x, y, z <= n - 1 ,且 pos1x < pos1y < pos1z 和 pos2x < pos2y < pos2z 都成立的 (x, y, z) 。

请你返回好三元组的 总数目 。
示例 1:

输入:nums1 = [2,0,1,3], nums2 = [0,1,2,3] 输出:1 解释: 总共有 4 个三元组 (x,y,z) 满足 pos1x < pos1y < pos1z ,分别是 (2,0,1) ,(2,0,3) ,(2,1,3) 和 (0,1,3) 。 这些三元组中,只有 (0,1,3) 满足 pos2x < pos2y < pos2z 。所以只有 1 个好三元组。
示例 2:

输入:nums1 = [4,0,1,3,2], nums2 = [4,1,0,2,3] 输出:4 解释:总共有 4 个好三元组 (4,0,3) ,(4,0,2) ,(4,1,3) 和 (4,1,2) 。

提示:

n == nums1.length == nums2.length 3 <= n <= 105 0 <= nums1[i], nums2[i] <= n - 1 nums1 和 nums2 是 [0, 1, ..., n - 1] 的排列。

解:

js
/**
 * @param {number[]} nums1
 * @param {number[]} nums2
 * @return {number}
 */
var goodTriplets = function(nums1, nums2) {
    let n =nums1.length,res=0
    // 哈希表存储nums2每个元素出现的位置 为了好算线段树,位置从1开始
    let map = new Map()
    for(let i=0,n=nums2.length;i<n;i++){
        map.set(nums2[i],i+1)
    }
    // 线段树存储已遍历过的nums1中元素对应nums2中的位置
    let tree = Array(n+1).fill(0)
    // 线段树模版
    let lowbit = (x) => x & -x
    let modify = (idx, x) => {
        for(;idx<=n;idx+=lowbit(idx))tree[idx]+=x
    }
    let sum = (idx) => {
        let res = 0
        for(;idx>=1;idx-=lowbit(idx))res+=tree[idx]
        return res
    }
    // 遍历nums1
    for(let i =0;i<n;i++){
        // 将nums1当前元素放到对应nums2的位置
        let idx = map.get(nums1[i])
        // 统计左边出现过的元素个数
        let left = sum(idx)
        // 统计右边没出现过的元素个数 = 右边的位置数 - 右边出现的个数
        // 右边共有(n-idx)个位置,右边出现的个数 = 已经出现的-左边出现的 = i-left 
        let right = n - idx - i + left
        // 加上当前遍历元素能形成的好三元组数目
        res+=(left*right)
        // 将当前元素放入用以更新线段树
        modify(idx,1)
    }
    return res
};

MIT Licensed