贪心算法(算法详解+模板+例题)

1.贪心是什么

贪心算法(Greedy Algorithm)是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望导致结果是全局最好的策略。虽然这种策略并不保证一定能得到全局最优解,但在许多情况下,它能提供近似最优解,而且计算效率高。贪心算法通常适用于那些具有“最优子结构”和“贪心选择性质”的问题,比如霍夫曼编码、最小生成树等。

2.基本思路

  1. 建立数学模型:首先需要将实际问题抽象成数学模型,明确问题的目标和约束条件。
  2. 分解子问题:将原问题分解成多个子问题,这些子问题的解决方案有助于构建整个问题的解决方案。
  3. 局部最优解:对于每一个子问题,贪心算法会做出在当前状态下看似最优的选择。这种选择通常是基于某种度量标准或规则来确定的。
  4. 合成全局解:通过一系列局部最优解的组合,期望能够得到全局最优解。重要的是,一旦做出了选择,就不会再回头更改这个选择,即便后续发现有更好的选择也是如此。

 3.操作步骤

步骤:

  1. 问题建模:明确问题的边界条件、目标函数以及约束条件。
  2. 子问题划分:将问题分解为更小的子问题,这些子问题应该具有与原问题相同的结构。
  3. 贪心选择:对于每一个子问题,做出一个看起来最优的选择。这一步是贪心算法的核心。
  4. 构造解:将子问题的解组合起来,形成完整问题的一个解。
  5. 验证解:检查构造的解是否满足所有约束条件,并符合目标函数的要求。

 例子:

    假设我们有一个简单的例子,比如选择一组硬币来支付一个给定金额的情况,我们的目标是以        最少数量的硬币完成支付。

  1. 问题建模:设定硬币面值(例如1分、5分、10分、25分)和需要支付的总金额。
  2. 子问题划分:将支付金额逐步减少,每次减少一个硬币面值的金额。支付金额递减的过程,比如从37分开始,减去25分,然后减去10分,再减去1分。
  3. 贪心选择:每次选择面值最大的硬币,只要不超过剩余的支付金额。每次选择硬币的过程:先选25分,再选10分,最后选1分。
  4. 构造解:将选择的所有硬币加起来,构成最终的支付方案。最终选择的硬币组合,即25分+10分+1分。
  5. 验证解:确认所选硬币的总和等于需要支付的金额,并且硬币数量最少。验证过程,确认37分已完全支付并且只用了3枚硬币。

 4.代码模板(以硬币找零问题为例)

python模板:

def min_coins(coins, amount):
    # 对硬币面值进行降序排序
    coins.sort(reverse=True)
    
    # 初始化结果变量
    result = 0
    
    # 遍历每个硬币面值
    for coin in coins:
        # 计算当前硬币可以使用的次数
        count = amount // coin
        # 更新结果
        result += count
        # 减去已经支付的金额
        amount -= count * coin
        
        # 如果金额变为0,则返回结果
        if amount == 0:
            return result
    
    # 如果无法支付,则返回-1
    return -1

# 示例
coins = [1, 5, 10, 25]
amount = 37
print(min_coins(coins, amount))  # 输出: 3

c++模板:

#include <iostream>
#include <vector>
#include <algorithm>

int minCoins(std::vector<int>& coins, int amount) {
    // 对硬币面值进行降序排序
    std::sort(coins.begin(), coins.end(), std::greater<int>());
    
    int result = 0;
    
    // 遍历每个硬币面值
    for (int coin : coins) {
        // 计算当前硬币可以使用的次数
        int count = amount / coin;
        // 更新结果
        result += count;
        // 减去已经支付的金额
        amount -= count * coin;
        
        // 如果金额变为0,则返回结果
        if (amount == 0) {
            return result;
        }
    }
    
    // 如果无法支付,则返回-1
    return -1;
}

int main() {
    std::vector<int> coins = {1, 5, 10, 25};
    int amount = 37;
    std::cout << minCoins(coins, amount) << std::endl;  // 输出: 3
    return 0;
}

5.经典例题

p1223排队接水

题目描述

有 n 个人在一个水龙头前排队接水,假如每个人接水的时间为 Ti,请编程找出这 n 个人排队的一

种顺序,使得 n 个人的平均等待时间最小。

输入格式

第一行为一个整数 n。

第二行 n 个整数,第 i 个整数 Ti 表示第 i 个人的接水时间 Ti。

 输出格式

输出文件有两行,第一行为一种平均时间最短的排队顺序;第二行为这种排列方案下的平均等待时间(输出结果精确到小数点后两位)。

 样例 #1

 样例输入 #1
10 
56 12 1 99 1000 234 33 55 99 812

 样例输出 #1
3 2 7 8 1 4 9 6 10 5
291.90

 提示

1 < n < 1000,1 < ti <10^6,不保证 ti 不重复。

代码:

#include <bits/stdc++.h>
using namespace std;

// 定义结构体
struct ss {
    int num;  // 任务编号
    int time; // 任务时间
} a[1001];  // 结构体数组,最多存储1001个任务

int n;      // 任务总数
double ans = 0.0;  // 存储总的等待时间

// 自定义比较函数,用于排序
bool cmp(const ss &a, const ss &b) {
    return a.time < b.time;  // 按照时间从小到大排序
}

