算法——贪心算法

博客围绕贪心算法展开,介绍了其在区间选点、最大不相交区间数量、区间分组、区间覆盖等区间问题,以及合并果子、排队打水、货仓选址、耍杂技的牛等实际问题中的应用,给出了问题描述、输入输出格式和贪心算法思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1 区间选点

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

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

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

输入格式

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

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

输出格式

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

数据范围

1 ≤ N ≤ 1 0 5 1 \le N \le 10^5 1N105

− 1 0 9 ≤ a i ≤ b i ≤ 1 0 9 -10^9 \le a_i \le b_i \le 10^9 109aibi109

输入样例

3
-1 1
2 4
3 5

输出样例

2

y总的思路:

对所有的区间按照右端点从小到大排序

遍历这些区间,如果当前区间已经包含这个点(右端点),则直接pass;否则选择当前区间的右端点

自己的想法:

对所有的区间按照左端点排序

遍历区间,维护一个 [ l e f t , r i g h t ] [left, right] [left,right] 区间表示重叠部分,最后的结果就是有多少个这样的区间。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;
int n;
struct Range
{
    int l, r;
    bool operator< (const Range& w)const          //自定义排序
    {
        return r < w.r;
    }
}range[N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        int l, r;
        cin >> l >> r;
        range[i] = {l, r};
    }
    
    sort(range, range + n);
    
    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i++)
    {
        if (range[i].l > ed)                   //只有新的区间的左端点严格大于 ed,才会更新ed
        {
            res ++;
            ed = range[i].r;
        }
    }
    
    cout << res << endl;
    
    return 0;
}

2 最大不相交区间数量

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

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

输入格式

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

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

输出格式

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

数据范围

1 ≤ N ≤ 1 0 5 1 \le N \le 10^5 1N105

− 1 0 9 ≤ a i ≤ b i ≤ 1 0 9 -10^9 \le a_i \le b_i \le 10^9 109aibi109

输入样例

3
-1 1
2 4
3 5

输出样例

2
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range& w) const
    {
        return r < w.r;
    }
}range[N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        int l, r;
        cin >> l >> r;
        range[i] = {l, r};
    }
    
    sort(range, range + n);
    
    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i++)              //和区间选点的思路一样,算法也完全一致
    {
        if (range[i].l > ed)
        {
            res ++;
            ed = range[i].r;
        }
    }
    
    cout << res << endl;
    
    return 0;
}

3 区间分组

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

输出最小组数。

输入格式

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

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

输出格式

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

数据范围

1 ≤ N ≤ 1 0 5 1 \le N \le 10^5 1N105

− 1 0 9 ≤ a i ≤ b i ≤ 1 0 9 -10^9 \le a_i \le b_i \le 10^9 109aibi109

输入样例

3
-1 1
2 4
3 5

输出样例

2

1、将所有区间按照左端点从小到大排序

2、从前往后处理每个区间

​ 判断能否将其放到某个现有的组中 range[i].l > Max_r

​ 1 如果不存在(后面的区间全遍历)这样的区间,开一个新的组

​ 2 如果存在这样的区间,将其放进去,并更新当前组的Max_r

动态维护最小值,用堆来维护

#include <iostream>
#include <algorithm>
#include <queue>

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()
{
 	cin >> n;
    for (int i = 0; i < n; i++)
    {
        int l, r;
        cin >> l >> r;
        range[i] = {l, r};
    } 
    
    sort(range, range + n);
    
    priority_queue<int, vector<int>, greater<int>> heap;   //堆来存放每个组的右端点,
    for (int i = 0; i < n; i++)
    {
        auto t = range[i];
        if (heap.empty() || heap.top() >= t.l) heap.push(t.r);    //如果heap中的最小的组的右端点,比当前的l还要大,需要另开新组
        else 
        {
            heap.pop();              //相当于将这个区间添加到右端点值最小的那个组中
            heap.push(t.r);
        }
    }
    
    cout << heap.size() << endl;
    
    return 0;
}

4 区间覆盖

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

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

输入格式

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

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

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

输出格式

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

如果无解,则输出-1。

数据范围

1 ≤ N ≤ 1 0 5 1 \le N \le 10^5 1N105

− 1 0 9 ≤ a i ≤ b i ≤ 1 0 9 -10^9 \le a_i \le b_i \le 10^9 109aibi109

− 1 0 9 ≤ s ≤ t ≤ 1 0 9 -10^9 \le s \le t \le 10^9 109st109

输入样例

1 5
3
-1 3
2 4
3 5

输出样例

2

1、先将所有区间按照左端点从小到大排序

2、从前往后依次枚举每个区间,在所有能覆盖开始(start)端点的区间中,选择右端点值最大的区间 ,选完之后,将start更新成右端点的最大值

#include <iostream>
#include <algorithm>
#include <queue>

using namespace std;

