<二分查找+双指针+前缀和>解决子数组和排序后的区间和

题目重现:

给你一个数组 nums ,它包含 n 个正整数。你需要计算所有非空连续子数组的和,并将它们按升序排序,得到一个新的包含 n * (n + 1) / 2 个数字的数组。

请你返回在新数组中下标为 left 到 right (下标从 1 开始)的所有数字和(包括左右端点)。由于答案可能很大,请你将它对 10^9 + 7 取模后返回。

示例 1:输入:nums = [1,2,3,4], n = 4, left = 1, right = 5

输出:13

解释:所有的子数组和为 1, 3, 6, 10, 2, 5, 9, 3, 7, 4 。将它们升序排序后,我们得到新的数组 [1, 2, 3, 3, 4, 5, 6, 7, 9, 10] 。下标从 le = 1 到 ri = 5 的和为 1 + 2 + 3 + 3 + 4 = 13 。

示例 2:输入:nums = [1,2,3,4], n = 4, left = 3, right = 4

输出:6

解释:给定数组与示例 1 一样,所以新数组为 [1, 2, 3, 3, 4, 5, 6, 7, 9, 10] 。下标从 le = 3 到 ri = 4 的和为 3 + 3 = 6 。

示例 3:输入:nums = [1,2,3,4], n = 4, left = 1, right = 10

输出:50

提示:

  • 1 <= nums.length <= 10^3
  • nums.length == n
  • 1 <= nums[i] <= 100
  • 1 <= left <= right <= n * (n + 1) / 2

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/range-sum-of-sorted-subarray-sums

​ 这是在leetcode上碰到的一道题,但由于设置的测试样例并不是很好,而导致暴力解法也可通过,所以此题只是中等难度。但看过题解的解法思路后觉得有必要做一记录。由浅入深,先通过暴力解法,然后引出优化的方法。

暴力法

​ 这道题给出一个数组nums,如果暴力解题,可以先计算出它的所有非空连续子数组的和,然后进行排序,再计算它下标left到right的和,最后取余数即可。

​ 列举出所有的非空连续子数组,使用左右双指针,假设题目给定nums为1,2,3,4,那么先让左指针指1,右指针从1开始依次滑动过整个数组后面的数,即可得到以1开头的子数组和为1,3,6,10,再让左指针右移一位,继续按上述可得2,5,9......以此类推可得所有子数组,然后对其进行排序。子数组和的数目总共为n*(n+1)/2个。

//暴力法
class Solution {
public int rangeSum(int[] nums, int n, int left, int right) {
int[] new_arr = new int[n*(n+1)/2+1]; //定义数组存放所有子数组
int index = 1;
for (int i = 0; i < nums.length; i++) {
int pre = 0;
for (int j = i; j < nums.length; j++) {
new_arr[index++] = pre+nums[j]; //dp思想,左指针固定后,右指针滑动后的下一个子数组等于上次加nums[j]之和
pre = pre+nums[j]; //更新pre
}
}
Arrays.sort(new_arr); //对子数组进行排序
long count = 0;
for (int i = left; i <= right; i++) {
count+=new_arr[i];
} while (count >= 1000000007) { //取余数后返回
count -= 1000000007;
}
return (int)count;
}
}

前置讨论

​ 讨论二分查找+双指针解法前,先看leetcode的另一道题378. 有序矩阵中第K小的元素,这道题的解题思路有助于我们更好的解决上面的题目。

给定一个 n x n 矩阵,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。

请注意,它是排序后的第 k 小元素,而不是第 k 个不同的元素。

示例:

matrix = [

[ 1, 5, 9],

[10, 11, 13],

[12, 13, 15]

],

k = 8,

返回 13。

来源:力扣(LeetCode)

链接:https://leetcode-cn.com/problems/kth-smallest-element-in-a-sorted-matrix

​ 先观察这个给定的数组matrix发现:整个数组的行从左到右递增,从上到下递增,刚开始我想的是用优先级队列,首先加入一个最小的数(最左上角),然后每次加入队列头的右边的数和下边的数,周而复始的循环k次,队列头就是这个数。但由于优先级队列的维护本身就是非常耗时的,所以整个程序执行下来时间效率很低,运行了42ms。下面给出优化思路:

