当前位置:   article > 正文

小和问题及扩展

小和问题

一、写作本篇文章的目的

本篇主要讲述如何用递归法求解小和问题,以及小和问题类似的一类问题。

二、小和问题

【题目】求一个数组的小和。

什么是小和?
在一个数组中,每一个数左边比当前数小的数累加起来,叫做这个数组的小和。

【算法分析】

比如想要求解数组[1,3,4,2,5]的小和:

1、根据定义设计的算法

直接根据定义,从左往右遍历,每个数值都求出它的小和,并累加求解:
1左边比1小的数:没有
3左边比3小的数:1
4左边比4小的数:1,3
2左边比2小的数:1
5左边比5小的数:1,3,4,2
所以小和为1+1+3+1+1+3+4+2=16

参考代码C++:

  1. int GetSum(const vector<int>& arr)
  2. {
  3. int sum = 0;
  4. for (int i = 0; i < arr.size(); ++i)
  5. {
  6. int temp = arr[i];
  7. for (int j = 0; j < i; ++j)
  8. {
  9. if (arr[j] < temp)
  10. {
  11. sum += arr[j];
  12. }
  13. }
  14. }
  15. return sum;
  16. }

此算法简单易懂,但是时间复杂度为O(n^2),但这并不是我们想要讲的算法。

2、递归法求解

还是求解数组[1,3,4,2,5]的小和,我们可以换个思路:

1的右侧有4个数比1大:则 sum += 1 * 4

3的右侧有2个数比3大:则 sum += 3 * 2

4的右侧有1个数比4大:则 sum += 4 * 1

2的右侧有1个数比2大:则 sum += 2 * 1

最后sum = 16

为什么可以这样求解?因为我们在求解小和的过程中,根据定义法:如果左侧某数a比右侧某数b小,则在求b的小和的时候,肯定会累加一个a,即sum+=a。反过来,在遍历到a的时候,如果我们知道右侧有几个数比a大,则可以提前知道会累加几个a。

根据描述,这种思路如果还是用遍历法,则还是时间复杂度为O(N^2)的算法。我们需要用更加优质的算法处理这个思路-递归。

算法详述:

我们知道递归方法为一个数组排序只需要O(N*logN)的时间复杂度。而排序有个merge的过程,

左侧:1        右侧:3      merge:   因为1 < 3,        产生1*1=1的小和

左侧:1,3     右侧:4      merge:   因为1<4,     产生1*1=1的小和

                                                        因为3<4,产生3*1=4

左侧:2        右侧:5      merge:   因为2<5,          产生2*1的小和

左侧1,3,4      右侧:2,5  merge:   因为1<2,            产生1*2(右侧>=2的共2个数)= 2 的小和

                                                        因为 3<5,           产生3*1=3 的小和

                                                        因为 4<5,           产生4*1=4 的小和

总小和=16

总体上来说,还是上面的思路,只是把求右侧比某个数大的过程拆分了几步,但是由于比较过程没有重复,甚至由于排序了,还减少了一部分比较,所以是更优的。

比如求比1大的数我们知道有4个,分别是3,4,2,5,而递归时,我们只比较了3,4,2就得出了4个1的结论。

总之这个算法可能不是很好理解。我代码的注释写的很详细,大家可以参考,如果还是不理解可以参考最下面分享的视频讲解。

参考代码(C++实现)

  1. //只有每次merge的时候才会计算小和并且排序
  2. int merge(vector<int>& arr, int L, int R, int M)
  3. {
  4. //创建一个临时的合并数组,临时存放排序后的L--R之间的元素
  5. vector<int> merArr(R - L + 1);
  6. int p1 = L; //“左侧”元素的指针,“左侧”数组范围从L--M
  7. int p2 = M + 1; //“右侧”元素的指针,“右侧”数组范围从M+1--R
  8. int i = 0; //merArr的指针
  9. int sum = 0; //保存此次merge产生的小和
  10. //如果M+1 == p1“左侧”越界;如果R+1 == p2“右侧”越界
  11. while (p1 <= M && p2 <= R )
  12. {
  13. //p1 < p2会产生小和
  14. if (arr[p1] < arr[p2])
  15. {
  16. //因为右侧数组有序,如果arr[p2]比arr[p1]大,则p2位置的所有数据都会比arr[p1]大,则一共会产生R-p2+1个arr[p1]的小和
  17. sum += arr[p1] *(R-p2+1);
  18. //合进排序数组
  19. merArr[i++] = arr[p1++];
  20. }
  21. else
  22. {
  23. //合进排序数组
  24. merArr[i++] = arr[p2++];
  25. }
  26. //merArr[i++] = arr[p1] > arr[p2] ? arr[p2++] : arr[p1++];
  27. }
  28. //如果“右侧”数组已经合完,则不会再产生小和,把所有“左侧”数组合进排序数组
  29. while (p1 <= M)
  30. {
  31. merArr[i++] = arr[p1++];
  32. }
  33. //如果“左侧”数组已经合完,则不会再产生小和,把所有“右侧”数组合进排序数组
  34. while (p2 <= R)
  35. {
  36. merArr[i++] = arr[p2++];
  37. }
  38. //已经排好序,把排序结果反填充进入arr数组
  39. for (int it : merArr)
  40. {
  41. arr[L++] = it;
  42. }
  43. //返回小和
  44. return sum;
  45. }
  46. //排序并且得到阶段性的小和
  47. int Process(vector<int>& arr, int L, int R)
  48. {
  49. if (L == R)
  50. {
  51. return 0;
  52. }
  53. int mid = L + ((R - L) >> 1);
  54. return Process(arr, L, mid) + Process(arr, mid + 1, R) + merge(arr, L, R, mid);
  55. }
  56. //求解小和
  57. int smallSum(vector<int>& arr)
  58. {
  59. if (arr.size() < 2)
  60. {
  61. return 0;
  62. }
  63. return Process(arr, 0, arr.size() - 1);
  64. }