const int N = 100010;

int n, st, ed;
struct Range
{
    int l, r;
    bool operator< (const Range& w) const
    {
        return l < w.l;
    }
}range[N];

int main()
{
    cin >> st >> ed;
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        int l, r;
        cin >> l >> r;
        range[i] = {l, r};
    } 
    
    sort(range, range + n);
    
    int res = 0, i = 0;
    bool success = false;
    while(i < n)
    {
        int j = i, r = -2e9;                   //双指针算法,遍历所有左端点在start左边,右端点最大的值为多少
        while (j < n && range[j].l <= st)
        {
            r = max(r, range[j].r);
            j ++;
        }
        if (r < st)                           //最大的右端点都小于st,说明无解
        {
            res = -1;
            break;
        }
        
        res ++;
        if (r >= ed)
        {
            success = true;                  //只有这里是成功的
            break;
        }
        
        st = r;                      //更新st端点
        i = j;
    }
    
    if (!success) res = -1;
    cout << res << endl;
    
    return 0;
}

5 合并果子

哈夫曼树的典型应用,之前的动态规划也可以做

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

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

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

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

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

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

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

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

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

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

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

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

输入格式

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

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

输出格式

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

输入数据保证这个值小于 2 31 2^{31} 231

输入样例

3
1 2 9

输出样例

15

贪心的方法:每次从所有节点中选择值最小的两个节点,将它们合并并插入到剩下的节点中,然后接着寻找最小的两个节点。

最后得到的结果就是合并耗费体力值最小的。

Haffman树,关于上面的方法的正确性,建议自己看acwing视频(^ 0 ^)

#include <iostream>
#include <queue>
#include <algorithm>

using namespace std;

int main()
{
    int n;
    cin >> n;
    
    priority_queue<int, vector<int>, greater<int>> heap;
    while (n--)
    {
        int x;
        cin >> x;
        heap.push(x);
    }
    
    int res = 0;
    while (heap.size() > 1)
    {
        int a = heap.top(); heap.pop();
        int b = heap.top(); heap.pop();
        heap.push(a +b);
        res += a + b;
    }
    
    cout << res << endl;
}

priority_queue对于自定义的数据结构进行排序

#include <queue>
using namespace std;

struct node
{
    int x, y;
}arr[200];

struct cmp1
{
  bool operator () (const node &a, const node &b) const
  {
      return a.x < b.x;                    //这样优先队列排出来的是 从大到小
  }
};

int main()
{
    priority_queue<node, vector<node>, cmp1> heap1;
    
    return 0;
}

6 排队打水

排序不等式问题。

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

输入格式

第一行包含整数 n n n

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

输出格式

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

数据范围

1 ≤ n ≤ 1 0 5 1 \le n \le 10^5 1n105

1 ≤ t i ≤ 1 0 4 1 \le t_i \le 10^4 1ti104

输入样例

7
3 6 1 4 2 5 7

输出样例

56

将花费时间从小到大排序。

假设安排了第 i i i 个人在第一个打水,那么后面的人需要等待他 n - 1打水的时间,显然这个打水的时间越小,最后的值也越小。

#include <iostream>
#include <algorithm>

using namespace std;

typedef long long LL;             //用 long long ,有可能爆int

const int N = 100010;

int t[N];
int n;

int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> t[i];
    }
    
    sort(t, t + n);
    
    LL res = 0;
    for (int i = 0; i < n - 1; i++)
    {
        res += t[i] * (n - i - 1);          
    }
    
    cout << res << endl;
}

7 货仓选址

绝对值不等式问题。

在一条数轴上有 N N N 家商店,它们的坐标分别为 A 1 A_1 A1 ~ A N A_N AN

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

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

输入格式

第一行输入整数 N N N

第二行 N N N 个整数 A 1 A_1 A1 ~ A N A_N AN

输出格式

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

数据范围

1 ≤ N ≤ 100000 1 \le N \le 100000 1N100000
0 ≤ A i ≤ 40000 0 \le A_i \le 40000 0Ai40000

输入样例

4
6 2 9 1

输出样例

12

排序之后,将仓库安排在最中间就好了。

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;
int a[N];
int n;

int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        cin >> a[i];
    }
    
    sort(a, a + n);
    
    int res = 0, mid = n >> 1;
    for (int i = 0; i < mid; i++) 
        res += a[n - i - 1] - a[i];
    
    cout << res << endl;
}

8 耍杂技的牛

农民约翰的 N N N 头奶牛(编号为 1... N 1...N 1...N )计划逃跑并加入马戏团,为此它们决定练习表演杂技。

奶牛们不是非常有创意,只提出了一个杂技表演:

叠罗汉,表演时,奶牛们站在彼此的身上,形成一个高高的垂直堆叠。

奶牛们正在试图找到自己在这个堆叠中应该所处的位置顺序。

