算法篇:贪心算法

声明:若未特殊标出,则默认是leedcode原题。

3、2208. 将数组和减半的最少操作次数:

class Solution 
{
public:
    int halveArray(vector& nums) 
    {
        priority_queue heap;
        double sum = 0.0;
        for(auto x : nums)
        {
            heap.push(x);
            sum += x;
        }
        sum /= 2.0;
        int count = 0;
        while(sum > 0)
        {
            double t = heap.top() / 2.0;
            heap.pop();
            sum -= t;
            count++;
            heap.push(t);
        }
        return count;
    }
};

4、179. 最大数:

class Solution 
{
public:
    string largestNumber(vector& nums) 
    {
        //优化:将所有数按字典序排序
        vector strs;
        for(auto x : nums)
        {
            strs.push_back(to_string(x));
        }
        sort(strs.begin(), strs.end(), [](const string& s1, const string& s2)
        {
            return s1 + s2 > s2 + s1;
        });

        string ret;
        for(auto& s : strs) ret += s;

        if(ret[0] == '0') return "0";
        else return ret;
    }
};

补充知识:全序关系满足:完全性,反对称性,传递性。

5、376. 摆动序列:

class Solution 
{
public:
    int wiggleMaxLength(vector& nums) 
    {
        int n = nums.size(), len = 0, left = 0;
        if(n < 2) return n;
        for(int i = 0; i < n - 1; i++)
        {
            int right = nums[i + 1] - nums[i];
            if(right == 0) continue;
            if(0 >= left * right) len++;
            left = right;
        }
        return len + 1;
    }
};

6、300. 最长递增子序列:

