六月集训代码打卡---DAY6【滑动窗口】

前言

        来自 英雄哪里出来 的一个 免费 集训,每天 5 5 5 点打卡学习算法(我是为了卷吗,主要是想早起 😏),希望能坚持下去。这里用来复盘每天都的打卡题目。
       今日份知识点:滑动窗口
       今天的思维强度上去了,除了第三题形象一点,其他几道题都难在想思路上了,都比较巧妙(窝感觉)。

一、题目

题目难度
1984. 学生分数的最小差值⭐️
1763. 最长的美好子字符串⭐️⭐️
2269. 找到一个数字的 K 美丽值⭐️
995. K 连续位的最小翻转次数⭐️⭐️⭐️

二、算法思路

1、学生分数的最小差值

        (1)排序:题目所求为任意 k k k 个学生的最高分和最低分之差,那么答案必然存在于某个连续段之中,如果最佳 k k k 个选择不是连续段,那么可以调整为连续段之后,结果不会变差。
        (2)二分:对于某个连续段,最高分与最低分之差为 n u m s [ i + k − 1 ] − n u m s [ i ] nums[i+k-1]-nums[i] nums[i+k1]nums[i],最大值最小化问题直接二分答案。
        时间复杂度: O ( ( n + 1 ) log ⁡ n ) O((n+1)\log{n}) O((n+1)logn)

class Solution {
public:
    vector<int> nums;
    int k;
    int minimumDifference(vector<int>& _nums, int _k) {
        nums = _nums;
        k = _k;
        sort(nums.begin(), nums.end());
        int l = 0, r = 100010;
        while (l < r) {
            int mid = (l + r) >> 1;
            if (check(mid)) r = mid;
            else l = mid + 1;
        }
        return r;
    }
    bool check(int x) {
        int n = nums.size();
        int ans = nums[n - 1] - nums[0];
        for (int i = 0; i + k - 1 < n && ans > x; ++ i) {
            ans = min(ans, nums[i + k - 1] - nums[i]);
        }
        return ans <= x;
    }
};

        (3)滑动窗口:对排序后的数组,分别取出窗口内的最大值与最小值的下标,最后一次遍历取最大值与最小值之差的最小值。
        时间复杂度: O ( n log ⁡ n + 3 n ) O(n\log{n}+3n) O(nlogn+3n)

class Solution {
public:
    int minimumDifference(vector<int>& nums, int k) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        vector<int> Max, Min, q(n);

        int hh = 0, tt = -1;
        for (int i = 0; i < n; ++ i) {
            if (hh < tt && i - k + 1 > q[hh]) ++ hh;
            while (hh <= tt && nums[i] >= nums[q[tt]]) -- tt;
            q[++ tt] = i;
            if (i >= k - 1) Max.push_back(q[hh]);
        }

        hh = 0, tt = -1;
        q.clear();
        for (int i = 0; i < n; ++ i) {
            if (hh < tt && i - k + 1 > q[hh]) ++ hh;
            while (hh <= tt && nums[i] <= nums[q[tt]]) -- tt;
            q[++ tt] = i;
            if (i >= k - 1) Min.push_back(q[hh]);
        }

        int ret = 1e9;
        for (int i = 0; i < Max.size(); ++ i) {
            ret = min(ret, nums[Max[i]] - nums[Min[i]]);
        }
        return ret;
    }
};

        (4)直接取结果:排序完之后,每个窗口内的值都是一个连续段,然后这个连续段内的最大值与最小值之差为 n u m s [ i + k − 1 ] − n u m s [ i ] nums[i+k-1]-nums[i] nums[i+k1]nums[i],遍历数组,求出答案。
        时间复杂度: O ( n log ⁡ n + n ) O(n\log{n}+n) O(nlogn+n)

class Solution {
public:
    int minimumDifference(vector<int>& nums, int k) {
        sort(nums.begin(), nums.end());
        int n = nums.size();
        int ans = nums[n - 1] - nums[0];
        for (int i = k - 1; i < n; ++ i) {
            ans = min(ans, nums[i] - nums[i - k + 1]);
        }
        return ans;
    }
};

2、最长的美好子字符串

        (1)枚举:枚举所有的子串,然后对子串进行合法性检查。
        时间复杂度: O ( n 3 ) O(n^3) O(n3)

