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

题目重现:

给你一个数组 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个。

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

前置讨论

​ 讨论二分查找+双指针解法前,先看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小的数。

  1. private static int kthSmallest(int[][] matrix, int k) {
  2. int n = matrix.length;
  3. int left = matrix[0][0];
  4. int right = matrix[n-1][n-1];
  5. int mid;
  6. //二分查找,找到第k小的数
  7. while (left < right) {
  8. mid = left + ((right-left) >> 1);
  9. if (check(matrix,mid,k,n)) {
  10. right = mid;
  11. } else {
  12. left = mid + 1;
  13. }
  14. }
  15. return left;
  16. }
  17. //利用双指针检查当前mid是否过大(即是否在数组matrix中比mid大的数超过了k个)
  18. private static boolean check (int[][] matrix, int mid, int k, int n) {
  19. int i = n-1; //指示行坐标
  20. int j = 0; //指示列坐标
  21. int num = 0;
  22. while (i >= 0 && j < n) {
  23. if (matrix[i][j] <= mid) {
  24. j++;
  25. num += (i+1);
  26. } else {
  27. i--;
  28. }
  29. }
  30. return num>=k;
  31. }

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)

  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

  1. /**
  2. * 获取小于mid的数的个数
  3. * @param sums 原数组的前缀和
  4. * @param n 原数组的大小
  5. * @param mid 二分法中的当前mid
  6. * @return 返回严格小于mid数的个数
  7. */
  8. private int getCnt (long[] sums, int n, int mid) {
  9. int res = 0; //返回的个数
  10. for (int i = 0, p = 1; i < n; i++) {
  11. while (p <= n && sums[p] - sums[i] <= mid) {
  12. p++;
  13. }
  14. //因为每次符合都对p++,所以当最后一次符合条件后也对p进行了加1操作,而加1后p已经指向了最后一个符合条件的下一个数,所以还要给p-1
  15. res += p-1-i;
  16. }
  17. return res;
  18. }
  19. /**
  20. * 利用二分查找获取第k小的数
  21. * @param sums 原数组的前缀和
  22. * @param n 原数组的大小
  23. * @param k 第k小
  24. * @return 返回第k小的数
  25. */
  26. private int getKth (long[] sums, int n, int k) {
  27. int left = 0, right = Integer.MAX_VALUE; //二分查找指示左右的两个指针
  28. while (left < right) {
  29. int mid = left + ((right-left) >> 1);
  30. if (getCnt(sums, n, mid) >= k) {
  31. right = mid;
  32. } else {
  33. left = mid + 1;
  34. }
  35. }
  36. return left;
  37. }

