侧边栏壁纸
博主头像
牛马 OJ 博主等级

行动起来,活在当下

  • 累计撰写 9 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

知识点题单-贪心

Administrator
2025-03-01 / 0 评论 / 2 点赞 / 63 阅读 / 0 字

一、考察频率以及难度

考察频率:⭐⭐⭐⭐

笔试题难度:⭐⭐⭐

算法难度:⭐

贪心算法是面试和笔试中非常常见的考察题型之一。它广泛应用于各种实际问题场景中,无论是在数组、字符串处理,还是在图论、区间调度等问题里都可能出现。

贪心算法本身的思想并不复杂,它的核心在于每一步都做出当前看起来最优的选择,期望通过局部最优来达到全局最优。不过,判断一个问题是否可以使用贪心算法求解,以及如何设计贪心策略是考察的重点和难点。

二、学习技巧

  1. 理解贪心本质:深入理解贪心算法的核心思想,即每一步都选择当前状态下的最优解。要明确贪心选择和最优子结构这两个关键要素,贪心选择是指问题的全局最优解可以通过一系列局部最优解得到,最优子结构是指问题的最优解包含其子问题的最优解。

  2. 分析问题特征:学会分析问题的特征,判断该问题是否适合使用贪心算法。一般来说,如果一个问题具有贪心选择性质和最优子结构性质,那么就可以尝试使用贪心算法求解。例如,在一些资源分配、区间调度等问题中,往往可以通过贪心策略得到最优解。

  3. 多做练习题:通过大量的练习题来熟悉不同类型的贪心问题,掌握常见的贪心策略和解题思路。在做题过程中,要注意总结归纳,分析每个问题的特点和贪心策略的选择依据。

  4. 学会猜结论:很多贪心的题目,策略可能不复杂,只是比较难证明正确性,因此在多做练习题的同时,一定要注意找到猜贪心结论的题干,拿到贪心题目时最好脑海中先能有几种候选的贪心策略,然后逐一验证。

  5. 验证贪心策略:在设计出贪心策略后,要通过严格的证明或者反例来验证该策略是否能得到全局最优解。有时候,局部最优选择并不一定能导致全局最优,所以需要谨慎验证。

三、应用场景

  1. 对于暴力的做法会超时、动态规划不好设计状态等问题时,不妨看下能否找到一种贪心策略来解决。

  2. 对于区间问题、资源分配问题、或是一些图论问题时,可以考虑贪心算法。

四、算法讲解

贪心算法是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好或最优的算法。贪心算法并不从整体最优上加以考虑,所做出的仅是在某种意义上的局部最优解。
算法步骤:

  • 问题分析:明确问题的目标和约束条件,判断问题是否具有贪心选择性质和最优子结构性质。

  • 设计贪心策略:根据问题的特点,设计一个贪心策略,即每一步如何做出最优选择。

  • 执行贪心算法:按照贪心策略,逐步做出选择,直到得到问题的解。

  • 验证解的最优性:通过证明或者反例来验证所得到的解是否是全局最优解。

例题:

在一个国家,流通着多种不同面值的货币。现有不同面值的硬币,其面值分别为 [1, 5, 10](单位:元)。当顾客购物后,收银员需要找给顾客一定金额的零钱。请编写一个程序,计算出使用最少数量的硬币来凑出给定找零金额的方案,并输出所需硬币的最少数量。(硬币数量无限)

分析思路:

由于需要使用最少的数量凑出目标金额,因此不难想到贪心策略是优先找零的金额越大越好,因此只需要能用10就用10,对剩下的金额再能用5就用5,最后剩下的用1来找零。但是如何证明这样贪心的正确性呢?其实这题之所以可以用贪心,还有一个关键的特点:观察题目给的硬币金额,5可以只由1凑出,10可以只由5或只由1凑出,因此可以这样贪心。

试想,如果硬币面值有25元的,还可以这样做吗?(比如顾客购买31元的商品,最佳方案是什么?)

def coin_change(coins, amount):
    coins.sort(reverse=True)
    count = 0
    index = 0
    while amount > 0:
        if coins[index] <= amount:
            num = amount // coins[index]
            count += num
            amount -= num * coins[index]
        index += 1
    return count

# 测试代码
coins = [10, 5, 1]
amount = 63
print(coin_change(coins, amount))

