代码手记笔录——前缀和
前沿
对于 O(n^2) 时间/空间复杂度无法解决的情况,而题目又是要求某段连续的满足某种条件的序列,考虑使用【前缀和】/【前缀状态】的思路。
注意: 【和】只是一种统计数值状态,如果是统计频率,考虑使用【状态压缩】。
1371. 每个元音包含偶数次的最长子字符串【前缀和 + 状态压缩 + 位运算】
最直观的思路就是:探索指针 ptr 每移动一次,就将其对应的字符加入统计。然后在统计结果里,遍历所有记录,进行判断。问题:时间复杂度不得超过 O(n^2)。
· 优化思路之一:【空间复杂度】将统计的结果进行状态压缩,这样在统计结果里无需遍历所有记录。 如何压缩状态表征统计结果呢? 根据题意,我们仅需统计字符出现的奇偶次数,偶数次用 0 表示,奇数次用 1 表示。其数值从 0 到 1,又从 1 到 0。这样就是异或位运算。
· 优化思路之二:【时间复杂度】若使用双指针,二层遍历,其时间复杂度是 O(n^2)。我们发现,若中间存在某满足条件的连续子序列,如 arr[i, j-1],则可以通过判断 arr[j] 与 arr[i] 的状态来判断是否满足要求。具体言之,若 arr[i] 的奇偶状态与 arr[j] 的奇偶状态相同,则说明 arr[i:j) 出现了偶数次。
备注:1)确定定义:presum[i] :不考虑第 i 个字符,前 i -1 个的状态;2)初始化:在前缀和模板中,需要对前缀和初始化,并记录于结构体中。preSum=0; um[preSum] = 0;
容易犯错:当移动到第 i 个字符,并更新了前缀和 preSum,计算长度为 i+1 - um[preSum],且更新在 um 的 value 也为 um[preSum] = i+1。参考 [0,i) 与 [0, j) 中间的长度计算。

class Solution {
public:
int findTheLongestSubstring(string s) {
unordered_map<int, int> um; // key:状态,value:包含 s[i]
int n = s.size(), ans = 0;
int preSum = 0; // 使用前缀和+压缩状态(奇偶)的思想,表示第 i 个字符之前的前缀和状态
um[preSum] = 0;
for (int i=0; i<n; ++i) {
char c = s[i];
if (c == 'a')
preSum ^= (1<<0);
else if (c == 'e')
preSum ^= (1<<1);
else if (c == 'i')
preSum ^= (1<<2);
else if (c == 'o')
preSum ^= (1<<3);
else if (c == 'u')
preSum ^= (1<<4);
// 若此状态先前就有维护 ans,不更改 um
if (um.count(preSum) > 0)
ans = max(i - um[preSum] + 1, ans);
else
um[preSum] = i+1;
}
return ans;
}
};
微软笔试题
这道题是 1371. 每个元音包含偶数次的最长子字符串 的变形,统计的是每个字母,而不是元音字母。

class Solution {
public:
int findTheLongestSubstring(string s) {
unordered_map<int, int> um; // key:状态,value:包含 s[i]
int n = s.size(), ans = 0;
int preSum = 0; // 使用前缀和+压缩状态(奇偶)的思想,表示第 i 个字符之前的前缀和状态
um[preSum] = 0;
for (int i=0; i<n; ++i) {
char c = s[i];
int shiftNum = c - 'a';
preSum ^= (1<<shiftNum);
// 若此状态先前就有维护 ans,不更改 um
if (um.count(preSum) > 0)
ans = max(i - um[preSum] + 1, ans);
else
um[preSum] = i+1;
}
return ans;
}
};
543. 二叉树的直径

