0%

热题100 – 字串

针对连续子数组问题,通常有两种解法

1、滑动窗口解法,但是该解法只局限于数组中的值全体非负或非正

2、前缀和 ( + 哈希表 )


题源一  560. 和为 K 的子数组 – 力扣(LeetCode)

算法思想: 前缀和 + 哈希表, 具体思路参考  – 前缀和 + 哈希表 |

问题关键在于怎样去统计 i < j , s[j] – s[i] = k 的个数

 

下列各方法时间复杂度均为 O(n) [都需要遍历一遍sum数组]

 

阿Q解决

s[j] = s[i] + k  ->   哈希表键s[i] + k

遍历一遍sum数组,查询当前的sum[i] 是否在哈希表中,并用哈希表记录 sum[i] + k 值的下标位置数组

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        // 前缀和数组
        vector<int> sum;
        sum.emplace_back(0);
        int n = nums.size();

        for(int i = 0; i < n; ++i)
            sum.emplace_back(sum.back() + nums[i]);

        // 找符合条件的子数组:i < j  s[j] - s[i] = k
        // 转换为 s[j] = s[i] + k -  枚举s[i]的值,并存入哈希表中
        unordered_map<int, vector<int>> mp;
        int ans = 0;

        for(int i = 0; i < n + 1; ++i){
            auto left = mp.find(sum[i]);
            if(left != mp.end())
                ans += left->second.size();
            mp[sum[i] + k].emplace_back(i - 1);  

        }
        return ans;
    }
};

/*
    出现的问题:
        在循环中是对sum数组进行相关操作,而非nums
    
    代码思想:
        前缀和解决对应的连续子数组问题
        由于需要满足 i < j, s[j] - s[i] = k, 从左到右遍历sum数组,查找当前的sum值是否在哈希表,并将 s[i] + k 加入哈希表内
*/

 

优化:由于题中不要求返回 子数组,因此哈希表的value 记录 sum[i] + k 的个数

 

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        // 前缀和数组
        vector<int> sum;
        sum.emplace_back(0);
        int n = nums.size();

        for(int i = 0; i < n; ++i)
            sum.emplace_back(sum.back() + nums[i]);

        // 找符合条件的子数组:i < j  s[j] - s[i] = k
        // 转换为 s[j] = s[i] + k -  枚举s[i]的值,并存入哈希表中
        unordered_map<int, int> mp;
        int ans = 0;

        for(int i = 0; i < n + 1; ++i){
            auto left = mp.find(sum[i]);
            if(left != mp.end())
                ans += left->second;
            ++mp[sum[i] + k];  
        }
        return ans;
    }
};

/*
    出现的问题:
        在循环中是对sum数组进行相关操作,而非nums
    
    代码思想:
        前缀和解决对应的连续子数组问题
*/

官解

s[i] = s[j] – k   哈希表键s[i]

一遍枚举j,一边在 哈希表中 查找 s[j] – k 的数量

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        // s[i] = s[j] - k 的方法
        // 两次遍历
        int n = nums.size();
        vector<int> sum(n + 1, 0);

        for(int i = 0; i < n; ++i)
            sum[i + 1] = sum[i] + nums[i];
        
        unordered_map<int, int> mp;
        int ans = 0;

        // 遍历sum数组 并查表
        for(const int& sj: sum){
            ans += (mp.contains(sj - k))? mp[sj - k]: 0;
            // cout << ans << endl;
            ++mp[sj]; 
        }
        return ans;
    }
};

 

 优化:可以将两次遍历合并到一起,一边枚举j,一边查表

 

class Solution {
public:
    int subarraySum(vector<int>& nums, int k) {
        // s[i] = s[j] - k 的方法
        unordered_map<int, int> mp;
        int ans = 0;

        // 一边枚举j,一边查表
        mp[0] = 1;   // 十分重要的初始化,类似于sum[0] = 0
        int sj = 0;

        for(const int& x: nums){
            sj += x;
            ans += (mp.contains(sj - k))?mp[sj - k]:0;
            ++mp[sj];
        }
        return ans;
    }
};

 

题源二  239. 滑动窗口最大值 – 力扣(LeetCode)

方法一   优先队列

class Solution {
public:
    //  优先队列解决
    //  优先队列设计默认最大值置顶
    //  pair比较内部已实现先比较第一个,再比较第二个
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();

        if(k > n) return vector<int>();

        vector<int> ans;

        priority_queue<pair<int, int>> q;

        for(int i = 0; i < n ; ++i){
            q.emplace(nums[i], i);

            // 若优先队列头部元素早已超出滑动窗口,则踢出优先队列
            while(i - q.top().second >= k)  q.pop();

            if(i >= k - 1) 
              ans.emplace_back(q.top().first); 
        }
        return ans;
    }
};

 

方法二   单调队列

思想:“裁员”广进

1、年轻能力不逊于年长员工,裁掉老员工

2、老员工岁数超过阈值,裁掉老员工

class Solution {
public:
    /*
        此题的难点在于
            每一个窗口内查找最值是没什么关系的,如何在`相邻滑动窗口间建立联系`是本题的难点

            使用 单调队列 从而解决问题
            
            思路 (“降本增效”):
                1、若新员工不逊于老员工,则将老员工裁掉
                2、若老员工35岁,也裁掉

    */
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        // 队列中存取下标
        deque<int> deq;

        vector<int> ans;

        int n = nums.size();

