算法小课堂(五)贪心算法

一、概述

贪心算法是一种常见的算法思想,用于解决优化问题。其基本思想是在每一步选择中都采取当前状态下最优的选择,从而希望能够获得全局最优解。

具体来说,贪心算法通常分为以下步骤:

  1. 定义问题的最优解,通常需要将问题转化为求最大值或最小值;
  2. 选择一个局部最优解,并将其加入到最终解中;
  3. 将局部最优解所对应的子问题规模缩小,并重复步骤2,直到达到最优解。

贪心算法的优点在于其简单、高效,并且可以用于解决许多实际问题,例如:

  • 最小生成树问题;
  • 最短路径问题;
  • 部分背包问题;
  • 区间调度问题;
  • 分糖果问题等。

然而,贪心算法并不总是能够得到全局最优解,因为其仅考虑当前局部最优解,并没有考虑到未来可能出现的情况。因此,在使用贪心算法时需要仔细思考问题本身的性质,并根据具体问题来选择合适的贪心策略。

二、区间问题

2.1区间选点

给定 N 个闭区间 [ai,bi],请你在数轴上选择尽量少的点,使得每个区间内至少包含一个选出的点。

输出选择的点的最小数量。

位于区间端点上的点也算作区间内。

输入格式

第一行包含整数 N,表示区间数。

接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。

输出格式

输出一个整数,表示所需的点的最小数量。

数据范围

1≤N≤105,
−109≤ai≤bi≤109

输入样例:

3
-1 1
2 4
3 5

输出样例:

2

算法小课堂(五)贪心算法_第1张图片

贪心算法

(1)首先将这些区间按照右端点从小到大进行排序,即 bi 升序排序。

(2)初始化一个点 p 为最左端的区间的右端点。

(3)遍历每个区间 [ai, bi],如果 ai > p,则将 p 更新为 bi,计数器加一。

(4)遍历完所有的区间之后,得到的计数器即为所需的最小点数。

#include 
#include 

using namespace std;

const int N = 1e5 + 10;

struct Range  //定义一个结构体Range,表示一个区间
{
    int l, r;  //区间左右端点
    bool operator < (const Range &W) const  //重载运算符,排序时按照右端点从小到大排序
    {
        return r < W.r;
    }
}range[N];

int n;

int main()
{
    scanf("%d", &n);

    for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);

    sort(range, range + n);  //将所有区间按照右端点从小到大排序

    int res = 1, p = range[0].r;  //初始化计数器res为1,p为最左端区间的右端点

    for (int i = 1; i < n; i ++ )  //遍历所有区间
        if (range[i].l > p)  //如果当前区间的左端点比p要大
        {
            res ++ ;  //计数器加一
            p = range[i].r;  //更新p为当前区间的右端点
        }

    printf("%d", res);  //输出所需点的最小数量

    return 0;
}

2.2最大不相交区间数量

给定 N个闭区间 [ai,bi]请你在数轴上选择若干区间,使得选中的区间之间互不相交(包括端点)。

输出可选取区间的最大数量。

输入格式

第一行包含整数 N,表示区间数。

接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。

输出格式

输出一个整数,表示可选取区间的最大数量。

数据范围

1≤N≤105
−109≤ai≤bi≤109

输入样例:

3
-1 1
2 4
3 5

输出样例:

2

算法小课堂(五)贪心算法_第2张图片

 算法(贪心)

可以参考区间选点那题,二者在算法思路上是一致的

(1)将每个区间按照右端点从小到大进行排序
(2)ed表示当前放置在数轴上的点的位置,开始初始化为无穷小,表示没有放置,此时数轴上没有点
(3)依次遍历排序好的区间。如果区间的左端点大于当前放置点的位置,说明当前点无法覆盖区间,则把点的位置更新成该区间的右端点,表示在该区间的右端点放置一个新的点,同时更新点的个数

#include 
#include 

using namespace std;