①回顾dp的做法:
状态表示:dp[i]表示:以i的位置的元素为结尾的所有子序列中,最长递增子序列的长度。
状态转移方程:dp[i]=max(dp[j]+1)(j

②贪心优化:O(N^2)
存什么:所有长度为x的递增子序列中,最后一个元素的最小值。
存哪里:所有大于等于nums[i]的最小值的位置。

③二分优化:O(NlogN)
细节:边界情况。

class Solution 
{
public:
    int lengthOfLIS(vector& nums) 
    {
        int n = nums.size();
        vector ret;
        ret.push_back(nums[0]);

        for(int i = 1; i < n; i++)
        {
            if(nums[i] > ret.back()) ret.push_back(nums[i]);
            else
            {
                // 二分插入位置
                int left = 0, right = ret.size() - 1;
                while(left < right)
                {
                    int mid = (left + right) >> 1;
                    if(ret[mid] < nums[i]) left = mid + 1;
                    else right = mid;
                }
                ret[left] = nums[i];
            }
        }
        return ret.size();
    }
};

7、334. 递增的三元子序列:

法1:同上题。

法2:

class Solution 
{
public:
    bool increasingTriplet(vector& nums) 
    {
        int a = nums[0], b = INT_MAX;
        for(int i = 1; i < nums.size(); i++) 
        {
            if(nums[i] > b) return true;
            else if(nums[i] > a) b = nums[i];
            else a = nums[i];
        }
        return false;
    }
};

8、674. 最长连续递增序列:

解法:贪心+双指针。

class Solution 
{
public:
    int findLengthOfLCIS(vector& nums) 
    {
        int ret = 1, n = nums.size();
        for(int i = 0; i < n;)
        {
            int j = i + 1;
            while(j < n && nums[j] > nums[j - 1]) j++;
            ret = max(ret, j - i);
            i = j; // 直接在循环中更新下一个位置的起点
        }
        return ret;
    }
};

9、121. 买卖股票的最佳时机:

class Solution 
{
public:
    int maxProfit(vector& prices) 
    {
        int ret = 0;
        for(int i = 0, prevmin = INT_MAX; i < prices.size(); i++)
        {
            ret = max(ret, prices[i] - prevmin);
            prevmin = min(prevmin, prices[i]);
        }
        return ret;
    }
};

10、122. 买卖股票的最佳时机 II:

class Solution 
{
public:
    int maxProfit(vector& p) 
    {
        // 实现方式1:
        int ret = 0, n = p.size();
        for(int i = 0; i < n; i++)
        {
            int j = i;
            while(j + 1 < n && p[j + 1] >= p[j]) j++;
            ret += p[j] - p[i];
            i = j;
        }
        return ret;

        // // 实现方式2:拆分成一天一天
        // int ret = 0;
        // for(int i = 1; i < p.size(); i++)
        // {
        //     if(p[i] > p[i - 1])
        //         ret += p[i] - p[i - 1];
        // }
        // return ret;
    }
};

11、1005. K 次取反后最大化的数组和:

分情况讨论:设整个数组中负数的个数是m个:
①m>k:把前k小负数,转化为正数。
②m==k:把所有的负数全部转化成正数。
③m

class Solution 
{
public:
    int largestSumAfterKNegations(vector& nums, int k) 
    {
        int m = 0, n = nums.size(), minElem = INT_MAX;
        for(auto x : nums)
        {
            if(x < 0) m++;
            minElem = min(minElem, abs(x)); // 求绝对值最小的那个数
        }
        int ret = 0;
        if(m >= k)
        {
            sort(nums.begin(), nums.end());
            for(int i = 0; i < k; i++) ret += -nums[i];
            for(int i = k; i < n; i++) ret += nums[i];
        }
        else
        {
            // 把所有的负数变成正数
            for(auto x : nums) ret += abs(x);
            if((k - m) % 2) ret -= minElem * 2;            
        }
        return ret;
    }
};

​12、2418. 按身高排序:

解法一:创建二元组:
①创建一个数组pair
②对新的数组排序;
③按照顺序把名字提取出来即可。
解法二:利用哈希表存下映射关系
①先用哈希表存下映射关系<身高,名字>;
②对身高数组排序;
③根据排序后的结果,往哈希表里找名字即可。
解法三:对下标排序:
①创建一个下标数组;
②仅需对下标数组排序;
③根据下标数组排序后的结果,找到原数组的信息。

class Solution 
{
public:
    vector sortPeople(vector& names, vector& heights) 
    {
        // 1、创建一个数组
        int n = names.size();
        vector index(n);
        for(int i = 0; i < n; i++) index[i] = i;

        // 2、对下标进行排序
        sort(index.begin(), index.end(), [&](int i, int j)
        {
            return heights[i] > heights[j];
        });

        // 3、提取结果
        vector ret;
        for(int i : index) ret.push_back(names[i]);
        return ret;
    }
};

13、870. 优势洗牌:

算法原理:田忌赛马:
①如果比不过,就去拖累对面最强的那一个;
②如果能比过,那就直接比。

class Solution 
{
public:
    vector advantageCount(vector& nums1, vector& nums2) 
    {
        int n = nums1.size();
        // 1、排序
        sort(nums1.begin(), nums1.end());
        vector index2(n);
        for(int i = 0; i < n; i++) index2[i] = i;
        sort(index2.begin(), index2.end(), [&](int i, int j)
        {
            return nums2[i] < nums2[j];
        });

        //2、田忌赛马
        vector ret(n);
        int left = 0, right = n - 1;
        for(auto x : nums1)
        {
            if(x > nums2[index2[left]]) ret[index2[left++]] = x;
            else ret[index2[right--]] = x;
        }
        return ret;
    }
};

14、409. 最长回文串:

class Solution 
{
public:
    int longestPalindrome(string s) 
    {
        // 1、计数 - 用数组模拟哈希表
        int hash[127] = { 0 };
        for(auto ch : s) hash[ch]++;

        // 2、统计结果
        int ret = 0;
        for(auto x : hash) ret += x / 2 * 2;

        return ret == s.size() ? ret : ret + 1;
    }
};

15、942. 增减字符串匹配:

算法原理:
①当遇到“I”:选择当前最小的那个数;
②当遇到“D”:选择当前最大的那个数。

class Solution 
{
public:
    vector diStringMatch(string s) 
    {
        int left = 0, right = s.size();
        vector ret;
        for(auto ch : s)
        {
            if(ch == 'I') ret.push_back(left++);
            else ret.push_back(right--);
        }
        ret.push_back(left); // 把最后一个数放进去
        return ret;
    }
};

16、455. 分发饼干:

class Solution 
{
public:
    int findContentChildren(vector& g, vector& s) 
    {
        sort(g.begin(), g.end());
        sort(s.begin(), s.end());

        int ret = 0, m = g.size(), n = s.size();
        for(int i = 0, j = 0; i < m && j < n; i++, j++)
        {
            while(j < n && s[j] < g[i]) j++;
            if(j < n) ret++;
        }
        return ret;
    }
};

17、553. 最优除法:

算法原理:
解法一:暴力解法->递归->记忆化搜索->动态规划;
解法二:贪心:除了前两个数以外,其余的数全放在分子上即可。

class Solution 
{
public:
    string optimalDivision(vector& nums)
    {
        int n = nums.size();
        if(1 == n) return to_string(nums[0]);
        if(2 == n) return to_string(nums[0]) + "/" + to_string(nums[1]);
        string ret = to_string(nums[0]) + "/(" + to_string(nums[1]);
        for(int i = 2; i < n; i++) ret += "/" + to_string(nums[i]);
        return ret + ")";
    }
};

18、45. 跳跃游戏 II:

class Solution 
{
public:
    int jump(vector& nums)
    {
        int left = 0, right = 0, maxpos = 0, ret = 0, n = nums.size();
        while(left <= right) // 以防跳不到n - 1的位置
        {
            if(maxpos >= n - 1) return ret; 
            // 遍历当前层,更新下一层的最右节点
            for(int i = left; i <= right; i++) maxpos = max(maxpos, nums[i] + i);
            left = right + 1;
            right = maxpos;
            ret++;
        }
        return -1; // 跳不到的情况
    }
};

20、134. 加油站:

算法原理:
解法一:暴力解法->枚举:
①依次枚举所有的起点;
②从起点开始,模拟一遍加油的流程即可。
解法二:优化->找规律(贪心):O(N)

class Solution 
{
public:
    int canCompleteCircuit(vector& gas, vector& cost) 
    {
        int n = gas.size();
        for(int i = 0; i < n; i++)
        {
            int rest = 0; // 标记净收益
            int step = 0;
            for(; step < n; step++) // 枚举向后走的步数
            {
                int index = (i + step) % n; // 枚举向后走的步数
                rest= rest + gas[index] - cost[index];
                if(rest < 0) break;
            }
            if(rest >= 0) return i;
            i = i + step;
        }
        return -1;
    }
};

21、738. 单调递增的数字:

解法一:暴力枚举  O(NlogN)
①从大到小的顺序,枚举[n, 0]区间内的数字;
②判断数字是否是“单调递增的”。

解法二:贪心(找规律) O(N)
①如果高位单调递增的话,我们不去修改;
②从左往右,找到第一个递减的位置,从这个位置向前推,推到相同区域的最左端,使其减小1,后面的数全部修改成9。

class Solution 
{
public:
    int monotoneIncreasingDigits(int n) 
    {
        string s = to_string(n); // 把数字转化为字符串
        
        int i = 0, m = s.size();
        // 找第一个递减的位置
        while(i + 1 < m && s[i] <= s[i + 1]) i++;
        
        if(i + 1 == m) return n; // 判断一下特殊情况

        while(i - 1 >= 0 && s[i - 1] == s[i]) i--;
        s[i]--;
        for(int j = i + 1; j < m; j++) s[j] = '9';

        return stoi(s);
    }
};

22、991. 坏了的计算器:

解法一:正向推导。

解法二:正难则反(没有小数)
①end<=begin:begin-end次+1操作;
②end>begin:奇数时只能+1,偶数时可+1可除2(除法更优)。

class Solution 
{
public:
    int brokenCalc(int startValue, int target) 
    {
        // 正难则反 + 贪心
        int ret = 0;
        while(target > startValue)
        {
            if(target % 2 == 0) target /= 2;
            else target += 1;
            ret++;
        }
        return ret + startValue - target;
    }
};

23、56. 合并区间:

解法:
①先按照左端点排序(能够合并的区间都是连续的);
②如何合并?求并集。

class Solution 
{
public:
    vector> merge(vector>& intervals) 
    {
        // 1、先按照左端点排序
        sort(intervals.begin(), intervals.end()); 

        // 2、合并区间
        int left = intervals[0][0], right = intervals[0][1];
        vector> ret;
        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 // 没有重叠部分
            {
                ret.push_back({left, right}); // 加入到结果中
                left = a;
                right = b;
            }
        }
        // 别忘了最后一个区间
        ret.push_back({left, right});
        return ret;
    }
};