​ 我们设计一个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]

  1. class Solution {
  2. final int MODULO = 1000000007;
  3. //二分+双指针
  4. /**
  5. * 获取小于mid的数的个数
  6. * @param sums 原数组的前缀和
  7. * @param n 原数组的大小
  8. * @param mid 二分法中的当前mid
  9. * @return 返回严格小于mid数的个数
  10. */
  11. private int getCnt (long[] sums, int n, int mid) {
  12. int res = 0; //返回的个数
  13. for (int i = 0, p = 1; i < n; i++) {
  14. while (p <= n && sums[p] - sums[i] <= mid) {
  15. p++;
  16. }
  17. //因为每次符合都对p++,所以当最后一次符合条件后也对p进行了加1操作,而加1后p已经指向了最后一个符合条件的下一个数,所以还要给p-1
  18. res += p-1-i;
  19. }
  20. return res;
  21. }
  22. /**
  23. * 利用二分查找获取第k小的数
  24. * @param sums 原数组的前缀和
  25. * @param n 原数组的大小
  26. * @param k 第k小
  27. * @return 返回第k小的数
  28. */
  29. private int getKth (long[] sums, int n, int k) {
  30. int left = 0, right = Integer.MAX_VALUE; //二分查找指示左右的两个指针
  31. while (left < right) {
  32. int mid = left + ((right-left) >> 1);
  33. if (getCnt(sums, n, mid) >= k) {
  34. right = mid;
  35. } else {
  36. left = mid + 1;
  37. }
  38. }
  39. return left;
  40. }
  41. /**
  42. * 获取前k小的数的和
  43. * @param sums 原数组的前缀和
  44. * @param ssums 原数组前缀和的前缀和
  45. * @param n 原数组大小
  46. * @param k k
  47. * @return 返回前k小的数字之和
  48. */
  49. private long getSum (long[] sums, long[] ssums, int n, int k) {
  50. long res = 0, cnt = 0;
  51. long kth = getKth(sums, n, k); //第k小的数字
  52. //分两部分计算,考虑到有的数字会重复,所以先计算严格小于kth的数字的和与个数cnt,在加上剩余k-cnt个第k小的数字
  53. for (int i = 0, p = 1; i < n; i++) {
  54. while (p<=n && sums[p]-sums[i] < kth) {
  55. p++;
  56. }
  57. res = (res + ssums[p-1] - ssums[i] - (long)(p-1-i)*sums[i]);
  58. cnt += p-1-i;
  59. }
  60. return (res + (k-cnt)*kth);
  61. }
  62. /**
  63. * 计算
  64. * @param nums
  65. * @param n
  66. * @param left
  67. * @param right
  68. * @return
  69. */
  70. public int rangeSum (int[] nums, int n, int left, int right) {
  71. long[] sums = new long[n+1];
  72. long[] ssums = new long[n+1];
  73. for (int i = 1; i <= n; i++) {
  74. sums[i] = sums[i-1]+nums[i-1];
  75. ssums[i] = ssums[i-1]+sums[i];
  76. }
  77. long r = getSum(sums, ssums, n, right);
  78. long l = getSum(sums, ssums, n, left-1);
  79. return (int) ((r-l)%MODULO);
  80. }
  81. }

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

  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. Sympy解方程-求极限-微分-积分-矩阵运算

    简介 Sympy是一个Python的科学计算库,用一套强大的符号计算体系完成诸如多项式求值.求极限.解方程.求积分.微分方程.级数展开.矩阵运算等等计算问题.虽然Matlab的类似科学计算能力也很强大 ...

  2. webstorm单标签设置成双标签展开解决iview中col展开问题

    大家好!我是木瓜太香,今天给大家带来一个 webstorm 小技巧 场景:有使用过 vue 框架并且使用 iview 做 ui webstorm 做 ide 的同学,可能会遇到一个比较奇怪的问题,iv ...

  3. 【小程序】---- 封装Echarts公共组件,遍历图表实现多个饼图

    一.问题描述: 在小程序的项目中,封装公共的饼图组件,并在需要的页面引入使用.要求一个页面中有多个饼图,动态渲染不同的数据. 二.效果实现: 1. 查看——小程序使用Echarts的方式 2. 封装饼 ...

  4. Django进入监听端口就自动打开指定页面,无需导航栏手动添加(Django六)

    在我们进入监听端口时画面如下:而因为在urls.py中写过如下语句 我们在监听端口后加上/login就会跳转到login.html页面,如下图 那么如何一打开监听端口就可以走动跳转到login.htm ...

  5. 分布式系统监视zabbix讲解八之自动发现/自动注册

    自动发现(LLD) 概述 自动发现(LLD)提供了一种在计算机上为不同实体自动创建监控项,触发器和图形的方法.例如,Zabbix可以在你的机器上自动开始监控文件系统或网络接口,而无需为每个文件系统或网 ...

  6. 解Bug之路-记一次对端机器宕机后的tcp行为

    解Bug之路-记一次对端机器宕机后的tcp行为 前言 机器一般过质保之后,就会因为各种各样的问题而宕机.而这一次的宕机,让笔者观察到了平常观察不到的tcp在对端宕机情况下的行为.经过详细跟踪分析原因之 ...

  7. 获取NX装配结构信息

    最近在做一个项目,需要获取NX装配结构信息,这里把代码分享给大家,希望对各位有帮助,注意以下几点: 1)代码获取了PART的属性.表达式等,因此一些细节可能需要您根据实际情况修改. 2)读写XML用的 ...

  8. 原生JavaScript封装的jsonp跨域请求

    原生JavaScript封装的jsonp跨域请求 <!DOCTYPE html> <html lang="en"> <head> <met ...

  9. 推荐一款轻量小众却高效免费开源windows热键脚本语言Autohotkey

    写在前面的话 Autohotkey是一款轻量小众但高效免费开源的windows热键脚本语言,游戏操纵.鼠标操作.键盘快捷重定义,快捷短语等等,只有你想不到,没有它做不到,神器中的神器呀,相见恨晚. 安 ...

  10. Python练习题 003:完全平方数

    [Python练习题 003]一个整数,它加上100后是一个完全平方数,再加上168又是一个完全平方数,请问该数是多少? --------------------------------------- ...