一、移项法
●原理:
所谓的移项法,就是将目标等式 进行左右等价变化,辅助以哈希表/桶,从而减少类似for(j:for(i))
的双层循环
●效果: 将算法从 $ O(n^2) $ 复杂度降低为 $ O(n) $
前置余数知识
1、( a + b ) % p = a % p + b % p
2、假如( x - y) % p == 0
1.
假设x >= 0,y >= 0
, 则有 x % p == y % p
2.
假设上面的某一方x < 0
,则有x % p + p == y % p
思路
1、利用余数结论,计算前缀和对p的余数 ,则
s[n]
就表示整体sum对p的余数。
2、则问题等价于找到 - 2.1、在前缀和数组上找到两个数
s[left]
和 s[right]
,满足
right−left
最小且(s[right]−s[left]) % p == s[n]
- 2.2、移项 ,s[right]−s[n] % p == s[left]
-
2.3、考虑可能出现正负((s[right] − s[n]) % p + p) % p = s[left]
3、
避免双层遍历找left和right: 使用一个哈希,存储s[i]
出现的最近下标
4、遍历一遍,到了right,就找哈希里面是否存在left满足 =
((s[right] − s[n]) % p + p) % p
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int minSubarray (vector<int > &nums, int p) { int n = nums.size (), ans = n, s[n + 1 ]; s[0 ] = 0 ; for (int i = 0 ; i < n; ++i) s[i + 1 ] = (s[i] + nums[i]) % p; int x = s[n]; if (x == 0 ) return 0 ; unordered_map<int , int > last; for (int i = 0 ; i <= n; ++i) { last[s[i]] = i; auto it = last.find ((s[i] - x + p) % p); if (it != last.end ()) ans = min (ans, i - it->second); } return ans < n ? ans : -1 ; }
思路
1、前缀计数:left1[i]
记录到i位置的字母个数,left2[j]
记录到j位置的数字个数
2、思路1:暴力,$ O(n^2) $
,看看什么位置满足left1[j]-left1[i] == left2[j]-left2[i]
,找到最小的i
3、思路2:移项 ,$ O(n) $
,即找到left1[j]-left2[j] == left1[i]-left2[i]
,那么用map
保存当前差值(left1[i]-left2[i])
出现的最早位置
当然这题对字母、数字建模为-1,1。可以压缩空间到O(n);
且用桶代替map的时间效率更高。 这里重点介绍移项的思想
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 vector<string> findLongestSubarray (vector<string> &array) { unordered_map<int , int > uii; vector<int > left1 (array.size() + 1 , 0 ) ; vector<int > left2 (array.size() + 1 , 0 ) ; vector<string> ansv; int ans = 0 ; pair<int , int > ansp = make_pair (array.size () + 1 , array.size () + 1 ); for (int i = 0 ; i < array.size (); i++) { if ((array[i][0 ] >= 'A' && array[i][0 ] <= 'Z' ) || (array[i][0 ] >= 'a' && array[i][0 ] <= 'z' )) { left1[i + 1 ] = left1[i] + 1 ; left2[i + 1 ] = left2[i]; } else { left1[i + 1 ] = left1[i]; left2[i + 1 ] = left2[i] + 1 ; } } for (int j = 0 ; j <= array.size (); j++) { int tp = left1[j] - left2[j]; if (uii.find (tp) != uii.end ()) { if (j - uii[tp] + 1 > ans) { ansp.first = uii[tp]; ansp.second = j; ans = max (ans, j - uii[tp] + 1 ); } else if (j - uii[tp] + 1 == ans && uii[tp] < ansp.first) { ansp.first = uii[tp]; ansp.second = j; } } else { uii[tp] = j; } } if (ansp.first == array.size () + 1 ) return ansv; for (int i = ansp.first; i < ansp.second; i++) { ansv.push_back (array[i]); cout << array[i] << " " ; } return ansv; }
思路1
0、找到k所在的位置kInd
1、写出目标表达式: : 1 2 3 4 5 6 7 8 9 10 「左侧小于 + 右侧小于 = 左侧大于 + 右侧大于」 「左侧小于 - 左侧大于 = 右侧大于 - 右侧小于」 「左侧小于 + 右侧小于 + 1 = 左侧大于 + 右侧大于」 「左侧小于 - 左侧大于 = 右侧大于 - 右侧小于 - 1 」
2、统计kInd左右侧,每个位置到kInd这段子数组中,小于、大于K值的位置数,分别记为smallK、bigK;
3、用一个哈希记录左侧每个位置的smallK -
bigK位置数、右侧bigK-smallK的位置数,然后同时开第2个维度,记录位置的距离kInd的奇、偶。
4、之后根据不同位置距离kInd的奇数、偶数,可以得到子序列的奇偶,也就可以使用步骤1中的目标表达式,具体见下面代码
思路2:
参考:灵神的神级建模,见原题解
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 int countSubarrays (vector<int > &nums, int k) { int ans = 0 ; int kInd = -1 ; int bigK; int smallK; unordered_map<int , vector<int >> leftH; unordered_map<int , vector<int >> rightH; set<int > hasPos; for (int i = 0 ; i < nums.size (); i++) if (nums[i] == k) kInd = i; int i = kInd - 1 , j = kInd + 1 ; leftH[0 ] = {1 , 0 }; rightH[0 ] = {1 , 0 }; smallK = 0 ; bigK = 0 ; hasPos.insert (0 ); while (i >= 0 ) { if (nums[i] < k) { smallK++; } else { bigK++; } if (leftH.find (smallK - bigK) == leftH.end ()) { vector<int > tp = {0 , 0 }; tp[(kInd - i) % 2 ] = 1 ; leftH[smallK - bigK] = tp; } else { leftH[smallK - bigK][(kInd - i) % 2 ]++; } i--; } smallK = 0 ; bigK = 0 ; while (j < nums.size ()) { if (nums[j] < k) { smallK++; } else { bigK++; } if (rightH.find (bigK - smallK) == rightH.end ()) { vector<int > tp = {0 , 0 }; tp[(j - kInd) % 2 ] = 1 ; rightH[bigK - smallK] = tp; } else { rightH[bigK - smallK][(j - kInd) % 2 ]++; } hasPos.insert (bigK - smallK); j++; } for (auto pos : hasPos) { if (leftH.find (pos) != leftH.end ()) { int rOu = rightH[pos][0 ]; int lOu = leftH[pos][0 ]; int rJi = rightH[pos][1 ]; int lJi = leftH[pos][1 ]; ans += rOu * lOu; ans += rJi * lJi; } if (leftH.find (pos - 1 ) != leftH.end ()) { ans += rightH[pos][0 ] * leftH[pos - 1 ][1 ]; ans += rightH[pos][1 ] * leftH[pos - 1 ][0 ]; } } return ans; }
二、剪枝
●原理:
广义的剪枝法,就是将没有必要的搜索空间直接跳过,常见于递归问题。但要建立一个剪枝的思想,在各类问题中都适用,不局限于递归、搜索树。
●效果: 将算法进行不同程度优化
参考(关注思路2的分类讨论):leetcode官方题解