文章

贪心6

贪心算法通过每步选择局部最优解,期望最终达到全局最优,适用于最优化和排序问题。

贪心6

贪心6

1921. 消灭怪物的最大数量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <cstdio>
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    int eliminateMaximum(vector<int> &dist, vector<int> &speed) {
        int n = dist.size();
        // 每个怪物到达城市所需的时间(向上取整)
        vector<int> time(n);
        for (int i = 0; i < n; ++i) {
            // a / b 向上取整: (a + b - 1) / b
            time[i] = (dist[i] + speed[i] - 1) / speed[i];
        }
        // 按照到达时间从早到晚排序
        sort(time.begin(), time.end());
        for (int i = 0; i < n; ++i) {
            // 第 i 分钟只能消灭第 i 个到达的怪物
            // 有怪物提前或同时到达,游戏失败
            if (time[i] <= i) return i;
        }
        return n; // 成功消灭所有怪物
    }
};

2384. 最大回文数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

class Solution {
public:
    string largestPalindromic(string num) {
        int n = num.size();
        // 用 ASCII 值建表,cnts['0'] ~ cnts['9'] 表示各个数字出现次数
        vector<int> cnts(58, 0);
        // 统计每个字符的出现次数
        for (char c: num) cnts[c]++;

        string left;  // 回文左半部分
        char mid = 0; // 回文中心字符(出现奇数次的最大数字)

        // 先从 '9' 到 '1',为回文构造左半部分(跳过 '0' 是为了避免前导零)
        for (char i = '9'; i >= '1'; --i) {
            // 如果当前字符出现奇数次,并且还没有选中中心字符,就选它
            if ((cnts[i] & 1) && mid == 0) mid = i;
            // 将一半数量的字符放到左半部分
            left += string(cnts[i] / 2, i);
        }

        // 如果左半部分是空的(说明所有数字都只出现 0 次或 1 次)
        if (left.empty()) {
            if (mid == 0) {
                return "0"; // 没有任何可构造的数字,只能返回 "0"
            } else {
                return string(1, mid); // 返回唯一的中心字符
            }
        }

        // 将 '0' 补到左半部分,注意:'0' 不会出现在最前面,因为前面已构造过有效数字
        left += string(cnts['0'] / 2, '0');

        // 构造右半部分为左半部分的反转
        string right = left;
        reverse(right.begin(), right.end());

        // 如果还没有选中中心字符,但 '0' 出现了奇数次,那就用 '0' 做中心
        if (mid == 0 && (cnts['0'] & 1))
            mid = '0';

        // 返回结果:左半 + 中心 + 右半
        if (mid != 0) return left + mid + right;
        return left + right;
    }
};

1792. 最大平均通过率

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>

using namespace std;

class Solution {
public:
    struct ClassInfo {
        int pass;
        int total;

        // 比较器:通过率提升大的优先
        bool operator<(const ClassInfo& other) const {
            double currGain = (double)(pass + 1) / (total + 1) - (double)pass / total;
            double otherGain = (double)(other.pass + 1) / (other.total + 1) - (double)other.pass / other.total;
            return currGain < otherGain;
        }
    };

    double maxAverageRatio(vector<vector<int>>& classes, int extraStudents) {
        priority_queue<ClassInfo> pq;

        // 初始化堆
        for (auto& c : classes) {
            pq.push({c[0], c[1]});
        }

        // 分配聪明学生
        while (extraStudents--) {
            ClassInfo top = pq.top();
            pq.pop();
            pq.push({top.pass + 1, top.total + 1});
        }

        // 累加平均通过率
        double totalRatio = 0.0;
        while (!pq.empty()) {
            ClassInfo cls = pq.top();
            pq.pop();
            totalRatio += (double)cls.pass / cls.total;
        }

        return totalRatio / classes.size();
    }
};

857. 雇佣 K 名工人的最低成本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
#include <iostream>
#include <vector>
#include <algorithm>
#include <queue>
#include <limits>

using namespace std;

struct Employee {
    double ratio; // 薪水与质量的比例 wage[i] / quality[i]
    int quality;  // 工作质量

    Employee(double r, int q) : ratio(r), quality(q) {}
};