24、435. 无重叠区间:

解法:排序(左端点)+贪心策略。
①按照左端点排序;
②移除最少区间<==>保留更多区间。

class Solution 
{
public:
    int eraseOverlapIntervals(vector>& intervals) 
    {
        // 1、先按照左端点排序
        sort(intervals.begin(), intervals.end()); 

        // 2、移除区间
        int left = intervals[0][0], right = intervals[0][1];
        int ret = 0;
        for(int i = 1; i < intervals.size(); i++)
        {
            int a = intervals[i][0], b = intervals[i][1];
            if(a < right) // 有重叠部分
            {
                ret++; // 删掉一个区间
                right = min(right, b);
            }
            else // 没有重叠部分
            {
                // left = a;
                right = b;
            }
        }
        return ret;
    }
};

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

解法:
①先按照左端点排序(能够重叠的区间都是连续的);
②提出贪心策略:一支箭应该引爆更多气球->将互相重叠的所有区间都拿出来引爆。
如何求出互相重叠的区间?求交集。

class Solution 
{
public:
    int findMinArrowShots(vector>& points) 
    {
        // 1、先按照左端点排序
        sort(points.begin(), points.end()); 

        // 2、求互相重叠区间的数量
        int right = points[0][1];
        int ret = 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 // 没有重叠部分
            {
                ret++; // 加入到结果中
                right = b;
            }
        }
        return ret;
    }
};

