C++算法 —— 贪心(5)


1、合并区间

56. 合并区间

在这里插入图片描述
在合并区间时,得先排序一下,方便判断。方便可以按照左或者右端点排序。很多问题左右端点都可以排序,这里就用左端点排序。

用左端点排序,排好后的所有区间,可以发现能够合并的区间都会是连续的。合并本质上就是在求并集,有可能连续几个区间左端点数字都相同,但右端点不同,这部分就得用最大的右端点做新的右端点;左端点的不同,有3种情况:右端点小于,大于等于上一个右端点,以及左端点大于上一个右端点。

假设第一个区间左右端点是left和right,第二个区间左右端点是ab。如果两个区间有重叠部分(即使只有一个端点也是重叠),那么a <= right,新区间就是[left, max(right, b)];如果a > right,那就不重叠,不合并,更新left和right。

    vector<vector<int>> merge(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end());
        int left = intervals[0][0], right = intervals[0][1];
        vector<vector<int>> res;
        for(int i = 1; i < intervals.size(); ++i)
        {
            int a = intervals[i][0], b = intervals[i][1];
            if(a <= right) right = max(right, b);
            else
            {
                res.push_back({left, right});
                left = a;
                right = b;
            }
        }
        //最后一个区间其实没加入
        res.push_back({left, right});
        return res;
    }

2、无重叠区间

435. 无重叠区间

在这里插入图片描述
先看上一题。和上一题不同的是,这道题需要保留尽可能多的不重叠的区间,也就是移除最少的区间。

关于区间重叠的所有情况,画图就能很好地展现出来,这个也简单,就不放图了。对于左端点相同,右端点不同的,应当留小的那个区间,因为更长的区间就更有可能和其它区间相交,这种情况就是right > b,留b所在的区间。其它情况也是如此,我们要留下更小的区间。

a < right,留下min(right, b),等于的情况不需要考虑;a >= right,也就是出现了不相交的情况,那就用下一个区间的右端点继续去判断,也就是更新right为b。

    int eraseOverlapIntervals(vector<vector<int>>& intervals) {
        sort(intervals.begin(), intervals.end());
        int left = intervals[0][0], right = intervals[0][1];
        int res = 0;
        for(int i = 1; i < intervals.size(); ++i)
        {
            int a = intervals[i][0], b = intervals[i][1];
            if(a < right)
            {
                res++;
                right = min(right, b);
            }
            else right = b;
        }
        return res;
    }

3、用最少的箭引爆气球

452. 用最少数量的箭引爆气球

在这里插入图片描述
在这里插入图片描述
先看合并区间和重叠区间这两道题。一支箭希望尽量引爆更多气球。根据之前两个题目,按照左端点排序后,如果求交集,会发现互相重叠的区间是连续的,如果求并集,会发现能够合并的区间是连续的。交集出现的这个性质其实是比较强的,三个区间可以合并,但有可能第三个区间和第一个区间无法合并,它们需要通过第二个区间才能三个全部合并,而重叠就无法这样。

对于这道题来说,我们要找的应当是重叠的区间,也就是右端点小于下一个左端点。我们可以求出两个区间的交集,让这个交集去和第三个区间比较,只要都能重叠,那就可以继续求交集,比较下去,交集会逐渐变小。

当a <= right时,交集区间是[max(left, a), min(right, b)];当遇到不重叠的时候,就得另发一支箭了。

    int findMinArrowShots(vector<vector<int>>& points) {
        sort(points.begin(), points.end());
        int right = points[0][1];
        int res = 1;
        for(int i = 1; i < points.size(); ++i)
        {
            int a = points[i][0], b = points[i][1];
            if(a <= right) right = min(right, b);
            else
            {
                res++;
                right = b;
            }
        }
        return res;
    }

4、整数替换

397. 整数替换

在这里插入图片描述
先来看一些二进制知识。

如果是偶整数,二进制表示中最后一位是0;如果是奇整数,二进制表示中最后一位是1;除2操作就是二进制表示中统一右移一位。

在这个题目中,如果是偶数,那就只能除2;如果是奇数,那就有两个选择,+1或-1,我们得判断走哪步更好。

二进制位的最后两位,可能是01,可能是11。对于01,+1变成10,-1变成00,这时候都可以除2,假设原二进制位前面几个是0,那么这时候10和00都除2的话,就变成了01和00,这时候明显00更好,它可以除2,而01只能继续做+1或者-1,所以对于01这个的二进制末二位,-1再除2更好。

对于…11,+1变成…00,-1变成10,这时候也都可以除2,但实际上00除2更好,这个情况可以自己列举。而如果数字是3,二进制表示就是00000011,那么-1再除2就好了。