        for(int i = 0; i < n; ++i){
            // 入: 若新员工不逊于老员工,则将老员工裁掉
            while(!deq.empty() && nums[deq.back()] <= nums[i])
                deq.pop_back();
            deq.push_back(i);

            // 出:若老员工35岁,也裁掉
            if(i - deq.front() == k) deq.pop_front();

            if(i >= k - 1) ans.emplace_back(nums[deq.front()]);
        }
        return ans;
    }
};

方法三   分块 +  预处理(十分巧妙)

此思想阿Q第一次遇到,分块后的未越界部分由suffixMax决定(suffixMax的每一块都是从从右到左填最值);越界部分由 prefixMax决定(prefixMax的每一块都是从左到右填最值)

最终的每个滑动窗口的最终为  max(suffixMax[i], prefixMax[i + k –  1])

class Solution {
public:
    // 分块 + 预处理
    /*
      有两种情况:

      1、如果 i 是 k 的倍数,那么 nums[i] 到 nums[i+k−1] 恰好是一个分组。我们只要预处理出每个分组中的最大值,即可得到答案;

      2、 如果 i 不是 k 的倍数,那么 nums[i] 到 nums[i+k−1] 会跨越两个分组,占有第一个分组的后缀以及第二个分组的前缀。
          假设 j 是 k 的倍数,并且满足 i<j≤i+k−1,那么 nums[i] 到 nums[j−1] 就是第一个分组的后缀,nums[j] 到 nums[i+k−1] 就是第二个分组的前缀。
          如果我们能够预处理出每个分组中的前缀最大值以及后缀最大值,同样可以在 O(1) 的时间得到答案
               
      【阿Q备注: 存在越界时,prefix处理越界的部分最值,suffix处理未越界部分的最值】
    */
    vector<int> maxSlidingWindow(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> prefixMax(n, 0);  
        vector<int> suffixMax(n, 0); 

        vector<int> ans;

        // prefixMaX预处理
        /* 
            prefixMax[i]有两种情况
            组头: prefixMax[i] = nums[i] 
            非组头:prefixMax[i] = max(prefixMax[i - 1], nums[i])
        */
        for(int i = 0; i < n; ++i){
          if( !(i % k) )  prefixMax[i] = nums[i];
          else prefixMax[i] = max(prefixMax[i - 1], nums[i]);
        }

        // suffixMax预处理
        /*
            suffixMax[i]也有两种情况
            组尾:prefixMax[i] = nums[i]
            非组尾:prefixMax[i] = max(prefixMax[i + 1], nums[i])
        */
        suffixMax[n - 1] = nums[n - 1];
        for(int i = n - 2; i > 0; --i){
            if( !((i + 1) % k) ) suffixMax[i] = nums[i];
            else suffixMax[i] = max(suffixMax[i  + 1], nums[i]);
        }

        // 滑动窗口
        for(int i = 0; i <= n - k; ++i){
            ans.emplace_back(max(suffixMax[i], prefixMax[i + k -1]));
        }
        
        return ans;
    }
};

 

题源三 76. 最小覆盖子串 – 力扣(LeetCode)

阿Q笨重的方法

此题阿Q起初用了十分杂糅的办法(收到滑动窗口解法的启发)

class Solution {
public:
    /*
        思路:
          先遍历一次字符串,将符合要求的字符用pair(char, int) 存入数组中
          然后滑动窗口解决匹配问题,记录各个匹配情况的子串字符数量,最终返回结果
    */
    string minWindow(string s, string t) {
        int tsize = t.size(), ssize = s.size();
        if (tsize > ssize)
            return "";

        // 模式串字符数组记录数量
        vector<int> tcount(128);

        for (int i = 0; i < tsize; ++i)
            ++tcount[t[i]];

        // 遍历字符串, 存入模式串中存在的字符进入数组
        vector<pair<char, int>> t_in_x;

        for (int i = 0; i < ssize; ++i)
            if (tcount[s[i]])
                t_in_x.emplace_back(s[i], i);

        int n = t_in_x.size();

        // 在t_in_x数组中滑动窗口
        // diff 应该存取 t字符串中不同的字符数量
        int diff = 0;
        for(int i = 0; i < 128; ++i)
            if(tcount[i]) ++diff;

        cout << diff << endl;

        int minleft = -1, minright = -1;

        //  取最小值应该先存入最大值
        int minlen = INT_MAX;
        int right = 0;
        for (int left = 0; left < n; ++left) {
            // 右指针加值
            // 从不满足数量条件到满足
            while (diff && right < n) {
                if (tcount[t_in_x[right].first] == 1)
                    --diff;
                --tcount[t_in_x[right++].first];
            }

            if(right >= n && diff) break;

            if (!diff) {
                // 满足条件
                // 从满足条件到不满足条件
                if (tcount[t_in_x[left].first] == 0)
                    ++diff;
                ++tcount[t_in_x[left].first];

                if (minlen > t_in_x[right - 1].second - t_in_x[left].second + 1) {
                    minright = t_in_x[right - 1].second;
                    minleft = t_in_x[left].second;
                    minlen = minright - minleft + 1;
                    cout << minleft << "\t" << minright << "\t" << minlen << endl;
                }
            }
        }

        if (minright == -1)
            return "";
        return s.substr(minleft, minright - minleft + 1);
    }
};

/*
  出现的问题:
    1、diff 值应为 t字符串中不同字符的个数,而非 t.size()
    2、right始终指向的为出错(diff != 0 || right >= n)的下一个位置,因此 t_in_x 取下标 [right - 1] 而非 [right]
*/

此方法用时快要超时了,急需巧妙的方法去解决