另外补充一点,很多贪心的题目其实都有对应的动态规划做法,但是对于一些题目,贪心写起来会方便一些。对于明显可以贪心的题目,为了在笔试中节省宝贵的时间,最好直接选择贪心法。

五、例题解析

5.1 基础-坐船问题1

https://oj.niumacode.com/problem/P1394

有n个人和m条船,每条船最多只能做一个人,给定每个人的重量以及每条船的载重,问最少需要选择总载重为多少的船,可以把n个人运送过去?(n<=m<1e5)

如果运送不过去,请输出-1。

#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int a[100005], b[100005];

int main() {
    int n, m;
    cin >> n;
    for (int i = 1; i <= n; i++) cin >> a[i];
    cin >> m;
    for (int i = 1; i <= m; i++) cin >> b[i];

    // 对人的重量数组和船的载重数组进行排序
    sort(a + 1, a + n + 1);
    sort(b + 1, b + m + 1);

    int totalLoad = 0;
    int boatIdx = 1;
    int i = 1;
    for(; i<=n; i++) {
        while(boatIdx <= m && b[boatIdx] < a[i]) boatIdx++;
        if(boatIdx > m) break;
        totalLoad += b[boatIdx];
    }
    if(i <= n) cout << -1 << endl;
    else cout << totalLoad << endl;
    return 0;
}
n = int(input())
a = [0] + list(map(int, input().split()))

m = int(input())
b = [0] + list(map(int, input().split()))

# 对人的重量数组和船的载重数组进行排序
a.sort()
b.sort()

totalLoad = 0
boatIdx = 1
i = 1
while i <= n:
    # 找到能承载当前人的最小载重的船
    while boatIdx <= m and b[boatIdx] < a[i]:
        boatIdx += 1
    # 如果没有合适的船,退出循环
    if boatIdx > m:
        break
    totalLoad += b[boatIdx]
    i += 1

# 如果还有人没有船可坐,输出 -1
if i <= n:
    print(-1)
else:
    print(totalLoad)

5.2 基础-坐船问题2

https://oj.niumacode.com/problem/P1395

小牛班级里有n位同学参加春游,给出每位同学的重量a[i],现在进行划船比赛,每艘船两位同学,问如何分组,可以使得分组后最大载重的那艘船的载重最小?输出这个载重。(n<1e5,a[i]<=1e5)

输入
4
1 2 5 8
输出
9

题解和代码

贪心思路:最大和最小的匹配,第二大和第二小的匹配,以此类推。

题目转化成:给定一个n个数的无序数组,每个值a[i],最大和最小的匹配为一个小队,第二大和第二小的匹配为一个小队,以此类推,在计算的同时维护最大值即是答案。

#include<iostream>
#include<algorithm>
using namespace std;
int n;
int a[100005], b[100005];
int ans = 0;
int main()
{
        cin>>n;
        for(int i = 1; i<=n; i++) {
                cin>>a[i];
        }
        sort(a+1, a+n+1);
        for(int i = 1; i<=n; i++) {
                int t = a[i]+a[n-i+1];
                ans = max(ans, t);
        }
        cout << ans << endl;
        return 0;
 } 
n = int(input())

a = [0] + list(map(int, input().split()))

a[1:] = sorted(a[1:])

ans = 0

# 遍历元素,计算两两组合的和,并更新最大和
for i in range(1, n + 1):
    t = a[i] + a[n - i + 1]
    ans = max(ans, t)

# 输出最大和
print(ans)

5.3 基础-坐船问题3

https://oj.niumacode.com/problem/P1396

小牛班级里有n位同学参加春游,给出每位同学的重量a[i],现在进行划船比赛,每艘船最多两位同学,每艘船最多载重为x ,请输出想要承载所有人所需的最小船数。(n<1e5,a[i],x<=1e9)

输入
4 3
1 3 2 2
输出
3

题解与代码

考虑贪心,由于每位同学都要上船,因此肯定一轻一重搭配最好,定义左右两个指针分别从两端向中间靠拢收敛。

对于当前左右指针,有两种情况:

如果无法同乘一艘船,则右指针(较重者)肯定无法与其他任意一个人同乘一艘船,因此他自己单独乘船,原问题答案为求解剩余 n-1 人所需最小船数加一。(左指针不动,右指针--)