class Solution {
public:
    string longestNiceSubstring(string s) {
        string ans = "";
        int len = -1;
        for (int i = 0; i < s.size(); ++ i) {
            for (int j = i + 1; j < s.size(); ++ j) {
                string str = s.substr(i, j - i + 1);
                if (j - i + 1 > len && check(str)) {
                    len = j - i + 1;
                    ans = str;
                }
            }
        }
        return ans;
    }
    bool check(string s) {
        set<char> hashset;
        for (auto c: s) {
            hashset.insert(c);
        }
        for (auto c: hashset) {
            if (c >= 'a' && c <= 'z')
                c -= 32;
            else
                c += 32;
            if (!hashset.count(c))
                return false;
        }
        return true;
    }
};

        (2)二进制优化:int 4 4 4 个字节,共 32 32 32 位,那么使用第 26 26 26 位来记录每个字母是否出现。小写用 a a a,大写用 b b b,如果某一段字母中, a = = b a==b a==b,那么,该段字符串为美好字符串。
        时间复杂度: O ( n 2 ) O(n^2) O(n2)

class Solution {
public:
    string longestNiceSubstring(string s) {
        string ans = "";
        int len = -1;
        for (int i = 0; i < s.size(); ++ i) {
            int a = 0, b = 0;
            for (int j = i; j < s.size(); ++ j) {
                if (s[j] >= 'a' && s[j] <= 'z')
                    a |= 1 << (s[j] - 'a');
                else if (s[j] >= 'A' && s[j] <= 'Z')
                    b |= 1 << (s[j] - 'A');
                
                if (a != 0 && a == b) {
                    if (j - i + 1 > len) {
                        len = j - i + 1;
                        ans = s.substr(i, len);
                    }
                }
            }
        }
        return ans;
    }
};

        (3)分治:枚举下标从起点 b e g beg beg 到末尾 e n d end end 的字符串,如果某个字符 s [ i ] s[i] s[i] 不符合最美字符串的要求,那么符合条件的最美字符串所在范围为 [ b e g , i − 1 ] , [ i + 1 , e n d ] [beg,i-1],[i +1,end] [beg,i1][i+1,end] 这两个范围,再次进行递归,返回一个更长更早出现的最美字符串。
        时间复杂度: O ( n 2 ) O(n^2) O(n2)

class Solution {
public:
    string longestNiceSubstring(string s) {
        function<string(int, int)> dfs = [&] (int beg, int end)->string {
            if (beg >= end)
                return "";
            
            int a = 0, b = 0;
            for (int i = beg; i <= end; ++ i) {
                if (s[i] >= 'a' && s[i] <= 'z')
                    a |= 1 << (s[i] - 'a');
                else
                    b |= 1 << (s[i] - 'A');
            }
            int spilt = -1;
            int c = a & b;
            for (int i = beg; i <= end; ++ i) {
                int id = s[i] >= 'a' && s[i] <= 'z' ? s[i] - 'a' : s[i] - 'A';
                if (((c >> id) & 1) == 0) {
                    spilt = i;
                    break;
                }
            }

            if (spilt == -1) {
                return s.substr(beg, end - beg + 1);
            }

            string left = dfs(beg, spilt - 1), right = dfs(spilt + 1, end);

            return left.size() >= right.size() ? left : right;
        };


        return dfs(0, s.size() - 1);
    }
};

3、找到一个数字的 K 美丽值

        (1)枚举:将 n u m num num 转为 s t r i n g string string 类型,枚举字符串,截取一段长为 k k k 的子字符串,使用 a t o i atoi atoi 函数将该子字符串转为 i n t int int 类型,随后判断一下是否可以整除即可。
        时间复杂度: O ( n ) O(n) O(n)

class Solution {
public:
    int divisorSubstrings(int num, int k) {
        string s = to_string(num);
        int ans = 0;
        for (int i = 0; i + k <= s.size(); ++ i) {
            int div = atoi(s.substr(i, k).c_str());
            if (div != 0 && num % div == 0)
                ++ ans;
        }
        return ans;
    }
};

4、K 连续位的最小翻转次数

        基本思路是 贪心,枚举数组,每当遇到 0 0 0,即把包括当前位置的数字共 k k k 个数字与 1 1 1 做异或运算。如果发现 0 0 0 的位置及其后面的数组长度不足 k k k,那么满足条件,返回 − 1 -1 1;当数组全部枚举完,返回翻转的次数。
        (1)枚举:每次找到为 0 0 0 的数字即把包括其在内的之后 k k k 个数字与 1 1 1 做异或运算。
        时间复杂度: O ( k ∗ n ) O(k*n) O(kn),在极限数据的情况下,运算次数为 1 0 10 10^{10} 1010,会 TLE 。超时的原因就是对于每一个需要翻转的数都 真 真 实 实 真真实实 的进行了翻转操作。