N N N 头奶牛中的每一头都有着自己的重量 W i W_i Wi 以及自己的强壮程度 S i S_i Si

一头牛支撑不住的可能性取决于它头上所有牛的总重量(不包括它自己)减去它的身体强壮程度的值,现在称该数值为风险值,风险值越大,这只牛撑不住的可能性越高。

您的任务是确定奶牛的排序,使得所有奶牛的风险值中的最大值尽可能的小。(比较 N N N 个风险系数)

输入格式

第一行输入整数 N N N ,表示奶牛数量。

接下来 N N N 行,每行输入两个整数,表示牛的重量和强度,第 i i i 行表示第 i i i 头牛的重量 W i W_i Wi 和强度 S i S_i Si

输出格式

输出一个整数,表示最大风险值的最小可能值。

数据范围

1 ≤ N ≤ 50000 1 \le N \le 50000 1N50000

1 ≤ W i ≤ 10 , 000 1 \le W_i \le 10,000 1Wi10,000

1 ≤ S i ≤ 1 , 000 , 000 , 000 1 \le S_i \le 1,000,000,000 1Si1,000,000,000

输入样例

3
10 3
2 5
3 3

输出样例

2

按照 W i + S i W_i + S_i Wi+Si 从小到大的顺序去排,最大的危险系数一定是最小的。

证明这样的做法是正确的。我们用反证法来证明,

贪心得到的结果 >= 最优值

贪心得到的结果 <= 最优值

第一种,显然是成立的,最优值一定最小

第二种情况,贪心的做法,si + wi排序,若最优值不是采用我们的做法,则一定存在 W i + S i > W i + 1 + S i + 1 W_i + S_i > W_{i+1} + S_{i + 1} Wi+Si>Wi+1+Si+1

对最优值的情况做交换可以得到如下:(其他位置的危险系数不会变化)

第 i 个位置的牛第 i + 1个位置的牛
交换前 w 1 + w 2 + . . . + w i − 1 − s i w_1 + w_2 + ... + w_{i-1} - s_i w1+w2+...+wi1si w 1 + w 2 + . . . + w i − s i + 1 w_1 + w_2 + ... + w_i - s_{i+1} w1+w2+...+wisi+1
交换后 w 1 + w 2 + . . . + w i − 1 − s i + 1 w_1 + w_2 + ... + w_{i-1} - s_{i+1} w1+w2+...+wi1si+1 w 1 + w 2 + . . . + w i − 1 + w i + 1 − s i w_1 + w_2 + ... + w_{i-1} + w_{i + 1} - s_i w1+w2+...+wi1+wi+1si

四个值都去掉 w 1 + w 2 + . . . + w i − 1 w_1 + w_2 + ... + w_{i-1} w1+w2+...+wi1 ,然后全部加上 s i + s i + 1 s_i + s_{i + 1} si+si+1

第 i 个位置的牛第 i + 1个位置的牛
交换前 s i + 1 s_{i+1} si+1 w i + s i w_i + s_i wi+si
交换后 s i s_i si w i + 1 + s i + 1 w_{i + 1} + s_{i + 1} wi+1+si+1

显然最优值的情况中,如果交换了某两项,最大的危险系数不会变大 w i + s i > w i + 1 + s i + 1 > s i  and  s i + 1 w_i + s_i > w_{i+1} + s_{i+1} > s_i \ \text{and} \ s_{i+1} wi+si>wi+1+si+1>si and si+1

因而,贪心得到的结果 <= 最优值

#include <iostream>
#include <algorithm>

using namespace std;

typedef pair<int, int> PII;

const int N = 50010;

int n;
PII cow[N];

