D356周赛复盘:滑动窗口+三元问题思路

文章目录

    • 2798.满足目标工作时长的员工数目
      • 完整版
    • 2799.统计完全子数组的数目(滑动窗口)
      • 思路
      • 完整版
    • 2800.包含三个字符的最短字符串(复用思路与三元问题思想)
      • 思路
        • 复用减少字符串长度的思路
        • 为什么一次性操作两个字符串
      • 完整版
        • 进一步理解”三元问题一次只操作两个字符串“
        • 复用逻辑

2798.满足目标工作时长的员工数目

公司里共有 n 名员工,按从 0n - 1 编号。每个员工 i 已经在公司工作了 hours[i] 小时。

公司要求每位员工工作 至少 target 小时。

给你一个下标从 0 开始、长度为 n 的非负整数数组 hours 和一个非负整数 target

请你用整数表示并返回工作至少 target 小时的员工数。

示例 1:

输入:hours = [0,1,2,3,4], target = 2
输出:3
解释:公司要求每位员工工作至少 2 小时。
- 员工 0 工作 0 小时,不满足要求。
- 员工 1 工作 1 小时,不满足要求。
- 员工 2 工作 2 小时,满足要求。
- 员工 3 工作 3 小时,满足要求。
- 员工 4 工作 4 小时,满足要求。
共有 3 位满足要求的员工。

示例 2:

输入:hours = [5,1,4,2,2], target = 6
输出:0
解释:公司要求每位员工工作至少 6 小时。
共有 0 位满足要求的员工。

提示:

  • 1 <= n == hours.length <= 50
  • 0 <= hours[i], target <= 105

完整版

  • 简单模拟题,计数即可
class Solution {
public:
    int numberOfEmployeesWhoMetTarget(vector<int>& hours, int target) {
        int count = 0;
        for(int i=0; i<hours.size(); i++) {
            if(hours[i] >= target)
                count++;
        }
        return count;
    }
};

2799.统计完全子数组的数目(滑动窗口)

给你一个由 整数组成的数组 nums

如果数组中的某个子数组满足下述条件,则称之为 完全子数组

  • 子数组中 不同 元素的数目等于整个数组不同元素的数目。

返回数组中 完全子数组 的数目。

子数组 是数组中的一个连续非空序列。

示例 1:

输入:nums = [1,3,1,2,2]
输出:4
解释:完全子数组有:[1,3,1,2][1,3,1,2,2][3,1,2][3,1,2,2]

示例 2:

输入:nums = [5,5,5,5]
输出:10
解释:数组仅由整数 5 组成,所以任意子数组都满足完全子数组的条件。子数组的总数为 10

提示:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= 2000

思路

首先我们需要知道有多少种不同的元素,然后使用滑动窗口的方式,一直扩大右边界直到窗口内的元素种类与原数组相同,此时缩小左边界,每次缩小左边界,都可以对答案进行累加,因为滑动窗口内的子数组都是满足条件的。

注意,我们为了统计所有满足条件的子数组个数,滑动窗口的left左移需要同时统计map里面元素的个数

完整版

class Solution {
public:
    int countCompleteSubarrays(vector<int>& nums) {
        int n=nums.size();
        unordered_map<int,int>count;
        unordered_set<int>uniqueNums;
        //先得到所有元素的种类数目
        for(int num:nums){
            uniqueNums.insert(num);//uset插入元素只能用insert
            //count[num]++;count必须在窗口内部更新
        }
        int result=0;
        int left=0;
        for(int i=0;i<nums.size();i++){
            count[nums[i]]++;//nums[i]累积数目
            //元素种类相同,缩小左边界
            while(count.size()==uniqueNums.size()){
                result+=nums.size()-1-i+1;//包含所有的子数组情况
                count[nums[left]]--;
                if(count[nums[left]]==0){
                    count.erase(nums[left]);
                }
                left++;
            }
            //右边界i++包含在for循环里面了
        }
        return result;
    }
};

2800.包含三个字符的最短字符串(复用思路与三元问题思想)

给你三个字符串 abc , 你的任务是找到长度 最短 的字符串,且这三个字符串都是它的 子字符串

如果有多个这样的字符串,请你返回 字典序最小 的一个。

请你返回满足题目要求的字符串。

注意:

  • 两个长度相同的字符串 ab ,如果在第一个不相同的字符处,a 的字母在字母表中比 b 的字母 靠前 ,那么字符串 a 比字符串 b 字典序小
  • 子字符串 是一个字符串中一段连续的字符序列。

示例 1:

输入:a = "abc", b = "bca", c = "aaa"
输出:"aaabca"
解释:字符串 "aaabca" 包含所有三个字符串:a = ans[2...4] ,b = ans[3..5] ,c = ans[0..2] 。结果字符串的长度至少为 6 ,且"aaabca" 是字典序最小的一个。

示例 2:

输入:a = "ab", b = "ba", c = "aba"
输出:"aba"
解释:字符串 "aba" 包含所有三个字符串:a = ans[0..1] ,b = ans[1..2] ,c = ans[0..2] 。由于 c 的长度为 3 ,结果字符串的长度至少为 3"aba" 是字典序最小的一个。

提示:

  • 1 <= a.length, b.length, c.length <= 100
  • abc 只包含小写英文字母。

思路

本题的思路是枚举abc三个字符串所有的拼接情况,一边枚举一边进行字母的”复用

我们的目的是找到一个最短的字符串t,它既包含了a,也包含了b。为了得到最短字符串,需要使用尽可能少的b的字符

复用减少字符串长度的思路

复用这一步的核心思想就是,如果ab有重叠(也就是可以复用)的部分,那么这个重叠部分肯定出现在a的末尾和b的开头。因此,对于字符串b,我们可以选择倒着拼接,尝试找到这个b开头和a的重叠部分,以减少最终生成的字符串t的长度。