若能与最重者同乘,为最优选择,去掉最轻和最重者,原问题答案为求解剩余 n-2 人所需最小船数加一。(左指针++,右指针--)

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;


int main() {
    int limit, n;
    cin >> n >> limit;
    vector<int> people;
    for (int i = 0; i < n; ++i) {
        int num;
        cin >> num;
        people.push_back(num);
    }
    int ans = 0;
    sort(people.begin(), people.end());
    int light = 0, heavy = people.size() - 1;
    while (light <= heavy) {
        if (people[light] + people[heavy] > limit) {
            --heavy;
        } else {
            ++light;
            --heavy;
        }
        ++ans;
    }
    cout << ans << endl;
    return 0;
}
n, limit = map(int, input().split())

people = list(map(int, input().split()))

people.sort()

ans = 0

# 初始化轻量级和重量级人员的索引
light = 0
heavy = len(people) - 1

# 模拟将人员分配到船上的过程
while light <= heavy:
    if people[light] + people[heavy] > limit:
        # 如果最轻和最重的人不能一起上船,让最重的人单独上船
        heavy -= 1
    else:
        # 如果最轻和最重的人能一起上船,他们一起上船
        light += 1
        heavy -= 1
    # 每分配一次,所需船的数量加 1
    ans += 1

# 输出所需船的总数
print(ans)

5.4 基础-硬币向上

https://oj.niumacode.com/problem/P1400

有n枚硬币排成一排,用0表示反面,用1表示正面,每次操作需要翻转连续的三个硬币,问至少多少次操作可以使得所有硬币正面向上,如果无法做到请输出-1。(n<1e5)

输入
4
0 1 1 0
输出
2

题解与代码

最左边一个如果是0的话,一定要翻转最左边3个(这一步是必须做的,因为只有这样才可以让最左边变成1),最左边如果是1的话,则一定不翻(这也是必须的)。此时问题就转化为n-1的相同子问题,以此类推求解。

#include <iostream>
#include <vector>
using namespace std;
int n;
int a[100005];

int main() {
    cin >> n;
    for(int i = 1; i<=n; i++) cin>>a[i];
    int ans = 0;
    for (int i = 1; i<=n; i++) {
        if (a[i] == 0) {
            if (i > n-2) {
                cout << -1 << endl;
                return 0;
            }
            for (int j = i; j < i + 3; j++) {
                a[j] = 1 - a[j];
            }
            ans++;
        }
    }
    cout << ans << endl;
    return 0;
}
n = int(input())

a = [0] + list(map(int, input().split()))

ans = 0

for i in range(1, n + 1):
    # 如果当前元素为 0
    if a[i] == 0:
        # 如果当前位置往后不足 3 个元素,无法完成操作,输出 -1 并结束程序
        if i > n - 2:
            print(-1)
            break
        # 对当前位置及往后两个位置的元素进行取反操作
        for j in range(i, i + 3):
            a[j] = 1 - a[j]
        # 操作次数加 1
        ans = ans + 1
else:
    # 如果没有执行 break 语句,说明可以完成操作,输出操作次数
    print(ans)

5.5 进阶-小牛选择最大卡牌

https://oj.niumacode.com/problem/P1397

小牛和小马在玩一种游戏,该游戏中双方各抽n张卡牌,每张卡牌都是一个小于1e5的正整数,谁的卡牌之和最大谁赢。小牛十分想赢,但是他想赢的漂亮,他希望自己选择的卡牌之和是3的倍数,宁可放弃一些卡牌。问在满足小牛的想法的前提下,他可以获得的最大卡牌之和是多少?

输入
5
1 3 3 3 6
输出
15

题解与代码

本题思路很多,对于取模类题目,比较经典的是基于模数来维护信息,并使用动态规划来递推。当然,本题中也可以使用贪心的思想。

假设所有卡牌之和是3的倍数,那么不需要放弃卡牌;

假设所有卡牌之和%3=1,则要么丢弃一张%3=1的,要么丢弃两张%3=2的。

假设所有卡牌之和%3=2,则要么丢弃一张%3=2的,要么丢弃两张%3=1的。