class Solution {
public:
    int minKBitFlips(vector<int>& nums, int k) {
        int n = nums.size();
        int ret = 0;
        for (int i = 0; i < n; ++ i) {
            if (nums[i] == 0) {
                if (i + k > n)
                    return -1;
                ret ++;
                for (int j = i; j - i < k; ++ j) {
                    nums[j] ^= 1;
                }
            }
        }
        return ret;
    }
};

        (2)差分前缀和优化:通过寻找规律我们得知,每个数翻转 偶数 次之后依旧是其本身;翻转 奇数 次之后,0 变 1,1 变 0。
                ① 那么我们只需要记录每个数字的 翻转次数 来确定当前的数字的值,再进行操作。翻转操作的对象是一段区间内的数字,所以自然想到了 差分 来记录翻转次数,该差分数组的 前缀和 就是当前数字的翻转次数。差分数组 arr[l+1]++,arr[l+k]-- 代表了从 l + 1 l+1 l+1 l + k − 1 l+k-1 l+k1 位置的翻转次数 + 1 +1 +1
                ② 如果 n u m s [ i ] nums[i] nums[i] 奇 数 1 奇数_1 1,我们需要其的翻转次数为 偶数;如果 n u m s [ i ] nums[i] nums[i] 偶 数 0 偶数_0 0,那么我们需要其的翻转次数为 奇数
                ③ 如果 n u m s [ i ] nums[i] nums[i]1 c n t cnt cnt 是奇数, 那么需要再次进行翻转,反之不需要;如果 n u m s [ i ] nums[i] nums[i]0 c n t cnt cnt 是 偶数,那么需要再次翻转,反之不需要。
                ④ 发现规律:如果 n u m s [ i ] + c n t nums[i]+cnt nums[i]+cnt偶数 的话需要再次翻转,否则不需要。
        时间复杂度: O ( n ) O(n) O(n)

class Solution {
public:
    int minKBitFlips(vector<int>& nums, int k) {
        int ret = 0;
        vector<int> arr(nums.size() + 3, 0);
        int cnt = 0;
        for (int i = 0; i < nums.size(); ++ i) {
            cnt += arr[i];
            if ((cnt + nums[i]) % 2 == 0) {
                if (i + k > nums.size()) {
                    return -1;
                }
                ret ++;
                arr[i + 1] ++;
                arr[i + k] --;
            }
        }
        return ret;
    }
};

        (3)队列优化:使用一个队列保存需要翻转的起点,队列内的元素个数就是每个数字需要翻转的次数。

class Solution {
public:
    int minKBitFlips(vector<int>& nums, int k) {
        int ret = 0;
        vector<int> q(100010);
        int hh = 0, tt = 0;
        for (int i = 0; i < nums.size(); ++ i) {
            if (hh < tt && q[hh] + k <= i) ++ hh;
            int cnt = tt - hh;
            if ((nums[i] + cnt) % 2 == 0) {
                if (i + k > nums.size())
                    return -1;
                ++ ret;
                q[tt ++] = i;
            }
        }
        return ret;
    }
};

        (4)既然是用到了 贪心,那么说什么都要证明以下其的正确性。
                归纳推理:遇到 0 0 0 马上进行翻转得到最优解。
                ① 假设前 i − 1 i-1 i1 个元素已经全是 1 1 1,第 i i i 个元素是 0 0 0。如果要将前 i i i 个元素都变为 1 1 1,那么需要将第 i i i 个元素翻转 奇数 次,由于前 i − 1 i-1 i1 个元素已经是 1 1 1,那么只需要将窗口头部为 i i i 的窗口内的元素翻转 1 1 1 次即可将前 i i i 个元素变为 1 1 1,这是代价最小的做法。
                ② 如果第 i i i 个元素是 1 1 1,窗口头部滑过时不会进行操作,这样是把前 i i i 个元素变为 1 1 1 的最小代价。
                ③ 现在前 i i i 个元素已经是 1 1 1,扩大规模,那么将前 i + 1 i+1 i+1 个元素前变为 1 1 1 的操作的步骤同上,是最小代价做法。

结语

        今天的题对我来说挺难的,可能是之前基本没做过相关的题目,知识是学过的,但是没有用过,所以想题的时候抽象一点就没思路了,盯着屏幕在发呆,然后看题解找思路然后自己按照思路敲代码👊希望有点用。早上有点起不来了😣感觉,今晚早点睡,不刷视频了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值