const int N = 1e5 + 10;

struct Range  //定义一个结构体Range,表示一个区间
{
    int l, r;  //区间左右端点
    bool operator < (const Range &W) const  //重载运算符,排序时按照右端点从小到大排序
    {
        return r < W.r;
    }
}range[N];

int n;

int main()
{
    scanf("%d", &n);

    for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);

    sort(range, range + n);  //将所有区间按照右端点从小到大排序

    int res = 1, p = range[0].r;  //初始化计数器res为1,p为最左端区间的右端点

    for (int i = 1; i < n; i ++ )  //遍历所有区间
        if (range[i].l > p)  //如果当前区间的左端点比p要大
        {
            res ++ ;  //计数器加一
            p = range[i].r;  //更新p为当前区间的右端点
        }

    printf("%d", res);  //输出所需点的最小数量

    return 0;
}

2.3区间分组

给定 N 个闭区间 [ai,bi],请你将这些区间分成若干组,使得每组内部的区间两两之间(包括端点)没有交集,并使得组数尽可能小。

输出最小组数。

输入格式

第一行包含整数 N,表示区间数。

接下来 N行,每行包含两个整数 ai,bi表示一个区间的两个端点。

输出格式

输出一个整数,表示最小组数。

数据范围

1≤N≤105
−109≤ai≤bi≤109

输入样例:

3
-1 1
2 4
3 5

输出样例:

2

算法小课堂(五)贪心算法_第3张图片

#include 
#include 
#include  // C++ STL 提供的优先队列(堆)实现

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    // 重载小于号,按左端点排序
    bool operator< (const Range &W)const
    {
        return l < W.l;
    }
} range[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        scanf("%d%d", &l, &r);
        range[i] = {l, r};
    }

    sort(range, range + n); // 按左端点排序

    // 用小根堆维护所有组的右端点的最大值
    // 堆中的每一个值存的是每个组的右端点的最大值
    priority_queue, greater> heap; // 创建一个小根堆(优先队列)
    for (int i = 0; i < n; i ++ )
    {
        auto r = range[i];
        // 如果堆为空或者堆的最小右端点 >= 现在区间的左端点,则说明有交集,不能合并,需要新开一个堆
        if (heap.empty() || heap.top() >= r.l) heap.push(r.r);
        // 否则说明至少与最小的右端点的组没有交集,将它放到右端点最小的组里去
        else
        {
            heap.pop(); // 弹出这个区间的最小右端点,插入当前区间的右端点,即将其更新了
            heap.push(r.r);
        }
    }

    // 输出组的数量
    printf("%d\n", heap.size());

    return 0;
}

有若干个活动,第i个活动开始时间和结束时间是【si,sj】同一个教室安排的活动之间不能交叠,求要安排所有活动,少需要几个教室?

有时间冲突的活动不能安排在同一间教室,与该问题的限制条件相同,即最小需要的教室个数即为该题答案。

我们可以把所有开始时间和结束时间排序,遇到开始时间就把需要的教室加1,遇到结束时间就把需要的教室减1,在一系列需要的教室个数变化的过程中,峰值就是多同时进行的活动数,也是我们至少需要的教室数。

#include 
#include 

using namespace std;

const int N = 100100;

int n;
int b[2 * N], idx; // 将左右端点拆成两个数,左端点是偶数,右端点是奇数,存储在数组 b 中

int main()
{
    scanf ("%d", &n);
    for(int i = 0; i < n; i ++)
    {
        int l, r;
        scanf("%d %d", &l, &r);
        b[idx ++] = l * 2; // 标记左端点为偶数
        b[idx ++] = r * 2 + 1; // 标记右端点为奇数
    }

    sort(b, b + idx); // 将所有端点按大小排序

    int res = 1, t = 0; // res 存储最终答案,t 维护当前线段数量
    for(int i = 0; i < idx; i ++)
    {
        if(b[i] % 2 == 0) t ++; // 如果是偶数,则是左端点,将 t 加 1
        else t --; // 否则是奇数,是右端点,将 t 减 1
        res = max(res, t); // 更新答案
    }

    printf ("%d\n", res); // 输出答案
    return 0;
}