​ 二分查找的思路,以下图为例:(图片来自leetcode官方题解)

​ 通过观察发现mid = (1+16)/2 = 8,大于mid的都分布在红线下面,而不大于mid的部分分布在红线上面,所以可以使用二分查找。

​ 沿着图中蓝色箭头走一边,就可以计算出上方板块的大小,即不大于mid的数字的数目,这样通过二分将mid逐渐逼近第k小的元素。

​ 算法描述:目的是统计不大于当前mid的数的数目,从第0列最后一行开始,如果此列最下面的数都不大于mid,那么此列所有的数肯定都不大于mid,继续到下一列,将列指针向右移动,如果此时最后一行的数大于mid,则将指示行的指针上移直到遇到一个不大于mid的数就停止,而这个数上面的数肯定都不大于mid。如果行指针已经滑到0还没有不大于mid的数出现,那说明后面已经不可能有不大于mid的数了,因为这个数组向右和向下是递增的。

​ 当访问第j列的时候,如果第i+1行大于mid,而第i行不大于mid,则这列不大于mid的数数目为i+1(考虑第0行)。统计整个数组中不大于mid的数的数目。如果小于k,则说明mid太小,将left右移至mid+1处,否则将right移至mid处。直到左右指针相遇,此时它们所指向的就是第k小的数。

