当前位置:   article > 正文

算法总结-前缀和问题_解决问题前缀

解决问题前缀

前缀和(preSum)是一种常用的、较为高效的预处理方式,相比于暴力解法,能够有效减低查询的时间复杂度。

1、前缀和算法

前缀和其实就是数组的前n项之和,为了更好的理解和应用它,我们用一个题目来学习前缀和算法。

给你一个数组 nums 。数组「动态和」的计算公式为:runningSum[i] = sum(nums[0]…nums[i]) 。

请返回 nums 的动态和。

示例 1:

输入:nums = [1,2,3,4]
输出:[1,3,6,10]
解释:动态和计算过程为 [1, 1+2, 1+2+3, 1+2+3+4] 。

提示:

  • 1 <= nums.length <= 1000
  • -10^6 <= nums[i] <= 10^6

这是一个典型的前缀和题目。如果使用暴力解法,也可以做出这道题,但需要两个for循环,时间复杂度较大,暴力解法如下:

  1. int* runningSum(int* nums, int numsSize, int* returnSize){
  2. int *arr = malloc(sizeof(int) * (numsSize));
  3. *returnSize = numsSize;
  4. for(int i = 0;i < numsSize;i++) {
  5. int temp = 0;
  6. for(int j = 0;j < i+1;j++) {
  7. temp += nums[j];
  8. }
  9. arr[i] = temp;
  10. }
  11. return arr;
  12. }

如果用前缀和的解法,只需要一次循环,时间复杂度会大大降低。如下。

  1. int* runningSum(int* nums, int numsSize, int* returnSize) {
  2. *returnSize = numsSize;
  3. for (int i = 1; i < numsSize; i++) {
  4. nums[i] += nums[i - 1];
  5. }
  6. return nums;
  7. }

前缀和不仅代码简洁,性能上更是优于暴力解,因此能用前缀和来解决的题目应尽量避免使用暴力解法。

2、前缀和+哈希

前缀和有时会和哈希表一起使用,用来解决例如连续子数组之类的问题。如下问题

leetcode 560.和为k的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回 该数组中和为 k 的子数组的个数 。


示例 1:

输入:nums = [1,2,3], k = 3
输出:2
 

提示:

1 <= nums.length <= 2 * 104
-1000 <= nums[i] <= 1000
-107 <= k <= 107

这类问题很明显可以用前缀和来解答,但只使用前缀和在计算量较大的情况下效率仍然较低,可能会存在超时的问题,这时可以使用哈希表来进行优化。

前缀和问题的关键是找出所有前缀和为k的子数组,这个问题可以转换为”当前前缀和 - 前面前缀和 == k“,即preSum[i]  - preSum[j] = sum[i, j] ,也即preSum[i]  - preSum[j] = k。这样我们就可以用哈希表来存储每一个子数组和,以便于查找。

那么,如果存在preSum[i]  - preSum[j] = k,就可以利用哈希表记录preSum-k的元素,查找并计算preSum-k存在的次数即为目标值的个数。

代码如下:

  1. struct hashtable
  2. {
  3. int key;
  4. int cnt;
  5. UT_hash_handle hh;
  6. };
  7. struct hashtable *g_head = NULL;
  8. int subarraySum(int* nums, int numsSize, int k)
  9. {
  10. g_head = NULL;
  11. struct hashtable *node = (struct hashtable*)malloc(sizeof(struct hashtable));
  12. node->key = 0;
  13. node->cnt = 1;
  14. HASH_ADD_INT(g_head, key, node);
  15. int sum_i = 0, ans = 0;
  16. for (int i = 0; i < numsSize; i++) {
  17. sum_i += nums[i];
  18. int sum_j = sum_i-k;
  19. struct hashtable *s;
  20. HASH_FIND_INT(g_head, &sum_j, s);
  21. if (s != NULL) {
  22. ans += s->cnt;
  23. }
  24. HASH_FIND_INT(g_head, &sum_i, s); //查找当前preSum是否在哈希表,没有则将当前preSum入哈希表
  25. if (s == NULL) {
  26. s = (struct hashtable*)malloc(sizeof(struct hashtable));
  27. s->key = sum_i;
  28. s->cnt = 1;
  29. HASH_ADD_INT(g_head, key, s);
  30. } else {
  31. s->cnt++;
  32. }
  33. }
  34. return ans;
  35. }

这个题目的关键,一是要彻底理解hash表中所存放值的含义;二是要理解返回值为什么是ans += s->cnt;而不是ans++。

我们以一个例子来理解这两个问题,假设数组为[1, 0, 3, 2, 4],所求的目标值为5,那么其前缀和如下(假设下标从1开始):

sum[1] = 1;

sum[2] = 1;

sum[3] = 4;

sum[4] = 6;

sum[5] = 10;

这几个值是要存到hash表中去的,其在hash表中的键值对为(key-value):1-2,4-1,6-1,10-1,我们到hash表中查找时,是按其key值检索的。

那么sum - k分别为:

[-4, -4, -1, 1,5]

这几个值就是我们要在哈希表中查找的,如果存在,那么就是我们的目标值,找到其对应的value即可,value就是其个数。比如以上例子我们的目标值为5,那么sum - k值为1的时候,哈希表中是有对应值的,即键值对1-2,这个就是我们要找的值,其结果为2,此时当前的前缀和为sum[4] = 6,所要减去的前缀和为sum[0]、sum[1](因为两个值都为1)。

对于第二个问题,用一个简单的例子就可以说明。一般情况下,hash表中的键值对都是x-1的形式,但有些特殊情况下例外,如数组为[1, -1, 0, 0],目标值为0的情况,这时

sum[0] = 1;

sum[1] = 0;

sum[2] = 0;

sum[3] = 0;

将各前缀和存到hash表中后,值为0的key,有3个,分别为sum[1]、sum[2]、sum[3],但在哈希表实际上占用一个键值对,即0-3。假如i = 3,那么当前前缀和为sum[3] = 0,以0 - k = 0到哈希表中查找目标值,此时会有两个值满足条件,即sum[1]、sum[2],其逻辑为,对于当前前缀和sum[3],存在前缀和sum[1]、sum[2]使得sum[3] - sum[1]或sum[2],其结果值为k。

这样的话,对于sum[3]而言,满足条件的值就不是一个了,所以计算结果时要用ans += s->cnt,而不是ans++来计算。

总结:要真正理解hash表中存的值是什么,hash表中存的是sum[0] - sum[i]的各个前缀和(不包括中间截取的情况)。而我们要拿去hash表查找的key,其实是sum-k,也就是当前前缀和之前的前缀和(如sum[3]之前的前缀和sum[1]或sum[2]),如果这个值存在hash表中,就说明条件成立。

解决这类问题要注意问题的转变,比如前面题目中我们将前缀和的问题转换为了hash表中寻找sum-k的问题,这时我们的问题已经转变,不再是求子数组的个数,而是求hash表中sum-k的个数,当问题转变时,条件也会转变,切不要将转变后的问题和条件与原来的问题和条件相混淆。

最后还应该释放哈希表。

  1. HASH_ITER(hh, g_head, current, next) {
  2. if (current) {
  3. HASH_DEL(g_head, current);
  4. }
  5. }

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

闽ICP备14008679号