2.4区间覆盖

给定 N 个闭区间 [ai,bi]以及一个线段区间 [s,t],请你选择尽量少的区间,将指定线段区间完全覆盖。

输出最少区间数,如果无法完全覆盖则输出 −1−1。

输入格式

第一行包含两个整数 s和 t,表示给定线段区间的两个端点。

第二行包含整数 N,表示给定区间数。

接下来 N 行,每行包含两个整数 ai,bi,表示一个区间的两个端点。

输出格式

输出一个整数,表示所需最少区间数。

如果无解,则输出 −1。

数据范围

1≤N≤105
−109≤ai≤bi≤109
−109≤s≤t≤109

输入样例:

1 5
3
-1 3
2 4
3 5

输出样例:

2
#include 
#include 

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const  // 自定义结构体排序规则
    {
        return l < W.l;
    }
}range[N]; // 定义一个结构体数组range,其中l和r分别表示每个区间的左右端点

int main()
{
    int st, ed; 
    scanf("%d%d", &st, &ed); // 输入起点和终点
    scanf("%d", &n); // 输入区间的个数
    for (int i = 0; i < n; i ++ )
    {
        int l, r;
        scanf("%d%d", &l, &r); // 输入每个区间的左右端点
        range[i] = {l, r}; // 存储区间信息到结构体数组中
    }

    sort(range, range + n); // 对结构体数组进行排序,按照左端点从小到大排序

    int res = 0; // 记录能够覆盖的区间个数
    bool success = false; // 标记是否能够覆盖终点ed
    for (int i = 0; i < n; i ++ )
    {
        int j = i, r = -2e9; // j表示当前区间的右端点能够覆盖的最远位置,r表示当前能够覆盖的最远位置,初始值为负无穷
        while (j < n && range[j].l <= st) // 遍历所有左端点在起点st左边或等于st的区间,更新右端点能够覆盖的最远位置
        {
            r = max(r, range[j].r);
            j ++ ;
        }

        if (r < st) // 如果找不到任何一个区间能够覆盖st,则无法到达终点,返回-1
        {
            res = -1;
            break;
        }

        res ++ ; // 能够找到一个区间覆盖st,将res加1
        if (r >= ed) // 如果该区间能够覆盖ed,则能够到达终点,将success标记为true,结束循环
        {
            success = true;
            break;
        }

        st = r; // 更新起点为当前能够覆盖的最远位置
        i = j - 1;  // 更新i的位置,从当前已经覆盖的区间后面开始查找下一个区间
    }

    if (!success) res = -1; // 如果无法到达终点,则返回-1
    printf("%d\n", res); // 输出能够覆盖的区间个数

    return 0;
}

三、Huffman树

3.1合并果子

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

达达决定把所有的果子合成一堆。

每一次合并,达达可以把两堆果子合并到一起,消耗的体力等于两堆果子的重量之和。

可以看出,所有的果子经过 n−1 次合并之后,就只剩下一堆了。

达达在合并果子时总共消耗的体力等于每次合并所耗体力之和。

因为还要花大力气把这些果子搬回家,所以达达在合并果子时要尽可能地节省体力。

假定每个果子重量都为 1,并且已知果子的种类数和每种果子的数目,你的任务是设计出合并的次序方案,使达达耗费的体力最少,并输出这个最小的体力耗费值。

例如有 3种果子,数目依次为 1,2,9。

可以先将 1、2 堆合并,新堆数目为 3,耗费体力为 3。

接着,将新堆与原先的第三堆合并,又得到新的堆,数目为 12,耗费体力为 12。

所以达达总共耗费体力3+12=15。

可以证明 15 为最小的体力耗费值。

输入格式