class Solution {
public:
int maxLen = 0;
int diameterOfBinaryTree(TreeNode* root) {
if (!root)
return 0;
TreeNode* ptr = root;
stack<TreeNode*> stk;
while (ptr || !stk.empty()) {
while (ptr) {
int lftMaxLen = getLongestRootLeafPath(ptr->left, 0);
int rgtMaxLen = getLongestRootLeafPath(ptr->right, 0);
int diameter = lftMaxLen + 1 + rgtMaxLen;
maxLen = max(maxLen, diameter);
stk.push(ptr);
ptr = ptr->left;
}
ptr = stk.top();
stk.pop();
ptr = ptr->right;
}
return maxLen-1;
}
int getLongestRootLeafPath(TreeNode* root, int len) {
if (!root)
return len;
len += 1;
int lftLen = getLongestRootLeafPath(root->left, len);
int rgtLen = getLongestRootLeafPath(root->right, len);
return max(lftLen, rgtLen);
}
};
437. 路径总和 III
重点:um 的 key 是前缀和数值,val 是当前路径,前缀和为 key 的路径个数。当 um 存在 preSum[curIdx] - targetNum 时,说明存在 val 个以 Node[curIdx] 结尾的满足条件的路径【是 + val 而不是 + 1】。回溯时,需要将 preSum - 1。

class Solution {
public:
unordered_map<long long, int> um;
int cnt = 0;
int pathSum(TreeNode* root, int targetSum) {
if (!root)
return 0;
long long preSum = 0;
um[preSum] = 1;
dfs(root, 0, targetSum);
return cnt;
}
void dfs(TreeNode* ptr, long long preSum, int targetSum) {
if (!ptr)
return;
preSum += ptr->val;
if (um.count(preSum - targetSum) > 0) {
cnt += um[preSum - targetSum];
}
++um[preSum];
dfs(ptr->left, preSum, targetSum);
dfs(ptr->right, preSum, targetSum);
--um[preSum];
}
};
560. 和为 K 的子数组

class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
unordered_map<long long, int> um;
long long preSum = 0;
um[preSum] = 1;
int sum = 0;
for (int i=0; i<nums.size(); ++i) {
int curData = nums[i];
preSum += curData;
// 若满足条件,则累加
if (um.count(preSum-k) > 0)
sum += um[preSum-k];
++um[preSum];
}
return sum;
}
};
剑指 Offer II 008. 和大于等于 target 的最短子数组【前缀和技巧!!重做!!】
算法思想:找大于等于,必定用二分查找。
前缀和+二分查找的思想。最开始的想法是,动态维护存放【前缀和:index】的哈希表,在 curIndex 的时候判断是否存在大于等于 target 的数。这种思路,在查找等于 target 的数的时候适用,但无法判断“大于”的情况。
解决办法:以 nums[0] 为起点,记录 nums[0:j] 的前缀和。若我们需要在以 nums[i] (i>0) 为起点的子数组中查找大于等于 target 的数,一个技巧是将 target 增大 preSum[i-1]。
技巧重点:preSum 的长度设计为 n + 1。
for (int i=1; i<preSum.size(); ++i) {
int checkTarget = target + preSum[i-1]; // 重点
...
}

class Solution {
public:
vector<int> preSum;
int minSubArrayLen(int target, vector<int>& nums) {
int n = nums.size(), ans = INT_MAX;
preSum.resize(n+1, 0);
for (int i=0; i<n; ++i) {
int data = nums[i];
preSum[i+1] = preSum[i] + data;
}
// 从 nums[i] 开始计算的前缀和中,查找 target
for (int i=1; i<preSum.size(); ++i) {
int checkTarget = target + preSum[i-1]; // 重点
int index = firstGtEq(preSum, checkTarget);
if (index != -1) {
ans = min(ans, index - (i-1));
}
}
return ans == INT_MAX ? 0 : ans;
}
int firstGtEq(vector<int> &preSum, int target) {
int lft = 0, rgt = preSum.size() - 1, mid;
while (lft <= rgt) {
mid = rgt - ((rgt-lft)>>1);
if (preSum[mid] >= target)
rgt = mid - 1;
else
lft = mid + 1;
}
return lft >= preSum.size() ? -1 : lft;
}
};

浙公网安备 33010602011771号