327. 区间和的个数
给你一个整数数组 nums 以及两个整数 lower 和 upper 。
求数组中,值位于范围[lower,upper] (包含 lower 和 upper)之内的 区间和的个数 。
区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
示例 1: 输入:nums = [-2,5,-1], lower = -2, upper = 2 输出:3
* 解释:存在三个区间:[0,0]、[2,2] 和 [0,2] ,对应的区间和分别是:-2 、-1 、2 。示例 2:
* 输入:nums = [0], lower = 0, upper = 0 输出:1
package leetcode;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
class BIT {
int[] tree;
int n;
BIT(int n) {
this.n = n;
this.tree = new int[n + 1];
}
static int lowbit(int x) {
return x & (-x);
}
void update(int x, int d) {
while (x <= n) {
tree[x] += d;
x += lowbit(x);
}
}
int query(int x) {
int ans = 0;
while (x != 0) {
ans += tree[x];
x -= lowbit(x);
}
return ans;
}
}
public class 三二七 {
static int[] tr;
static int max;
public static void main(String[] args) {
int[] nums = { -2, 5, -1 };
System.out.println(countRangeSum1(nums, -2, 2));
}
// 方法一:树状数组+离散化
static int countRangeSum1(int[] nums, int lower, int upper) {
long sum = 0;
long[] preSum = new long[nums.length + 1];
for (int i = 0; i < nums.length; ++i) {
sum += nums[i];
preSum[i + 1] = sum;
}
Set<Long> allNumbers = new TreeSet<Long>();
for (long x : preSum) {
allNumbers.add(x);
allNumbers.add(x - lower);
allNumbers.add(x - upper);
}
// 利用哈希表进行离散化
Map<Long, Integer> values = new HashMap<Long, Integer>();
int idx = 0;
for (long x : allNumbers) {
values.put(x, idx);
idx++;
}
System.out.println(allNumbers);
System.out.println(values);
int ret = 0;
BIT bit = new BIT(values.size());
for (int i = 0; i < preSum.length; i++) {
int left = values.get(preSum[i] - upper), right = values.get(preSum[i] - lower);
ret += bit.query(right + 1) - bit.query(left);
System.out.print(right+" "+left+" | ");
System.out.println(bit.query(right + 1)+" "+bit.query(left));
System.out.println();
bit.update(values.get(preSum[i]) + 1, 1);
}
return ret;
}
// 方法二:归并排序
static int countRangeSum2(int[] nums, int lower, int upper) {
// 前缀和数组+归并排序 Nlog(N)
long[] preSum = new long[nums.length + 1]; // nums[i]=preSum[i+1]-preSum[i] 所以前驱和长度多1
long sum = 0;
for (int i = 0; i < nums.length; i++) {
sum += nums[i];
preSum[i + 1] = sum;
}
return mergesort(preSum, 0, preSum.length - 1, lower, upper);
}
static int mergesort(long[] arr, int left, int right, int lower, int upper) {
// 归并分而治之,拆分待排序数据,递归实现
if (left == right)
return 0;
int mid = (left + right) / 2;
int resLeft = mergesort(arr, left, mid, lower, upper);
int resRight = mergesort(arr, mid + 1, right, lower, upper);
int res = resLeft + resRight;
// 统计当前待合并数组间的区间和个数,每一个右边元素减去每一个左边元素(表示一段区间和)
// 转化为两升序数组之间的差在某一区间的问题,维护i指向arr1,l和r指向arr2
// 让l和r分别指向与i的差在区间边界的情况,再统计个数即可,主要是为了降低时间复杂度!
// [l,r)都符合条件
int i = left;
int l = mid + 1;
int r = mid + 1;
while (i <= mid) {
while (l <= right && arr[l] - arr[i] < lower) {
l++;
}
while (r <= right && arr[r] - arr[i] <= upper) {
r++;
}
res += r - l;
i++;
}
// 归并,使用传统方法,维护i和j两个变量即可
long[] temp = new long[right - left + 1];
int t = 0;
i = left;
int j = mid + 1;
while (i <= mid && j <= right) {
if (arr[i] <= arr[j])
temp[t++] = arr[i++];
else
temp[t++] = arr[j++];
}
while (i <= mid) {
temp[t++] = arr[i++];
}
while (j <= right) {
temp[t++] = arr[j++];
}
t = 0;
while (t < temp.length) {
arr[left++] = temp[t++];
}
return res; // 返回当前节点满足条件的区间和个数
}
// 方法三:线段树+离散化
static int countRangeSum3(int[] nums, int lower, int upper) {
long sum = 0;
long[] preSum = new long[nums.length + 1];
for (int i = 0; i < nums.length; ++i) {
sum += nums[i];
preSum[i + 1] = sum;
}
Set<Long> allNumbers = new TreeSet<Long>();
for (long x : preSum) {
allNumbers.add(x);
allNumbers.add(x - lower);
allNumbers.add(x - upper);
}
// 利用哈希表进行离散化
Map<Long, Integer> values = new HashMap<Long, Integer>();
int idx = 0;
for (long x : allNumbers) {
values.put(x, idx);
idx++;
}
SegNode327 root = build(0, values.size() - 1);
int ret = 0;
for (long x : preSum) {
int left = values.get(x - upper), right = values.get(x - lower);
ret += count(root, left, right);
insert(root, values.get(x));
}
return ret;
}
static SegNode327 build(int left, int right) {
SegNode327 node = new SegNode327(left, right);
if (left == right) {
return node;
}
int mid = (left + right) / 2;
node.lchild = build(left, mid);
node.rchild = build(mid + 1, right);
return node;
}
static int count(SegNode327 root, int left, int right) {
if (left > root.hi || right < root.lo) {
return 0;
}
if (left <= root.lo && root.hi <= right) {
return root.add;
}
return count(root.lchild, left, right) + count(root.rchild, left, right);
}
static void insert(SegNode327 root, int val) {
root.add++;
if (root.lo == root.hi) {
return;
}
int mid = (root.lo + root.hi) / 2;
if (val <= mid) {
insert(root.lchild, val);
} else {
insert(root.rchild, val);
}
}
// 方法四:动态增加节点的线段树
static int countRangeSum4(int[] nums, int lower, int upper) {
long sum = 0;
long[] preSum = new long[nums.length + 1];
for (int i = 0; i < nums.length; ++i) {
sum += nums[i];
preSum[i + 1] = sum;
}
long lbound = Long.MAX_VALUE, rbound = Long.MIN_VALUE;
for (long x : preSum) {
lbound = Math.min(Math.min(lbound, x), Math.min(x - lower, x - upper));
rbound = Math.max(Math.max(rbound, x), Math.max(x - lower, x - upper));
}
SegNode327 root = new SegNode327(lbound, rbound);
int ret = 0;
for (long x : preSum) {
ret += count(root, x - upper, x - lower);
insert(root, x);
}
return ret;
}
static int count(SegNode327 root, long left, long right) {
if (root == null) {
return 0;
}
if (left > root.hi || right < root.lo) {
return 0;
}
if (left <= root.lo && root.hi <= right) {
return root.add;
}
return count(root.lchild, left, right) + count(root.rchild, left, right);
}
static void insert(SegNode327 root, long val) {
root.add++;
if (root.lo == root.hi) {
return;
}
long mid = (root.lo + root.hi) >> 1;
if (val <= mid) {
if (root.lchild == null) {
root.lchild = new SegNode327(root.lo, mid);
}
insert(root.lchild, val);
} else {
if (root.rchild == null) {
root.rchild = new SegNode327(mid + 1, root.hi);
}
insert(root.rchild, val);
}
}
// 方法五:平衡二叉搜索树
static int countRangeSum5(int[] nums, int lower, int upper) {
long sum = 0;
long[] preSum = new long[nums.length + 1];
for (int i = 0; i < nums.length; ++i) {
sum += nums[i];
preSum[i + 1] = sum;
}
BalancedTree treap = new BalancedTree();
int ret = 0;
for (long x : preSum) {
long numLeft = treap.lowerBound(x - upper);
int rankLeft = (numLeft == Long.MAX_VALUE ? (int) (treap.getSize() + 1) : treap.rank(numLeft)[0]);
long numRight = treap.upperBound(x - lower);
int rankRight = (numRight == Long.MAX_VALUE ? (int) treap.getSize() : treap.rank(numRight)[0] - 1);
ret += rankRight - rankLeft + 1;
treap.insert(x);
}
return ret;
}
}
class BalancedTree {
private class BalancedNode {
long val;
long seed;
int count;
int size;
BalancedNode left;
BalancedNode right;
BalancedNode(long val, long seed) {
this.val = val;
this.seed = seed;
this.count = 1;
this.size = 1;
this.left = null;
this.right = null;
}
BalancedNode leftRotate() {
int prevSize = size;
int currSize = (left != null ? left.size : 0) + (right.left != null ? right.left.size : 0) + count;
BalancedNode root = right;
right = root.left;
root.left = this;
root.size = prevSize;
size = currSize;
return root;
}
BalancedNode rightRotate() {
int prevSize = size;
int currSize = (right != null ? right.size : 0) + (left.right != null ? left.right.size : 0) + count;
BalancedNode root = left;
left = root.right;
root.right = this;
root.size = prevSize;
size = currSize;
return root;
}
}
private BalancedNode root;
private int size;
private Random rand;
BalancedTree() {
this.root = null;
this.size = 0;
this.rand = new Random();
}
long getSize() {
return size;
}
void insert(long x) {
++size;
root = insert(root, x);
}
long lowerBound(long x) {
BalancedNode node = root;
long ans = Long.MAX_VALUE;
while (node != null) {
if (x == node.val) {
return x;
}
if (x < node.val) {
ans = node.val;
node = node.left;
} else {
node = node.right;
}
}
return ans;
}
long upperBound(long x) {
BalancedNode node = root;
long ans = Long.MAX_VALUE;
while (node != null) {
if (x < node.val) {
ans = node.val;
node = node.left;
} else {
node = node.right;
}
}
return ans;
}
int[] rank(long x) {
BalancedNode node = root;
int ans = 0;
while (node != null) {
if (x < node.val) {
node = node.left;
} else {
ans += (node.left != null ? node.left.size : 0) + node.count;
if (x == node.val) {
return new int[] { ans - node.count + 1, ans };
}
node = node.right;
}
}
return new int[] { Integer.MIN_VALUE, Integer.MAX_VALUE };
}
private BalancedNode insert(BalancedNode node, long x) {
if (node == null) {
return new BalancedNode(x, rand.nextInt());
}
++node.size;
if (x < node.val) {
node.left = insert(node.left, x);
if (node.left.seed > node.seed) {
node = node.rightRotate();
}
} else if (x > node.val) {
node.right = insert(node.right, x);
if (node.right.seed > node.seed) {
node = node.leftRotate();
}
} else {
++node.count;
}
return node;
}
}
class SegNode327 {
int lo, hi, add;
SegNode327 lchild, rchild;
SegNode327(int left, int right) {
lo = left;
hi = right;
add = 0;
lchild = null;
rchild = null;
}
SegNode327(long left, long right) {
lo = (int) left;
hi = (int) right;
add = 0;
lchild = null;
rchild = null;
}
}
细讲线段树+离散化
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
class SegNode {
int lo, hi, add;
SegNode lchild, rchild;
public SegNode(int left, int right) {
lo = left;
hi = right;
add = 0;
lchild = null;
rchild = null;
}
public void printf() {
if (this.lchild != null) {
this.lchild.printf();
}
System.out.println("lo=" + this.lo + ", hi=" + hi + ", add=" + add);
if (this.rchild != null) {
this.rchild.printf();
}
}
}
/**
* 整体思路(面向结果型,即不提供想到算法的思路,只描述代码实现的思路):
* 满足题目要求的连续数组通过数学公式表达如下:
* lower <= sum[i, j] <= upper
* 第一层转换:
* 实际计算过程中需要遍历所有0 <= i <= j < input.length的组合且存在大量的重复运算,因此通过前缀和进行转换
* preSum[i] = sum[0, i - 1]
* 第二层转换:
* 通过前缀和数组变形之后的满足题目要求的公式表达变为:
* lower <= preSum[j] - preSum[i - 1] <= upper
* 也就是说preSum[i - 1]数值在[preSum[j] - upper, preSum[j] - lower]范围内表示input[i, j]满足题目要求
* 至此,我们将原始输入转换成了各个前缀和数组之间的【范围】关系,将范围关系保存在helper数组中
* 第三层转换:
* 由于input[]、lower和upper内容离散导致helper数组内容也离散,为便于查找,将helper[]的内容进行hash映射
* 核心算法:
* 将helper数组转换为线段树保存,利用线段树可以快速查询连续数组数据特征的功能统计每个叶子节点的【范围】
*/
public class Main {
public int countRangeSumMergeSegmentTree(int[] nums, int lower, int upper) {
long sum = 0;
int tempCount = 0;
long[] preSum = new long[nums.length + 1];
for (int i = 0; i < nums.length; ++i) {
sum += nums[i];
preSum[i + 1] = sum;
}
Set<Long> helper = new TreeSet<Long>();
for (long x : preSum) {
helper.add(x);
helper.add(x - lower);
helper.add(x - upper);
}
/**
* 利用哈希表进行离散化,通常构建线段树时,需要严格划分所有叶子节点,也就是每个叶子节点表示的数据范围为1
* 因此,通过hashMap将原始数据映射到数据范围较小的数值上,以代码中输入[-2, 5, 1]为例
* sum=[0, -2, 3, 4]
* helper=[-4, -2, 0, 1, 2, 3, 4, 5, 6]
* map=[
* key : 0 1 2 3 4 5 6 7 8
* value: -4 -2 0 1 2 3 4 5 6
* ]
* 实现了将范围较大的value映射比较聚合的“连续”的“小”数组上
*/
Map<Long, Integer> values = new HashMap<Long, Integer>();
int idx = 0;
for (long x : helper) {
values.put(x, idx);
idx++;
}
System.out.println("preSum:");
System.out.println(Arrays.toString(preSum));
System.out.println("\nhelper:");
System.out.println(helper);
System.out.println("\nhelper map:");
System.out.println(values);
/* 按照helper[]的大小构建空的线段树,此时线段树只有标注map的key的位置的功能,还不具备统计【范围】的能力
* 本例的线段树如下([left, right]):
* [0,8]---------------------------+
* | |
* [0,4]---------------+ [5,8]---------+
* | | | |
* [0,2]---------+ [3,4]---+ [5,6]---+ [7,8]---+
* | | | | | | | |
* [0,1]---+ | | | | | | |
* | | | | | | | | |
* [0,0] [1,1] [2,2] [3,3] [4,4] [5,5] [6,6] [7,7] [8,8]
*/
SegNode root = build(0, values.size() - 1);
int ret = 0;
for (long x : preSum) {
/*
* left和right表示preSum[x]的范围,举例来说:
* preSum[0]=0,不需要判断
* preSum[1]=-2,它满足题目的要求就是[-4, 0]
*/
int left = values.get(x - upper), right = values.get(x - lower);
System.out.println("\nx=" + x + ", x.idx=" + values.get(x) + ", left.idx=" + left + ", right.idx=" + right);
/*
* 这里的代码实现不全是线段树统计的算法,比较顺畅的思路应该是先将preSum的范围插入线段树,也就是更新线段树的内容
* 然后再依次统计所有preSum的结果,由于上述说法需要两次for循环,从效率的角度以上两个操作可以合并在一个for循环中,
* 从结果正确性的角度来说,preSum[0]其实不是一个有效的结果
* 因此这里实际操作的是preSum[1]开始的数据,因为第一轮循环时,preSum[0]的数据还没有更新到线段树中
* 对于preSum[1]来说,它的范围通过前文的left、right保存,只需要在当前的线段树中统计节点【范围】在left、right之间即可
*/
tempCount = count(root, left, right);
ret += tempCount;
System.out.println("count=" + tempCount);
root.printf();
/*
* 将preSum[i]的数据更新到线段树中,这里更新的内容是preSum[i]对应的hash值
* 在for循环时这样考虑
* preSum[0]=0 实际没有进行过统计
* preSum[1]=-2 insert之后,本例的线段树如下({[left, right]=add}):
* {[0,8]=1}--------------------------------------------+
* | |
* {[0,4]=1}------------------------+ {[5,8]=1}-------------+
* | | | |
* {[0,2]=1}-------------+ {[3,4]=0}--+ {[5,6]=0}---+ {[7,8]=0}---+
* | | | | | | | |
* {[0,1]=0}---+ | | | | | | |
* | | | | | | | | |
* {[0,0]=0} {[1,1]=0} {[2,2]=1} {[3,3]=0} {[4,4]=0} {[5,5]=0} {[6,6]=0} {[7,7]=0} {[8,8]=0}
* 此时进行count,统计的是从input开始到input[0]为在满足要求的连续子数组的数量,对于preSum[1]=-2而言,满足要求的范围是[-4,-2],
* 对应树形hash结构的[0,2], count=1
*
* preSum[2]=3 insert之后
* {[0,8]=2}--------------------------------------------+
* | |
* {[0,4]=2}------------------------+ {[5,8]=1}-------------+
* | | | |
* {[0,2]=2}-------------+ {[3,4]=0}--+ {[5,6]=0}---+ {[7,8]=0}---+
* | | | | | | | |
* {[0,1]=0}---+ | | | | | | |
* | | | | | | | | |
* {[0,0]=0} {[1,1]=1} {[2,2]=1} {[3,3]=0} {[4,4]=0} {[5,5]=0} {[6,6]=0} {[7,7]=0} {[8,8]=0}
* 此时进行count,统计的是input开始input[2]为止满足要求的连续子数组的数量,对于preSum[2]=3而言,满足要求的范围是[1,5],
* 对应树形hash结构的[3,7], count=0
*
* preSum[3]=3 insert之后
* {[0,8]=3}--------------------------------------------+
* | |
* {[0,4]=2}------------------------+ {[5,8]=1}-------------+
* | | | |
* {[0,2]=2}-------------+ {[3,4]=0}--+ {[5,6]=1}---+ {[7,8]=0}---+
* | | | | | | | |
* {[0,1]=0}---+ | | | | | | |
* | | | | | | | | |
* {[0,0]=0} {[1,1]=1} {[2,2]=1} {[3,3]=0} {[4,4]=0} {[5,5]=1} {[6,6]=0} {[7,7]=0} {[8,8]=0}
* 此时进行count,统计的是input开始input[3]为止满足要求的连续子数组的数量,对于preSum[3]=3而言,满足要求的范围是[3,7],
* 对应树形hash结构的[3,7], count=1
* 后续的依次类推
*/
insert(root, values.get(x));
System.out.println("\nsegment tree after insert: ");
root.printf();
}
System.out.println("\nsegment tree: ");
root.printf();
return ret;
}
public SegNode build(int left, int right) {
SegNode node = new SegNode(left, right);
if (left == right) {
return node;
}
int mid = (left + right) / 2;
node.lchild = build(left, mid);
node.rchild = build(mid + 1, right);
return node;
}
public int count(SegNode root, int left, int right) {
if (left > root.hi || right < root.lo) {
return 0;
}
if (left <= root.lo && root.hi <= right) {
return root.add;
}
return count(root.lchild, left, right) + count(root.rchild, left, right);
}
public void insert(SegNode root, int val) {
root.add++;
if (root.lo == root.hi) {
return;
}
int mid = (root.lo + root.hi) / 2;
if (val <= mid) {
insert(root.lchild, val);
} else {
insert(root.rchild, val);
}
}
public static void main(String[] args) {
int[] input = new int[] {-2, 5, 1};
Main s = new Main();
System.out.println(s.countRangeSumMergeSegmentTree(input, -2, 2));
}
}