int main() {
    // 读入任务数量
    scanf("%d", &n);

    // 读入每个任务的时间戳
    for (int i = 0; i < n; i++) {
        scanf("%d", &a[i].time);
        a[i].num = i + 1;  // 设置任务编号
    }

    // 根据时间戳排序
    sort(a, a + n, cmp);

    // 计算平均等待时间和输出任务编号
    for (int i = 0; i < n - 1; i++) {
        ans += a[i].time * (n - i - 1);  // 计算每个任务的等待时间并累加
        printf("%d ", a[i].num);  // 输出当前任务的编号
    }
    printf("%d\n", a[n - 1].num);  // 输出最后一个任务的编号

    // 输出平均等待时间,保留两位小数
    printf("%.2lf\n", ans / n);

    return 0;
}
p1803凌乱的yyy / 线段覆盖

 题目背景

快 noip 了,yyy 很紧张!

 题目描述

现在各大 oj 上有 n 个比赛,每个比赛的开始、结束的时间点是知道的。

yyy 认为,参加越多的比赛,noip 就能考的越好(假的)。

所以,他想知道他最多能参加几个比赛。

由于 yyy 是蒟蒻,如果要参加一个比赛必须善始善终,而且不能同时参加 2 个及以上的比赛。

 输入格式

第一行是一个整数 n,接下来 n 行每行是 2 个整数 ai,bi(ai<bi),表示比赛开始、结束的时间。

 输出格式

一个整数最多参加的比赛数目。

 样例 #1

 样例输入 #1
3
0 2
2 4
1 3

 样例输出 #1
2

 提示

- 对于 20% 的数据,n < 10;
- 对于 50% 的数据,n < 10^3;
- 对于 70% 的数据,n < 10^5;
- 对于 100% 的数据,1< n < 10^6,0 < ai < bi <10^6。

代码:

#include <bits/stdc++.h>
using namespace std;

// 定义结构体
struct node {
    int st;  // 开始时间
    int ed;  // 结束时间

    // 自定义比较运算符
    bool operator<(const node &tmp) const {
        if (ed == tmp.ed) {
            return st < tmp.st;  // 如果结束时间相同,则按照开始时间排序
        } else {
            return ed < tmp.ed;  // 否则按照结束时间排序
        }
    }
} a[1000010];  // 结构体数组,最多存储1000010个时间段

int main() {
    int n, x, y, s, cnt;
    cnt = 0;
    s = 0;

    // 读入时间段数量
    cin >> n;

    // 读入每个时间段的开始时间和结束时间
    for (int i = 0; i < n; i++) {
        cin >> a[i].st >> a[i].ed;
    }

    // 按照结束时间排序
    sort(a, a + n);

    // 计算最少覆盖点
    for (int i = 0; i < n; i++) {
        if (a[i].st >= s) {
            s = a[i].ed;  // 更新覆盖点的位置
            cnt++;  // 覆盖点数量加1
        }
    }

    // 输出最少覆盖点的数量
    cout << cnt << endl;

    return 0;
}
p1090[NOIP2004 提高组] 合并果子 / [USACO06NOV] Fence Repair G

题目描述

在一个果园里,多多已经将所有的果子打了下来,而且按果子的不同种类分成了不同的堆。多多决定把所有的果子合成一堆。

每一次合并,多多可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。可以看出,所有的果子经过 n-1 次合并之后, 就只剩下一堆了。多多在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以多多在合并果子时要尽可能地节省体力。假定每个果子重量都为 1 ,并且已知果子的种类 数和每种果子的数目,你的任务是设计出合并的次序方案,使多多耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3 种果子,数目依次为 1 , 2 , 9 。可以先将 1 、 2 堆合并,新堆数目为 3 ,耗费体力为 3 。接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12 ,耗费体力为 12 。所以多多总共耗费体力 =3+12=15 。可以证明 15 为最小的体力耗费值。

 输入格式

共两行。  
第一行是一个整数 n(1<= n <= 10000) ,表示果子的种类数。  

第二行包含 n 个整数,用空格分隔,第 i 个整数 ai(1<=ai<= 20000) 是第 i 种果子的数目。

 输出格式

一个整数,也就是最小的体力耗费值。输入数据保证这个值小于 2^31 。

 样例 #1

 样例输入 #1

1 2 9

 样例输出 #1
15
 提示

对于 30% 的数据,保证有 n < 1000:

对于 50% 的数据,保证有 n < 5000;

对于全部的数据,保证有 n < 10000。

代码:

#include <bits/stdc++.h>
using namespace std;

long long n, a[100010];
int main() {
    // 读入数组长度
    cin >> n;

    // 读入数组元素
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }

    // 将数组从小到大排序
    sort(a + 1, a + n + 1);

    long long ans = 0;

    // 主循环,进行合并操作
    while (true) {
        int i = 1;
        
        // 找到第一个非零元素
        while (a[i] == 0) {
            i++;
        }
        
        // 如果已经只剩下一个非零元素,退出循环
        if (i == n) {
            break;
        } else {
            // 合并当前元素和下一个元素
            a[i] += a[i + 1];
            
            // 累加合并代价
            ans += a[i];
            
            // 移动数组元素,删除已合并的元素
            for (int l = i + 1; l < n; l++) {
                a[l] = a[l + 1];
            }
            
            // 数组长度减一
            n--;
            
            // 再次排序,确保数组有序
            for (int l = i; l < n; l++) {
                if (a[l] > a[l + 1]) {
                    swap(a[l], a[l + 1]);
                }
            }
        }
    }

    // 输出最终答案
    cout << ans << endl;

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值