如果我们正着拼接,即从b的开头开始,那么这个重叠部分可能无法有效复用。例如,如果a=“abc”,b=“bcd”,那么正着拼接的结果可能是"abc"(不使用b的任何字符),而实际上我们可以通过使用b的一个字符得到更短的结果"abcd"。所以,我们选择b的末尾开始倒着拼接,检查倒着拼接从不使用b的任何字符,到使用b的所有字符这个范围内,能不能通过复用b开头和a末尾重叠的部分,来使得原字符串包含b

为什么一次性操作两个字符串

为什么只能一次性合并两个字符串,而不是一口气把三个字符串全处理完?

这是因为我们需要处理的问题是一个三元问题(有三个字符串需要合并)。当我们一次处理两个字符串时,我们可以将问题简化为两个二元问题,这使得问题更容易处理。一次处理两个字符串,我们可以找到一个字符串,它包含了这两个字符串,然后我们再用这个字符串和第三个字符串进行合并,这样问题就被简化了。

可以在代码中进行进一步的理解。

完整版

class Solution {
public:
    string minimumString(string a, string b, string c) {
        // 创建一个 vector 来存储所有可能的字符串组合
        vector<string> combinations;

        // 生成所有可能的字符串组合
        combinations.push_back(combine(a, b, c));
        combinations.push_back(combine(a, c, b));
        combinations.push_back(combine(b, a, c));
        combinations.push_back(combine(b, c, a));
        combinations.push_back(combine(c, a, b));
        combinations.push_back(combine(c, b, a));

        // 对字符串组合进行排序,首先比较长度,如果长度相同,则按字典序排序
        sort(combinations.begin(), combinations.end(), [](string &s1, string &s2) {
            if (s1.size() == s2.size()) return s1 < s2;
            return s1.size() < s2.size();
        });

        // 返回最短的字符串组合
        return combinations.front();
    }

    // 生成一个既包含 a 又包含 b 的字符串,然后将这个字符串和 c 组合,生成一个既包含 a, b, c 的字符串
    string combine(string a, string b, string c) {
        return combine2(combine2(a, b), c);
    }

    // 生成一个既包含 a 又包含 b 的字符串
    string combine2(string a, string b) {
        // 从使用 b 的所有字符开始,逐步减少使用的字符数量
        for (int i = 0; i <= b.size(); ++i) {
            // 拼接 a 和 b 的后缀,生成一个新的字符串
            string t = a + b.substr(b.size() - i);
            // 如果这个新的字符串包含 b,则它一定也包含 a,所以返回这个字符串
            if (t.find(b) != string::npos) return t;  
        }
        // 如果 a 和 b 完全不重叠,则返回 a 和 b 的拼接
        return "";  
    }
};

进一步理解”三元问题一次只操作两个字符串“

combine2函数的作用是生成一个同时包含两个输入字符串ab的最小字符串combine函数则是用于生成同时包含abc三个输入字符串的最小字符串。为了实现这个目标,我们需要两次调用combine2函数,先将ab合并,然后将合并结果与c进行合并。为了避免代码冗余,我们将combine2的功能单独作为一个函数实现。

如果将combine1和combine2两个函数合并,也就是一次性处理三个字符串,会导致代码重复。比如,需要写两次类似的循环来分别处理三个字符串的组合,这会使代码变得复杂且难以维护。另一方面,通过将combine2函数单独实现,我们可以更清晰地表达出代码的逻辑,并使得代码更易于阅读和理解。

比如,如果真的要一次性处理三个字符串,我们只能这么写:

string combine(string a, string b, string c = "") {
    // 内部函数,用于合并两个字符串
    auto merge = [&](string s1, string s2) {
        for (int i = 0; i <= s2.size(); ++i) {
            string t = s1 + s2.substr(s2.size() - i);
            if (t.find(s2) != string::npos) return t;  
        }
        return ""; 
    };
    
    // 首先合并a和b
    string ab = merge(a, b);
    // 如果c为空,直接返回ab
    if (c == "") return ab;
    // 否则,继续合并ab和c
    return merge(ab, c);
}

在这个版本中,使用了一个Lambda表达式来处理两个字符串的合并,然后根据 c 是否为空来判断是否需要进一步合并。但是这个版本的代码显然更复杂,因为我们需要在函数内部再定义一个函数,并且引入了一个新的条件判断。所以从代码的清晰性和可维护性角度来说,原始的设计(将 combine2 函数单独分出来)更好。

复用逻辑

复用部分的代码:

    // 生成一个既包含 a 又包含 b 的字符串
    string combine2(string a, string b) {
        // 从使用 b 的所有字符开始,逐步减少使用的字符数量
        for (int i = 0; i <= b.size(); ++i) {
            // 拼接 a 和 b 的后缀,生成一个新的字符串
            string t = a + b.substr(b.size() - i);
            // 如果这个新的字符串包含 b,则它一定也包含 a,所以返回这个字符串
            // 如果 a 和 b 完全不重叠,则返回 a 和 b 的拼接
            if (t.find(b) != string::npos) return t;  
        }
        //随意返回一个数值即可
        return "";  
    }

这段复用逻辑,实际上是在判断,从不包含b任何字符,到包含b所有字符这个范围内,b开头的元素能不能和a末尾的元素实现复用。一旦能够复用(在倒着拼接的新字符串t里出现了完整的b),那么就可以直接返回t。

例子:a=“abbc” b=“bb”,那么最开始string t = a + b.substr(b.size() - i);,也就是t=a+b.substr(2),实际上此时b.substr(2)是空的,也就是最开始是在判断a里面是不是本来就包含了b

你可能感兴趣的:(leetcode周赛复盘,c++,leetcode)