【复杂度分析】

根据master公式,此算法的时间复杂度为 T(N) =  2 * T(N/2)+ O(N)

a=2,b=2,d=1   log(b,a)=0 == d , 则时间复杂度为 O(N*logN)

因为merArr为额外开辟的空间,最大为N,则空间复杂度为O(N)

三、小和问题扩展

小和问题是一个比较经典的问题。我们有一系列问题可以使用上述求小和问题的递归方法求解。

1、逆序对问题

【题目】一个数组中,如果某数A的右侧存在一个数B比A小,即A>B,则{A,B}是一个逆序对。求一个数组中有多少对逆序对(使用递归法求解)。

【分析】同上述小和问题

参考代码

其他函数的实现不需要变,只需要修改merge函数即可

  1. //只有每次merge的时候才会计算逆序个数并且排序
  2. int merge(vector<int>& arr, int L, int R, int M)
  3. {
  4. //创建一个临时的合并数组,临时存放排序后的L--R之间的元素
  5. vector<int> merArr(R - L + 1);
  6. int p1 = L; //“左侧”元素的指针,“左侧”数组范围从L--M
  7. int p2 = M + 1; //“右侧”元素的指针,“右侧”数组范围从M+1--R
  8. int i = 0; //merArr的指针
  9. int sum = 0; //保存逆序对个数
  10. //如果M+1 == p1“左侧”越界;如果R+1 == p2“右侧”越界
  11. while (p1 <= M && p2 <= R )
  12. {
  13. //arr[p1] > arr[p2]会产生逆序
  14. if (arr[p1] > arr[p2])
  15. {
  16. sum += R - p2 + 1; //一旦arr[p2]<arr[p1],则右侧中p2后面的数都小于arr[p1]
  17. //打印逆序对
  18. for (int r = p2; r < R + 1; ++r)
  19. {
  20. cout << arr[p1] << "," << arr[r] << endl;
  21. }
  22. //合进排序数组(降序排列)
  23. merArr[i++] = arr[p1++];
  24. }
  25. else
  26. {
  27. //合进排序数组
  28. merArr[i++] = arr[p2++];
  29. }
  30. //merArr[i++] = arr[p1] > arr[p2] ? arr[p2++] : arr[p1++];
  31. }
  32. //如果“右侧”数组已经合完,则不会再产生小和,把所有“左侧”数组合进排序数组
  33. while (p1 <= M)
  34. {
  35. merArr[i++] = arr[p1++];
  36. }
  37. //如果“左侧”数组已经合完,则不会再产生小和,把所有“右侧”数组合进排序数组
  38. while (p2 <= R)
  39. {
  40. merArr[i++] = arr[p2++];
  41. }
  42. //已经排好序,把排序结果反填充进入arr数组
  43. for (int it : merArr)
  44. {
  45. arr[L++] = it;
  46. }
  47. //返回
  48. return sum;
  49. }

四、总结

个人认为上述小和问题和逆序数问题,具有如下规律:

1、以某个数A为基准,需要在A之后寻找达成某个条件(比A大或比A小)的数;

2、这个A需要整体数组(设为arr)遍历一遍,用代码来说,A的值是这样的 for(auto A : arr)

如果待解问题可以用上诉思路解释,我们就可以考虑是否能够使用递归法求解。

【参考&致谢】

一周刷爆LeetCode,算法大神左神(左程云)耗时100天打造算法与数据结构基础到高级全家桶教程,直击BTAJ等一线大厂必问算法面试题真题详解_哔哩哔哩_bilibiliicon-default.png?t=M276https://www.bilibili.com/video/BV13g41157hK?p=3

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/227467
推荐阅读
相关标签
  

闽ICP备14008679号