int main()
{
    cin >> n;
    for (int i = 0; i < n; i++)
    {
        int w, s;
        cin >> w >> s;
        cow[i] = {w + s, w};             //这样存储省去了自定义排序,值得学习
    }
    
    sort(cow, cow + n);
    
    int res = -2e9, sum = 0;
    
    for (int i = 0; i < n; i++)
    {
        res = max(res, sum - cow[i].first + cow[i].second);
        sum += cow[i].second;
    }
    
    cout << res << endl;
    
    return 0;
}
<think>好的,我需要帮用户理解贪心算法在装船问题中的应用和实现。首先,我得回忆装船问题的基本概念,以及贪心算法的原理。装船问题要求在不超过货船最大载重量的情况下,选择货物使得总价值最大。贪心算法在这里的应用通常是基于某种局部最优的选择,比如价重比。 根据用户提供的引用,问题描述是选择若干货物,使得总重量不超过M,而总价重比最大。这里的价重比指的是每单位重量的价值,即p_i/w_i。所以,贪心策略应该是优先选择价重比高的货物,直到载重量用完。 接下来,我需要整理实现步骤。首先计算每种货物的价重比,然后按照这个比例从高到低排序。排序后依次选取货物,直到无法再装下更多的货物。这里需要注意,可能需要部分装载某一种货物,但根据引用中的输入描述,p_i是w_i的整数倍,可能意味着每种货物可以分割,或者必须整件装载?需要仔细看问题要求。 查看引用[4],输入描述中提到p_i是w_i的整数倍,这可能意味着货物可以按整数倍分割,比如如果选择部分重量,价值也按比例计算。但问题输出是整数,所以可能需要全部装载或者不装,但根据价重比的计算方式,可能允许部分装载。不过,根据代码示例中的做法,用户提供的引用[1]提到使用冒泡排序按价重比排序,然后选择货物直到载重用完,可能这里的货物是可以分割的,即可以取部分重量,这样总价值就是按照价重比计算的。比如,如果剩余载重不足以装下全部某货物,就装部分,此时价值为剩余载重乘以价重比。 然后,实现步骤包括计算价重比、排序、贪心选择。代码中的排序部分引用[1]指出冒泡排序效率低,建议改用快速排序,但装船问题中货物种类固定为10种,所以冒泡排序可能足够,但作为一般情况可能需要更高效的算法。 用户可能想知道具体的实现代码,但根据问题中的引用,代码示例中的结构可能涉及结构体存储货物信息,计算价重比,排序,然后遍历。例如,定义一个货物结构体,包含p、w、c(价重比),然后排序,再累加。 需要注意的问题包括排序的正确性,如何处理剩余载重,以及是否允许部分装载。根据引用中的问题描述,输出是最大价值,而输入中的p_i是w_i的整数倍,可能允许部分装载,但需要确认。例如,若货物A有w=2吨,p=4,价重比2。如果剩余载重为1吨,那么可以装0.5个货物A,得到价值1吨×2=2。但根据输入p_i是w_i的整数倍,这样分割后价值可能还是整数。 在代码实现中,引用[1]提到使用冒泡排序,但可以优化为快速排序。因此,在示例代码中,可能使用快速排序来提高效率,尤其是当货物种类较多时。 总结回答的结构应该是:问题描述、贪心策略(按价重比排序)、实现步骤、代码示例、优化建议、应用场景,以及潜在问题和注意事项。同时,生成相关问题,如与其他算法对比、时间复杂度和处理不可分割货物的情况等。 需要确保所有数学公式用$...$,比如价重比表示为$c_i = \frac{p_i}{w_i}$,独立公式用$$...$$。例如,总价值的计算公式可能需要单独成段。 最后,根据引用中的信息,正确添加引用标记,如引用[1]提到使用冒泡排序,可以将其放在对应段落末尾。同时,确保回答使用中文,代码语法正确,结构清晰。</think>### 贪心算法在装船问题中的实现与应用 #### 一、问题描述 装船问题要求从$N$种货物中挑选若干吨装入货轮,**总重量不超过$M$吨**,且**总价值最大化**。引用中明确说明,货物可分割(因$p_i$是$w_i$的整数倍),因此允许选择部分货物[^4]。 #### 二、贪心策略设计 贪心算法的核心是**价重比优先**,即优先选择$\text{价重比} = \frac{\text{价值}}{\text{重量}}$最高的货物。这一策略的数学表示为: $$c_i = \frac{p_i}{w_i}$$ 通过按$c_i$降序排序,逐步装载直至载重$M$耗尽[^1]。 #### 三、实现步骤 1. **计算价重比**:遍历所有货物,计算$c_i = p_i / w_i$ 2. **排序**:按$c_i$从高到低对货物排序(冒泡排序或更优算法) 3. **贪心选择**:依次装载货物,若当前货物重量$\leq$剩余载重,则全装;否则装部分[^3] #### 四、代码实现(Python) ```python def load_ship(M, goods): # 计算价重比并排序 sorted_goods = sorted(goods, key=lambda x: x[0]/x[1], reverse=True) total_value = 0 remaining = M for p, w in sorted_goods: if remaining >= w: total_value += p remaining -= w else: total_value += (p / w) * remaining break # 载重已满 return int(total_value) # 因p_i是w_i整数倍,结果必为整数 # 示例输入:M=100, goods格式为[(p1, w1), (p2, w2), ...] print(load_ship(100, [(60, 10), (120, 30), (100, 20)])) # 输出:240 ``` #### 五、优化与注意事项 1. **排序效率**:当$N$较大时,冒泡排序$O(n^2)$效率低,可改用快速排序$O(n \log n)$[^1] 2. **输入约束**:$p_i$是$w_i$的整数倍,确保部分装载时价值仍为整数 3. **空间优化**:无需存储完整结构体,直接使用元组排序 #### 六、应用场景 1. 物流运输中的**集装箱装载优化** 2. 资源分配问题(如云计算资源调度) 3. 背包问题的简化版本(可分割物品)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值