因此,基于这个思路,我们只需要维护每个模数下的所有卡牌,从小到大排序贪心选择丢弃即可。

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int a[100005];
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin>>a[i];
    }
    vector<int> v[3];
    long long tot = 0;
    for (int i = 1; i <= n; i++) {
        v[a[i] % 3].push_back(a[i]);
        tot += a[i];
    }
    sort(v[1].begin(), v[1].end(), greater<int>());
    sort(v[2].begin(), v[2].end(), greater<int>());
    int remove = 1e9;

    if (tot % 3 == 0) {
        remove = 0;
    }
    else if (tot % 3 == 1) {
        if (v[1].size() >= 1) {
            remove = min(remove, v[1].back());
        }
        if (v[2].size() >= 2) {
            remove = min(remove, v[2].back() + *(v[2].end() - 2));
        }
    }
    else {
        if (v[1].size() >= 2) {
            remove = min(remove, v[1].back() + *(v[1].end() - 2));
        }
        if (v[2].size() >= 1) {
            remove = min(remove, v[2].back());
        }
    }

    cout << tot - remove << endl;
    return 0;
}
# 读取数组长度
n = int(input())

# 读取数组元素
a = [0] + list(map(int, input().split()))

# 初始化三个列表来存储余数为 0、1、2 的元素
v = [[] for _ in range(3)]
# 计算所有元素的总和
tot = 0

# 遍历数组,将元素按余数分类,并计算总和
for i in range(1, n + 1):
    remainder = a[i] % 3
    v[remainder].append(a[i])
    tot += a[i]

# 对余数为 1 和 2 的列表进行降序排序
v[1].sort(reverse=True)
v[2].sort(reverse=True)

# 初始化要移除的元素总和为无穷大
remove = float('inf')

# 根据总和的余数情况计算要移除的元素总和
if tot % 3 == 0:
    remove = 0
elif tot % 3 == 1:
    if len(v[1]) >= 1:
        remove = min(remove, v[1][-1])
    if len(v[2]) >= 2:
        remove = min(remove, v[2][-1] + v[2][-2])
else:
    if len(v[1]) >= 2:
        remove = min(remove, v[1][-1] + v[1][-2])
    if len(v[2]) >= 1:
        remove = min(remove, v[2][-1])

# 输出最终结果
print(tot - remove)

5.6 进阶-最优任务分配(VIP)

https://oj.niumacode.com/problem/P1398

小牛是一个任务分配管理员,管理一个工厂,工厂里有若干个不同编号的任务,对应的任务量用数组a表示。

一开始,所有的工作岗位都处于未分配状态。小牛可以进行如下的任务分配操作任意次:

选择两个不同的且未分配的工作岗位编号 ij,若满足 2 * a[i] <= a[j],那么将这两个工作岗位标记为已分配。

可以执行上述操作任意次,计算出最多可以将多少个工作岗位标记为已分配状态。(n, a[i]<1e5)

输入
4
2 3 4 5
输出
2

题解与代码

不难发现本题最多凑出n/2对(i,j),因此把数组一分为二,其中i就从前一半中选,j从后一半中选,对于每一个i都保证找到一个最左的j与之匹配。因此用双指针向后遍历即可。其实这题也可以二分答案,感兴趣同学可以思考下看看

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;


int main() {
    int n;
    cin >> n;
    vector<int> nums;
    for (int i = 0; i < n; ++i) {
        int num;
        cin >> num;
        nums.push_back(num);
    }
    sort(nums.begin(), nums.end());
    int m = nums.size() / 2;
    int res = 0;
    int i = 0, j = m;
    while (i < m && j < nums.size()) {
        while (j < nums.size() && 2 * nums[i] > nums[j]) {
            j++;
        }
        if (j < nums.size()) {
            res += 2;
            j++;
        }
        i++;
    }
    cout << res << endl;
}
n = int(input())
nums = list(map(int, input().split()))
nums.sort()
m = len(nums) // 2
res = 0
i = 0
j = m
# 开始循环,只要 i 小于 m 且 j 小于列表长度就继续
while i < m and j < len(nums):
    # 当 j 小于列表长度且当前 i 位置元素的两倍大于 j 位置元素时,j 指针后移
    while j < len(nums) and 2 * nums[i] > nums[j]:
        j = j + 1
    # 如果 j 仍在列表范围内,说明找到了符合条件的配对,结果加 2,同时 j 指针后移
    if j < len(nums):
        res = res + 2
        j = j + 1
    # i 指针后移
    i = i + 1