输入包括两行,第一行是一个整数 n,表示果子的种类数。

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

输出格式

输出包括一行,这一行只包含一个整数,也就是最小的体力耗费值。

输入数据保证这个值小于 231231。

数据范围

1≤n≤10000
1≤ai≤20000

输入样例:

3 
1 2 9 

输出样例:

15

算法小课堂(五)贪心算法_第4张图片

#include 
#include 

using namespace std;

int n;
priority_queue, greater > q; //定义一个名为q的小根堆

int main()
{
    scanf("%d", &n);

    while (n --)
    {
        int x;
        scanf("%d", &x);
        q.push(x); //依序插入果子堆
    }

    int res = 0;
    while (q.size() > 1) //当还没有只剩一个果子堆的时候,即还没有合并完的时候
    {
        int a = q.top(); q.pop(); //取第一小值
        int b = q.top(); q.pop(); //取第二小值
        int c = a + b; //消耗体力值 与 新堆果子数

        res += c;
        q.push(c); //插入新果子堆
    }

    printf("%d\n", res);

    return 0;
}

 四、排序不等式

4.1排队打水

有 n 个人排队到 1 个水龙头处打水,第 i 个人装满水桶所需的时间是 ti,请问如何安排他们的打水顺序才能使所有人的等待时间之和最小?

输入格式

第一行包含整数 n。

第二行包含 n 个整数,其中第 i 个整数表示第 i个人装满水桶所花费的时间 ti。

输出格式

输出一个整数,表示最小的等待时间之和。

数据范围

1≤n≤105
1≤ti≤104

输入样例:

7
3 6 1 4 2 5 7

输出样例:

56

算法小课堂(五)贪心算法_第5张图片

#include 
#include  //引入STL的sort函数,方便排序

using namespace std;

const int N = 1e5 + 10; //常量N表示数组t的最大长度

typedef long long LL; //将long long类型定义为LL,方便使用

int t[N]; //定义数组t,用来存放输入的n个数

int main()
{
    int n;
    scanf("%d", &n); //输入n的值
    for (int i = 0; i < n; i ++ )   scanf("%d", &t[i]); //输入n个数,并存放到数组t中

    sort(t, t + n); //对数组t进行排序,从小到大排序

    LL res = 0; //定义res表示结果,并初始化为0
    for (int i = 0; i < n; i ++ )  res += t[i] * (n - i - 1); //计算结果,累加每个数t[i]与其后面所有数的乘积,累加到res中

    printf("%lld\n", res); //输出结果

    return 0;
}

 4.2货仓选址

在一条数轴上有 N家商店,它们的坐标分别为 A1∼AN。

现在需要在数轴上建立一家货仓,每天清晨,从货仓到每家商店都要运送一车商品。

为了提高效率,求把货仓建在何处,可以使得货仓到每家商店的距离之和最小。

输入格式

第一行输入整数 N。

第二行 N 个整数 A1∼AN

输出格式

输出一个整数,表示距离之和的最小值。

数据范围

1≤N≤100000
0≤Ai≤40000

输入样例:

4
6 2 9 1

输出样例:

12

算法小课堂(五)贪心算法_第6张图片

#include  // C++ 标准库中的算法库,包括排序算法

using namespace std;

const int N = 100005; // 声明一个常量

int n, res; // n 表示数组长度,res 表示最终结果
int a[N]; // 声明一个长度为 N 的整型数组 a

int main()
{
    scanf("%d", &n); // 输入数组长度
    for (int i = 0; i < n; i ++ ) scanf("%d", &a[i]); // 输入数组元素

    sort(a, a + n); // 对数组 a 进行升序排序

    // 计算数组 a 中每个元素与中位数之间的距离的绝对值之和
    for (int i = 0; i < n; i ++ ) res += abs(a[i] - a[n >> 1]);

    printf("%d\n", res); // 输出最终结果

    return 0;
}

五、其他常规问题