class Solution {
public:
    // 计算雇佣 k 名工人的最低成本
    double mincostToHireWorkers(vector<int> &quality, vector<int> &wage, int k) {
        int n = quality.size(); // 工人数目
        vector<Employee> employees;
        employees.reserve(n);  // 预留空间,提升效率

        // 构造员工数组,计算每个员工的薪资质量比
        for (int i = 0; i < n; ++i) {
            employees.emplace_back((double) wage[i] / quality[i], quality[i]);
        }

        // 按比例从小到大排序
        sort(employees.begin(), employees.end(), [](const Employee &a, const Employee &b) {
            return a.ratio < b.ratio;
        });

        // 创建一个大根堆(priority_queue默认是大根堆)
        // 用来维护当前选中员工的质量,方便弹出最大质量员工以控制质量和最小化
        priority_queue<int> maxHeap;

        // 当前堆中所有员工的质量和
        int qualitySum = 0;
        // 记录最低成本,初始设为无穷大
        double res = numeric_limits<double>::max();

        // 遍历排序后的员工数组,尝试以当前员工的比例作为组内最大比例
        for (int i = 0; i < n; ++i) {
            maxHeap.push(employees[i].quality);
            qualitySum += employees[i].quality;

            // 如果当前选中员工数量超过 k,移除质量最大的那个以保证只雇佣 k 人
            if ((int) maxHeap.size() > k) {
                qualitySum -= maxHeap.top();
                maxHeap.pop();
            }

            // 当选中 k 个员工时,计算当前方案的成本
            if ((int) maxHeap.size() == k) {
                // 成本 = 质量和 * 当前员工的薪资质量比
                // 由于排序,当前员工的 ratio 是所选组内最大薪资质量比,保证满足所有员工的最低工资要求
                res = min(res, qualitySum * employees[i].ratio);
            }
        }

        return res;
    }
};

使用:

1
2
3
4
5
vector<Employee> employees;
employees.reserve(n);
for (int i = 0; i < n; ++i) {
    employees.emplace_back((double)wage[i] / quality[i], quality[i]);
}

vector size 初始为 0,capacity 为 n,每次 emplace_back() 真正创建一个对象并放到末尾,没有无用对象生成,性能优,语义明确。

而不是:

1
2
3
4
vector<Employee> employees(n);
for (int i = 0; i < n; ++i) {
    employees[i] = Employee((double)wage[i] / quality[i], quality[i]);
}

vector size 初始为 n,capacity 也是 n,会立即调用 Employee默认构造函数 n 次来初始化这 n 个对象(即使很快会覆盖它们),接着再用赋值运算符 = 去替换这些对象的值(调用拷贝赋值运算符)。

所以:

  • 多了一次默认构造 + 一次赋值
  • Employee 构造开销大(比如有复杂成员),就会 影响性能
  • 而且必须保证 Employee 提供了 **默认构造函数,否则会编译错误

3211 砍树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

// 每棵树的信息
struct Tree {
    int initWeight;  // 初始重量
    int growWeight;  // 每天增重
};

// 功能函数:计算最大收益
int computeMaxProfit(const vector<Tree> &trees, int m) {
    int n = trees.size();
    // 排序:增长慢的树尽早砍,增长快的树尽可能晚砍
    vector<Tree> sortedTrees = trees;
    sort(sortedTrees.begin(), sortedTrees.end(), [](const Tree &a, const Tree &b) {
        return a.growWeight < b.growWeight;
    });

    // dp[i][j]:前 i 棵树,选 j 棵砍,最大收益
    vector<vector<int>> dp(n + 1, vector<int>(m + 1, 0));

    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= m; ++j) {
            // 两种选择:不选第 i 棵树,或在第 j 天砍第 i 棵树
            dp[i][j] = max(
                    dp[i - 1][j],
                    dp[i - 1][j - 1] + sortedTrees[i - 1].initWeight + sortedTrees[i - 1].growWeight * (j - 1)
            );
        }
    }
    return dp[n][m];
}

int main() {
    int t;
    cin >> t;
    while (t--) {
        int n, m;
        cin >> n >> m;

        vector<int> init(n), grow(n);
        for (int i = 0; i < n; ++i) cin >> init[i];
        for (int i = 0; i < n; ++i) cin >> grow[i];

        vector<Tree> trees(n);
        for (int i = 0; i < n; ++i)
            trees[i] = Tree{init[i], grow[i]};

        int res = computeMaxProfit(trees, m);
        cout << res << '\n';
    }

    return 0;
}
本文由作者按照 CC BY 4.0 进行授权