对于01或11的判断,用数字 % 4就好。

    int integerReplacement(int n) {
        int res = 0;
        while(n > 1)
        {
            if(n % 2 == 0)
            {
                n /= 2;
                res++;
            }
            else
            {
                if(n == 3)
                {
                    res += 2;
                    n = 1;
                }
                else if(n % 4 == 1)
                {
                    res+= 2;
                    n /= 2;
                }
                else
                {
                    n = n / 2 + 1;
                    res += 2;
                }
            }
        }
        return res;
    }

5、俄罗斯套娃信封

354. 俄罗斯套娃信封问题

在这里插入图片描述
这道题最终会变成最长递增子序列的问题,用到这个问题的动规、贪心 + 二分思路,所以事先知道会更好,这道题也会在此基础上来写。我的博客动规5,贪心2中有最长递增子序列。

常规解法,虽然这道题会超时,但思路有用。和上面几个题一样,先按照左端点排序就不用去比较右边的区间了,只需要去左边比较右端点了。当排完序后,这其实就像一个最长递增子序列,只要后面的能套前面的,就可以连在一起,就是一个序列的。用动规思路。

状态表示:dp[i]是以i位置的信封为结尾的所有的套娃序列中,最长的序列的长度。

确定状态转移方程:0~i - 1的所有位置中,找到某个区间j,只要i能套j,那么就是dp[j] + 1。所以方程就是max(dp[j] + 1),需要的最大长度,条件则是0 <= j < i,e[i][0] > e[j][0],e[i][1] > e[j][1]。

初始化:dp表全初始化为1,因为1个区间可以套娃自己。

返回值:dp表里最大值。

    int maxEnvelopes(vector<vector<int>>& e) {
        //动规
        sort(e.begin(), e.end());
        int n = e.size();
        vector<int> dp(n, 1);
        int res = 1;
        for(int i = 1; i < n; ++i)
        {
            for(int j = 0; j < i; ++j)
            {
                if(e[i][0] > e[j][0] && e[i][1] > e[j][1])
                    dp[i] = max(dp[j] + 1, dp[i]);
            }
            res = max(res, dp[i]);
        }
        return res;
    }

适用的解法是重写排序 + 贪心。因为只是左端点排序后就用贪心,会需要考虑很多细节。

左端点排完序后,可能会出现几个区间左端点都相同,而右端点不同,这时候要考虑的就是右端点的顺序,如果右端点此时从大到小排序,就可以尽量地套娃下去;当左端点不同的时候,左端点从小到大排序,就不需要再排右端点。

    int maxEnvelopes(vector<vector<int>>& e) 
    {
        //贪心
        sort(e.begin(), e.end(), [&](const vector<int>& v1, const vector<int>& v2)
        {
            return v1[0] != v2[0] ? v1[0] < v2[0] : v1[1] > v2[1];
        });
        vector<int> res;
        res.push_back(e[0][1]);
        for(int i = 1; i < e.size(); ++i)
        {
            int b = e[i][1];
            if(b > res.back())
            {
                res.push_back(b);
            }
            else
            {
                int left = 0, right = res.size() - 1;
                while(left < right)
                {
                    int mid = (left + right) / 2;
                    if(res[mid] >= b) right = mid;
                    else left = mid + 1;
                }
                res[left] = b;
            }
        }
        return res.size();
    }

6、可被3整除的最大和

1262. 可被三整除的最大和

在这里插入图片描述
既然要得到最大的数,不妨在最一开始把所有数都加起来,模3如果等于0的话就直接返回这个数。

模3的结果还有2种情况,也就是余数1和2的情况,假设x是模3余数为1的数,y是余数为2的数。如果出现了% 3 == 1的情况,那么去掉这个x就没问题了,此时要么剩余的数的和是3的倍数,要么中间有几个有余数的数,但它们加起来就是3的倍数了,最终抵消了。所有整体可以看作去掉余数为1的这个x,其余数的和就是3的倍数。这种情况,要删的就是余数为1的这个数。

除此之外,余数为1的情况,不只是有一个x,可能有两个y,也就是两个余数为2的,加起来就是余数为4,那么% 3的话也是1,也还有更多的y,但和上面一样,最终都是2个y的情况,那么为了消除这个余数为1的情况,把最小的和次小的y去除就好。

余数为1的情况,综上所述,取两个情况的最大值,max(sum - x1, sum - y1 - y2)。

余数为2的情况。要么是一个y,要么是2个x。根据上面的思路,要最小的y,或最小和次小的两个x,max(sum - y1, sum - x1 - x2)。

求一堆数的最小值和次小值,可以用sort,这样最快是nlogn的时间复杂度,可以O(n)来解决,用分类讨论的方法。先选定两个数,每当新来一个数,就去和这两个数比较,一次次更新,最终得到的就是最小和次小的。如果x < x1,则x2 = x1, x1 = x;如果x1 <= x <= x2,则x2 = x。

    int maxSumDivThree(vector<int>& nums) {
        const int INF = 0x3f3f3f3f;
        int sum = 0, x1 = INF, x2 = INF, y1 = INF, y2 = INF;
        for(auto x : nums)
        {
            sum += x;
            if(x % 3 == 1)
            {
                if(x < x1)
                {
                    x2 = x1;
                    x1 = x;
                }
                else if(x < x2) x2 = x;
            }
            else if(x % 3 == 2)
            {
                if(x < y1)
                {
                    y2 = y1;
                    y1 = x;
                }
                else if(x < y2) y2 = x;
            }
        }
        if(sum % 3 == 0) return sum;
        else if(sum % 3 == 1) return max(sum - x1, sum - y1 - y2);
        else return max(sum - y1, sum - x1 - x2);
    }