# 输出最终结果
print(res)

5.7 进阶-小牛服务公司的最大利润(VIP)

https://oj.niumacode.com/problem/P1399

小牛有一家服务公司,公司里有 n 项服务项目和 m 个服务人员。每个项目都有对应的复杂程度a[i]以及对应的收益b[i]。每个服务人员都有自己的业务能力值c[i],每个服务人员只能胜任复杂程度不超过其业务能力的服务项目。

一项服务可以被选择多次,每个人员最多选择一项服务,请问服务公司的最大收益是多大?(m<=n<=1e5,1<=a[i],b[i],c[i]<=1e5)

输入
4 3
1 2 3 4
20 10 30 40
1 2 3
输出
70

题解与代码

把项目的复杂程度和收益打包后从小到大排序,然后维护前缀最大值,最后对服务人员排序后遍历,为他找一个复杂度不大于他的收益最高的项目(这一步可以双指针也可以二分),最后累加即可。

注意一些双指针顺序遍历的题目,其实都也可以用二分来考虑,只不过时间复杂度上会带一个log。

#include <iostream>
#include <vector>
#include <utility>
#include <algorithm>
using namespace std;
int a[100005],b[100005],c[100005];
int n, m;
int main() {

    cin >> n >> m;
    for(int i = 1; i<=n; i++) cin>>a[i];
    for(int i = 1; i<=n; i++) cin>>b[i];
    for(int i = 1; i<=n; i++) cin>>c[i];
    vector<pair<int, int>> jobs;
    for(int i = 1; i<=n; i++) {
        jobs.emplace_back(a[i], b[i]);
    }
    sort(jobs.begin(), jobs.end());
    sort(c+1, c+m+1);
    int j = 0, mx = 0;
    long long ans = 0;
    for (int i = 1; i<=m; i++) {
        while (j < n && c[i] >= jobs[j].first) {
            mx = max(mx, jobs[j].second);
            j++;
        }
        ans += mx;
    }
    cout << ans << endl;
    return 0;
}

5.8 进阶-和为target的子数组数量(VIP)

给定一个长度为n的数组a,问最多可以选出多少互不重叠的子数组,使得这些子数组的和都为target。(n<1e5, 1e5<a[i],target<1e5)

输入
5 2
-1 1 1 1 1
输出
2

思路与代码

贪心考虑对剩余子问题影响最小(副作用最小)的右端点,因此从左到右遍历,只要找到一段子区间为target则为候选答案。

证明可使用等价成立证明法:考虑如果有a[l,..,r1]和a[l,..,r2]都为target,如果a[l,..,r2]在最终答案中,则替换为子区间a[l,..,r1],一定不会使得答案更差,也不影响后续其他选择,因此可以把a[l,..,r2]等价替换成a[l,..,r1]。

#include <iostream>
#include <vector>
#include <set>

using namespace std;

int main() {
    int n, target;
    cin >> n >> target;

    vector<int> nums(n);
    for (int i = 0; i < n; ++i) {
        cin >> nums[i];
    }

    int ret = 0;
    int i = 0;

    while (i < n) {
        set<int> s = {0};
        // 初始化当前前缀和
        int total = 0;
        while (i < n) {
            // 累加当前元素到前缀和
            total += nums[i];
            // 检查是否存在满足条件的子数组
            if (s.find(total - target) != s.end()) {
                // 若存在,不重叠子数组数量加 1
                ++ret;
                break;
            } else {
                // 若不存在,将当前前缀和加入集合
                s.insert(total);
            }
            // 移动到下一个元素
            ++i;
        }
        // 无论是否找到满足条件的子数组,都移动到下一个位置开始新的查找
        ++i;
    }

    cout << ret << endl;

    return 0;
}
n, target = map(int, input().split())

nums = list(map(int, input().split()))

ret = 0
i = 0

while i < n:
    s = {0}
    total = 0
    while i < n:
        total += nums[i]
        if total - target in s:
            # 若存在,不重叠子数组数量加 1
            ret += 1
            break
        else:
            # 若不存在,将当前前缀和加入集合
            s.add(total)
            # 移动到下一个元素
            i += 1
    # 无论是否找到满足条件的子数组,都移动到下一个位置开始新的查找
    i += 1

print(ret)

2
博主关闭了所有页面的评论