5.1找零钱

假设你在一家商店购买商品,需要支付一定的现金,并希望得到尽可能少的硬币找零。这个问题可以用贪心算法来解决。下面是一个例子:

假设你需要支付15元,你手上有1元、2元、5元的硬币,如何得到最少数量的硬币找零呢?

#include 
using namespace std;

int findChange(int amount, int coins[], int n) {
    int count = 0;
    for (int i = n - 1; i >= 0; i--) {
        while (amount >= coins[i]) {
            amount -= coins[i];
            count++;
        }
    }
    return count;
}

int main() {
    int coins[] = {5, 2, 1};  // 硬币面额
    int n = sizeof(coins) / sizeof(coins[0]);  // 硬币种类数
    int amount = 15;  // 需要找零的金额
    int numCoins = findChange(amount, coins, n);  // 找零所需最小硬币数
    cout << "最少需要" << numCoins << "个硬币来找零" << endl;
    return 0;
}

5.2活动安排问题

活动安排问题(贪心)

有若干个活动,第i个开始时间和结束时间是[Si,fi),同一个教室安排的活动之间不能交叠,求要安排所有活动,最少需要几个教室?

Input 第一行一个正整数n (n <= 10000)代表活动的个数。 第二行到第(n + 1)行包含n个开始时间和结束时间。 开始时间严格小于结束时间,并且时间都是非负整数,小于1000000000

Output 一行包含一个整数表示最少教室的个数。

Sample Input 3 1 2 3 4 2 9

Sample Output 2

// 活动安排问题(贪心)
#include 
#include 

using namespace std;

const int N = 100100;

int n;
int b[2 * N], idx; // 将左右端点拆成两个数,左端点是偶数,右端点是奇数,存储在数组 b 中

int main()
{
    scanf ("%d", &n); // 输入活动的个数
    for(int i = 0; i < n; i ++)
    {
        int l, r;
        scanf("%d %d", &l, &r); // 输入每个活动的开始时间和结束时间
        b[idx ++] = l * 2; // 将左端点乘以2作为偶数存储在数组b中
        b[idx ++] = r * 2 + 1; // 将右端点乘以2加1作为奇数存储在数组b中
    }

    sort(b, b + idx); // 将所有端点按大小排序

    int res = 1, t = 0; // res 存储最终答案,t 维护当前线段数量
    for(int i = 0; i < idx; i ++)
    {
        if(b[i] % 2 == 0) t ++; // 如果是偶数,则是左端点,将 t 加 1
        else t --; // 否则是奇数,是右端点,将 t 减 1
        res = max(res, t); // 更新答案,取当前的最大值
    }

    printf ("%d\n", res); // 输出答案,即教室数量
    return 0;
}

5.3 最优装载问题

任务描述 有一天,海盗截获了一艘装满各种各样古董的货船,每一件古董都价值连城,一但打碎就失去了价值,虽然海盗船足够大,但载重量为C,每件古董的重量为Wi,海盗们如何把尽量多的宝贝装上海盗船呢?

输入格式 第一行输入n,c,代表有n个古董和船的重量 第二行输入n个数,代表每个古董的重量。 1

输出格式 输出一个数,代表最多装多少个古董

输入样例 8 30 4 10 7 11 3 5 14 2

输出样例 5

#include 
#include 
using namespace std;

const int MAXN = 10005;
int w[MAXN];

bool cmp(int a, int b) {
    return a > b;
}

int main() {
    int n, c;
    cin >> n >> c;
    for (int i = 1; i <= n; i++) {
        cin >> w[i];
    }
    sort(w + 1, w + n + 1, cmp);
    int sum = 0, cnt = 0;
    for (int i = 1; i <= n; i++) {
        if (sum + w[i] <= c) {
            sum += w[i];
            cnt++;
        } else {
            break;
        }
    }
    cout << cnt << endl;
    return 0;
}

你可能感兴趣的:(算法,数据结构)