7、距离相等的条形码

1054. 距离相等的条形码

在这里插入图片描述
把所有相同的数看成一个块,一个数组有好几个块,每个块的数字都隔一个位置来放置,这样就可以有效防止出现相邻数字相同。但这样还有问题,如果是122这样的数字,1放在第一个位置,后面没有1,再看2,两个2会放在相邻位置,所以要先处理出现次数最多的数,这样结果就是212,2先隔一个位置放置,再放置1。剩下的数的处理顺序无所谓,都按照隔一个位置放置的规则就行,每次只处理一批相同的数字。

    vector<int> rearrangeBarcodes(vector<int>& b) {
        unordered_map<int, int> hash;//统计每个数出现的频次
        int maxVal = 0, maxCount = 0;//出现次数最多的数和最多的次数
        for(auto x : b)
        {
            if(maxCount < ++hash[x])//统计一下频次再和maxCount比较
            {
                maxCount = hash[x];
                maxVal = x;
            }
        }
        int n = b.size();
        vector<int> res(n); 
        int index = 0;
        for(int i = 0; i < maxCount; i++)
        {
            res[index] = maxVal;
            index += 2;
        }
        hash.erase(maxVal);
        for(auto& [x, y] : hash)//这样是把x作为哈希表的key,y作为哈希表的value
        {
            for(int i = 0; i < y; i++)
            {
                if(index >= n) index = 1;
                res[index] = x;
                index += 2;
            }
        }
        return res;
    }

8、重构字符串

767. 重构字符串

在这里插入图片描述
和条形码基本一样的思路。在处理之前,如果次数最多的那个 > (n + 1) / 2,那么一定无法重排,就像例2。

    string reorganizeString(string s)
    {
        int hash[26] = {0};
        char maxChar = ' ';
        int maxCount = 0;
        for(auto ch : s)
        {
            if(maxCount < ++hash[ch - 'a'])
            {
                maxChar = ch;
                maxCount = hash[ch - 'a'];
            }
        }
        int n = s.size();
        if(maxCount > (n + 1) / 2) return "";
        string res(n, ' ');
        int index = 0;
        for(int i = 0; i < maxCount; i++)
        {
            res[index] = maxChar;
            index += 2;
        }
        hash[maxChar - 'a'] = 0;
        for(int i = 0; i < 26; i++)
        {
            for(int j = 0; j < hash[i]; j++)
            {
                if(index >= n) index = 1;
                res[index] = 'a' + i;
                index += 2;
            }
        }
        return res;
    }

结束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
贪心算法是一种问题求解方法,它在每一步总是做出当前情况下的最优选择,以期望获得最优解。而"最大整数"同样可以使用贪心算法来求解。 对于"最大整数"的问题,我们可以考虑如下的贪心策略:从高位开始,尽可能选择较大的数字。具体步骤如下: 1. 对于给定的整数,我们首先将其转化为一个数组,其中每个元素表示整数的一个位数。 2. 从最高位(最左侧)开始,遍历数组。 3. 对于当前位上的数字,从9开始递减,找到第一个小于等于当前数字的最大数字。 4. 如果找到了符合条件的最大数字,将其放在当前位。否则,不做任何操作。 5. 继续向下遍历,重复步骤3-4。 6. 最终,得到的数组即为满足条件的最大整数。 以一个具体的例子说明上述算法:假设给定的整数为5372。 1. 将整数转化为数组[5, 3, 7, 2]。 2. 从最高位开始遍历。 3. 对于第一位5,从9开始递减,找到第一个小于等于5的数字,为7。 4. 将7放在第一位,得到[7, 3, 7, 2]。 5. 对于第二位3,从9开始递减,找到第一个小于等于3的数字,为3(与当前数字相等)。 6. 不做任何操作,得到[7, 3, 7, 2]。 7. 对于第三位7,从9开始递减,找到第一个小于等于7的数字,为7。 8. 将7放在第三位,得到[7, 3, 7, 2]。 9. 对于第四位2,从9开始递减,找到第一个小于等于2的数字,为2。 10. 将2放在第四位,得到[7, 3, 7, 2]。 11. 遍历结束,最终得到的数组为[7, 3, 7, 2],转化为整数为7372。 通过上述贪心算法,我们得到了满足条件的最大整数7372。证明了贪心算法在"最大整数"问题中的有效性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值