private static int kthSmallest(int[][] matrix, int k) {
int n = matrix.length;
int left = matrix[0][0];
int right = matrix[n-1][n-1];
int mid;
//二分查找,找到第k小的数
while (left < right) {
mid = left + ((right-left) >> 1);
if (check(matrix,mid,k,n)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
} //利用双指针检查当前mid是否过大(即是否在数组matrix中比mid大的数超过了k个)
private static boolean check (int[][] matrix, int mid, int k, int n) {
int i = n-1; //指示行坐标
int j = 0; //指示列坐标
int num = 0;
while (i >= 0 && j < n) {
if (matrix[i][j] <= mid) {
j++;
num += (i+1);
} else {
i--;
}
}
return num>=k;
}

Q1:考虑check函数,为什么要matrix[i][j] <= mid而不是matrix[i][j] < mid

A:因为数组matrix中可能会出现重复的数字,加入第k小的数字也重复了,形如1,2,3,3,3,3,3,5,6第5小的数字,那第3和第4个数字等于3,当却确实在第5小的数字之前。

Q2:考虑check函数,为什么要return num >= k而不是return num > k

A:因为left和right以及mid是从最小到最大的数之间的任意一个数,所以并不能保证它们就一定是数组中存在的数,如果某个mid能保证小于等于它的数恰好为k个,则第k小的数就是它之前最近的一个存在于数组中的数。所以当此时不大于mid的数大于或 等于k个时,就可以保证要求的第k小的数一定在mid或mid之前,故而将right移动到mid处。

Q3:考虑kthSmallest函数,为什么check函数返回为真就左移,假就右移?

A:设置第k小的数为res,当mid在res左边时,此时数组中不大于mid的数会因为少了res而小于k,因为res是第k个,所以left会右移,以求使mid右移。当mid在res右边时或者就刚好等于res时,此时数组中大于等于mid的数会等于或超过k个,由于res为第k个,而mid又在其右或等于,所以此时数组中不大于mid的数至少为k个,所有使right左移,使得mid左移。

Q4:考虑kthSmallest函数,为什么能保证最后left指向的就是数组中的元素呢?

A:根据check函数返回的情况不断将左右指针逼近res,mid总是在res左右横跳,带动left和right逼近res,而mid终会有一次等于res,此时不大于mid的数大于或等于k个,右指针左移到res上,这时的mid总是小于res,而导致不大于mid的数目小于k,左指针右移,左右指针相邻时,下一次左指针移动,必定移动到res上,left == right,跳出循环。

Q5:考虑kthSmallest函数,为什么right = mid,而left = mid+1呢?

A:这是由于除法向下取整而导致的二分查找的特性,假设此时left=2,right=3,则mid=2,如果left = mid,则会一直原地打转。如果right = mid-1,则可能此时的mid == res(mid==res时必定是右指针左移,参考Q3),右指针就会移动到res之前,从而错过正解。

优化解法

​ 继续回到这个题,看完前面前置的讨论后相信对解答这个题会有很大帮助。如题目给的示例1:nums = {1,2,3,4},这样我们可以构造出它的非空连续子数组的和矩阵如下:

​ 第1行是以1开头的子数组的和,分别对应1;1,2;1,2,3;1,2,3,4,第2行是以2行开始的子数组的和,以此类推,观察此数组发现,这个数组从左到右以此递增,从上到下以此递增,看到这应该就明白了上面那个前置讨论的意义了。

​ 先确定我们的大思路:题目要求构造一个非空连续子数组的和,在这个新数组中从left到right的元素之和,那我们可以参考前置讨论里的方法先得到前left-1大的数字,然后计算前left-1个数字之和记为f(left-1),再同理计算前right个数字之和记为f(right),最后答案就是f(right) - f(left-1)

	<h5 id="1">flag</h5>

计算第k小的数字时候构造以1开始的前缀和数组sums,数组大小为n+1,我们实际有意义的从1开始,数组的第0个初始化为0,这样就不需要构建整个二位数组了,而计算第2行的时候,发现第2行的每列数字对应上一行相应列的数字只是少了sums[1],第三行相比第一行来说就是少了sums[2],所以只需用第一行的数字依次减去sums[i]就是第i行的各数,比如第二行的5就等于sums[3] - sums[1] = 6 - 1

/**
* 获取小于mid的数的个数
* @param sums 原数组的前缀和
* @param n 原数组的大小
* @param mid 二分法中的当前mid
* @return 返回严格小于mid数的个数
*/
private int getCnt (long[] sums, int n, int mid) {
int res = 0; //返回的个数
for (int i = 0, p = 1; i < n; i++) {
while (p <= n && sums[p] - sums[i] <= mid) {
p++;
}
//因为每次符合都对p++,所以当最后一次符合条件后也对p进行了加1操作,而加1后p已经指向了最后一个符合条件的下一个数,所以还要给p-1
res += p-1-i;
}
return res;
} /**
* 利用二分查找获取第k小的数
* @param sums 原数组的前缀和
* @param n 原数组的大小
* @param k 第k小
* @return 返回第k小的数
*/
private int getKth (long[] sums, int n, int k) { int left = 0, right = Integer.MAX_VALUE; //二分查找指示左右的两个指针
while (left < right) {
int mid = left + ((right-left) >> 1);
if (getCnt(sums, n, mid) >= k) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}

​ 我们设计一个getSum(k)这个函数,就是上述的f函数,用来计算前k小的数字之和,计算时我们使用前缀和数组,并构造一个前缀和的前缀和数组,如下示例:

​ 此时我们已经得到了第k小的数字,要计算前k小的所有数字之和,考虑到第k小的数字会有重复大小的数字,所以分开计算,明确一点:我们已经得到第k小个数字,假设为6,前k小的数字为1,2,3,6,6,6,可能后面还有几个6,不过由于k个数的限制,并不纳入计算,所以我们先计算严格小于6的数字之和以及这些数字的个数记为cnt,然后加上(k-cnt) * 6

​ 我们构造出了前缀和数组sums和前缀和的前缀和数组ssums

​ 这样以来如果我们要计算第1行的sums[2]+sums[3]的和,由于ssums[3] = sums[1] + sums[2] + sums[3],而ssums[1] = sums[1],所以sums[2] + sums[3] = ssums[3] - ssums[1]

​ 但是,我们如果要计算第2行的2+5要如何计算呢,通过前面的发现,2比上一行的3少一个1,5比上一上的6少个1,所以就等于ssums[3] - ssums[1] - 2*1,其实整个第2行都会比第1行少1,而第i行会比第1行少nums[i]

​ 因此对于连续非空子数组的和构成的数组我们要求所有严格小于第k小的数(记为kth)的和,遍历每一行,每行都是从小到大递增,当找到此行比kth小的最后一个数后,只需要根据前缀和的前缀和数组就可在O(1)的时间里算出来,假设第i行的第p列是此行最后一个小于kth的数,则此行小于kth的数字和为ssums[p] - ssum[i] - (p-i)*nums[i]

class Solution {
final int MODULO = 1000000007;
//二分+双指针 /**
* 获取小于mid的数的个数
* @param sums 原数组的前缀和
* @param n 原数组的大小
* @param mid 二分法中的当前mid
* @return 返回严格小于mid数的个数
*/
private int getCnt (long[] sums, int n, int mid) {
int res = 0; //返回的个数
for (int i = 0, p = 1; i < n; i++) {
while (p <= n && sums[p] - sums[i] <= mid) {
p++;
}
//因为每次符合都对p++,所以当最后一次符合条件后也对p进行了加1操作,而加1后p已经指向了最后一个符合条件的下一个数,所以还要给p-1
res += p-1-i;
}
return res;
} /**
* 利用二分查找获取第k小的数
* @param sums 原数组的前缀和
* @param n 原数组的大小
* @param k 第k小
* @return 返回第k小的数
*/
private int getKth (long[] sums, int n, int k) { int left = 0, right = Integer.MAX_VALUE; //二分查找指示左右的两个指针
while (left < right) {
int mid = left + ((right-left) >> 1);
if (getCnt(sums, n, mid) >= k) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
} /**
* 获取前k小的数的和
* @param sums 原数组的前缀和
* @param ssums 原数组前缀和的前缀和
* @param n 原数组大小
* @param k k
* @return 返回前k小的数字之和
*/
private long getSum (long[] sums, long[] ssums, int n, int k) {
long res = 0, cnt = 0;
long kth = getKth(sums, n, k); //第k小的数字
//分两部分计算,考虑到有的数字会重复,所以先计算严格小于kth的数字的和与个数cnt,在加上剩余k-cnt个第k小的数字
for (int i = 0, p = 1; i < n; i++) {
while (p<=n && sums[p]-sums[i] < kth) {
p++;
}
res = (res + ssums[p-1] - ssums[i] - (long)(p-1-i)*sums[i]);
cnt += p-1-i;
}
return (res + (k-cnt)*kth);
} /**
* 计算
* @param nums
* @param n
* @param left
* @param right
* @return
*/
public int rangeSum (int[] nums, int n, int left, int right) {
long[] sums = new long[n+1];
long[] ssums = new long[n+1]; for (int i = 1; i <= n; i++) {
sums[i] = sums[i-1]+nums[i-1];
ssums[i] = ssums[i-1]+sums[i];
}
long r = getSum(sums, ssums, n, right);
long l = getSum(sums, ssums, n, left-1);
return (int) ((r-l)%MODULO); }
}

<二分查找+双指针+前缀和>解决子数组和排序后的区间和的更多相关文章

  1. [LeetCode] #167# Two Sum II : 数组/二分查找/双指针

    一. 题目 1. Two Sum II Given an array of integers that is already sorted in ascending order, find two n ...

  2. [LeetCode] #1# Two Sum : 数组/哈希表/二分查找/双指针

    一. 题目 1. Two SumTotal Accepted: 241484 Total Submissions: 1005339 Difficulty: Easy Given an array of ...

  3. 【二分查找】 跳石头NOIP2015提高组 D2T1

    [二分查找]跳石头NOIP2015提高组 D2T1 >>>>题目 [题目描述] 一年一度的“跳石头”比赛又要开始了! 这项比赛将在一条笔直的河道中进行,河道中分布着一些巨大岩石 ...

  4. cb16a_c++_顺序容器的选用_排序_二分查找

    /*cb16a_c++_顺序容器的选用_排序_二分查找顺序容器: 1.vector的优点与缺点 vector优点:排序利用下标,快速排序,做二分查找非常快 2.list的优点与缺点 list优点:插入 ...

  5. 51 nod 1624 取余最长路 思路:前缀和 + STL(set)二分查找

    题目: 写这题花了我一上午时间. 下面是本人(zhangjiuding)的思考过程: 首先想到的是三行,每一行一定要走到. 大概是这样一张图 每一行长度最少为1.即第一行(i -1) >= 1, ...

  6. java 实现二分查找法

    /** * 二分查找又称折半查找,它是一种效率较高的查找方法. [二分查找要求]:1.必须采用顺序存储结构 2.必须按关键字大小有序排列. * @author Administrator * */ p ...

  7. 二分查找(Binary Search)的基本实现

    关于二分查找法二分查找法主要是解决在"一堆数中找出指定的数"这类问题. 而想要应用二分查找法,这"一堆数"必须有一下特征: 1,存储在数组中2,有序排列 所以如 ...

  8. JS算法之二分查找

    二分查找法主要是解决「在一堆有序的数中找出指定的数」这类问题,不管这些数是一维数组还是 多维数组,只要有序,就可以用二分查找来优化. 二分查找是一种「分治」思想的算法,大概流程如下: 1.数组中排在中 ...

  9. Task 4.5 求二维数组中的最大连通子数组之和

    任务:输入一个二维整形数组,数组里有正数也有负数. 求所有子数组的和的最大值.要求时间复杂度为O(n). 1.设计思想:因为用之前的解决子数组最大和的问题的思路一直没能解决这个问题,后来看到同学使用将 ...

随机推荐

  1. TCP三次握手、四次挥手理解及可能问为什么?

    三次握手:  TCP3次握手连接:浏览器所在的客户机向服务器发出连接请求报文(SYN标志为1),此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态. 服务器接收报文后,同意建立连接, ...

  2. 循序渐进VUE+Element 前端应用开发(20)--- 使用组件封装简化界面代码

    VUE+Element 前端应用,比较不错的一点就是界面组件化,我们可以根据重用的指导方针,把界面内容拆分为各个不同的组合,每一个模块可以是一个组件,也可以是多个组件的综合体,而且这一个过程非常方便. ...

  3. SpringBoot整合MinIO

    今天因为公司的需求接触到这个东西,我们先来看下MinIO的官网简介 MinIO 是一个基于Apache License v2.0开源协议的对象存储服务.它兼容亚马逊S3云存储服务接口,非常适合于存储大 ...

  4. RabbitMQ和Kafka的高可用集群原理

    前言 小伙伴们,通过前边文章的阅读,相信大家已经对RocketMQ的基本原理有了一个比较深入的了解,那么大家对当前比较常用的RabbitMQ和Kafka是不是也有兴趣了解一些呢,了解的多一些也不是坏事 ...

  5. python模块hashlib、xlwt、pymysql

    一.xlwt xlwt是python第三方模块,主要是对excel的写操作.xlwt使用时必须先安装. 1.安装 在操作系统的cmd窗口输入pip install xlwt回车即可在线安装. 安装完成 ...

  6. 将虚拟机IP与主机IP设置在同一网段的方法

    一.查看主机的网卡名称.IP地址.子网掩码 二.设置VMware Workstation软件 打开虚拟网络编辑器 弹出对话框,选择"更改设置"按钮. 进入虚拟网络编辑器 单选项选择 ...

  7. SpringMVC-08-整合SSM之CRUD

    查询书籍功能 完善Controller:BookController @Controller @RequestMapping("/book") public class BookC ...

  8. 浅说iOS二维码的那些事儿

    二维码需要用到 Quartz 2D 一般是三步走~1导入CoreImage框架,编写字符串转二维码图;2渲染二维码;3显示二维码. 导入头文件 #import <CoreImage/CoreIm ...

  9. 第17课 - make 中的路径搜索(上)

    第17课 - make 中的路径搜索(上) 1. 问题 在以往的 make 学习中,我们使用到的 .c 文件和 .h 文件都与 makefile 处在同一个路径.在实际的工程项目中,所有的源文件和头文 ...

  10. Shell编程(3)

    1. shell流程控制 2. for语句 3. while语句 4. break和continue语句 5. case语句 6. shell编程高级实战 shell流程控制 流程控制是改变程序运行顺 ...