代码手记笔录——前缀和

前沿

对于 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;
    }
};
posted @ 2022-08-27 10:20  MasterBean  阅读(0)  评论(0)    收藏  举报