26、397. 整数替换:

解法一:模拟(递归+记忆化搜索)。

class Solution 
{
    unordered_map hash; // 时间优化
public:
    int integerReplacement(int n) 
    {
        return dfs(n);
    }

    int dfs(long long n)
    {
        if(hash.count(n))
        {
            return hash[n];
        }

        if(n == 1)
        {
            hash[1] = 0;
            return 0;
        }

        if(n % 2 == 0) 
        {
            hash[n] = 1 + dfs(n / 2);
            return hash[n];
        }
        else 
        {
            hash[n] = 1 + min(dfs(n - 1), dfs(n + 1));
            return hash[n];
        }
    }
};

解法二:贪心
补充:二进制
①偶数:二进制表示中最后一位0;
②奇数:二进制表示中最后一位1;
③/2操作:二进制表示中统一右移一位。

class Solution 
{
    unordered_map hash; // 时间优化
public:
    int integerReplacement(int n) 
    {
        int ret = 0;
        while(n > 1)
        {
            // 分类讨论
            if(n % 2 == 0)
            {
                n /= 2;
                ret++;
            }
            else
            {
                if(n == 3)
                {
                    ret += 2;
                    n = 1;
                }
                else if(n % 4 == 1)
                {
                    n = n / 2;
                    ret += 2;
                }
                else
                {
                    n = n / 2 + 1;
                    ret += 2;
                }
            }
        }
        return ret;
    }
};

27、354. 俄罗斯套娃信封问题:

解法一:动态规划:(超时):
乱序->有序->按照左端点排序->最长递增子序列。
①状态表示:dp[i]表示:以i位置的信封为结尾的所有的套娃序列中,最长的套娃序列的长度。
②状态转移方程:max(dp[j]+1)
0<=je[j][0]&&e[i][1]>e[j][1]
③初始化:全初始化为1。
④填表顺序:从左往右。
⑤返回值:dp表里的最大值

class Solution 
{
public:
    int maxEnvelopes(vector>& e) 
    {
        // 解法一:动态规划
        // 预处理
        sort(e.begin(), e.end());
        
        int n = e.size();
        vector dp(n, 1);
        int ret = 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[i], dp[j] + 1);
                }
            }
            ret = max(ret, dp[i]);
        }
        return ret;
    }
};

解法二:重写排序+贪心+二分:
①重写排序
当左端点不同的时候:左端点从小到大排序。
当左端点相同的时候:右端点从大到小排序。
②最长递增子序列。

class Solution 
{
public:
    int maxEnvelopes(vector>& e) 
    {
        // 解法二:
        // 重写排序
        sort(e.begin(), e.end(), [&](const vector& v1, const vector& v2)
        {
            return v1[0] != v2[0] ? v1[0] < v2[0] :v1[1] > v2[1];
        });

        // 贪心+二分
        vector ret;
        ret.push_back(e[0][1]);

        for(int i = 1; i < e.size(); i++)
        {
            int b = e[i][1];
            if(b > ret.back())
            {
                ret.push_back(b);
            }
            else
            {
                int left = 0, right = ret.size() - 1;
                while(left < right)
                {
                    int mid = (left + right) / 2;
                    if(ret[mid] >= b) right = mid;
                    else left = mid + 1;
                }
                ret[left] = b;
            }
        }
        return ret.size();
    }
};

28、1262. 可被三整除的最大和:

解法一:动态规划。

解法二:正难则反+贪心+分类讨论:
先把所有的数累加在一起->根据累加和,删除一些数。
①sum%3=0,不删:
(x:标记%3=1的尽可能小的数,y:标记%3=2的尽可能小的数)
②sum%3=1,max(sum-x1,sum-y1-y2)
③sum%3=2,max(sum-y1,sum-x1-x2)

class Solution 
{
public:
    int maxSumDivThree(vector& 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);
    }
};

29、1054. 距离相等的条形码:

解法:贪心+模拟  O(N)
①每次处理一批相同的数。摆放的时候,每次隔一个格子。
②先处理出现次数最多的那个数,剩下的数顺序无所谓。

class Solution 
{
public:
    vector rearrangeBarcodes(vector& b) 
    {
        unordered_map hash; // 统计每个数出现的频次
        int maxVal= 0, maxCount= 0;
        for(auto x : b)
        {
            if(maxCount < ++hash[x])
            {
                maxCount = hash[x];
                maxVal = x;
            }
        }
        
        int n = b.size();
        vectorret(n);
        int index = 0;
        // 先处理出现次数最多的那个数
        for(int i = 0; i < maxCount; i++)
        {
            ret[index] = maxVal;
            index += 2;
        }

        // 处理剩下的数
        hash.erase(maxVal);
        for(auto& [x, y] : hash)
        {
            for(int i = 0; i < y; i++)
            {
                if(index >= n) index = 1;
                ret[index] = x;
                index += 2;
            }
        }
        return ret;
    }
};

30、767. 重构字符串:

解法:贪心+模拟  O(N)
①每次处理一批相同的字符。摆放的时候,每次隔一个格子。
②先处理出现次数最多的那个字符,剩下的数顺序无所谓。

class Solution {
public:
    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 ret(n, ' ');
        int index = 0;
        // 先处理出现次数最多的那个字符
        for(int i = 0; i < maxCount; i++)
        {
            ret[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;
                ret[index] = 'a' + i;
                index += 2;
            }
        }
        return ret;
    }
};

你可能感兴趣的:(算法,贪心算法)