Contest100000584 - 《算法笔记》4.4小节——算法初步->贪心

常用模板

贪心法是求解一类最优化问题的方法,它总是考虑在当前状态下局部最优(或较优) 的策略,来使全局的结果达到最优(或较优)。显然,如果采取较优而非最优的策略,得到的结果也无法是最优的。事实上,在现实应用中,最优策略往往可能不存在或者是很难被想到。但如果希望获得最优结果,则要求中间的每步策略都是最优的,因此严谨使用贪心法来求解最优化问题需要对采取的策略进行证明。 证明的一般思路是使用反证法及数学归纳法,即假设策略不能导致最优解,然后通过一系列的推导来得到矛盾,以此证明策略是最优的,最后再用数学归纳法保证全局最优。不过就平常使用来说,也许没有时间,或者不太容易对想到的策略进行严谨的证明(贪心的证明往往比贪
心本身更难)。
区间贪心
区间不相交问题:给出N个开区间(x, y),从中选择尽可能多的开区间,使得这些开区间两两没有交集。
首先考虑最简单的情况,如果开区间 I 1 I_1 I1被开区间 I 2 I_2 I2包含,如图a所示,那么显然 I 1 I_1 I1是更好的选择。因为 I 1 I_1 I1的跨度更小,与其他区间相交的概率也更小。
接下来把所有开区间按左端点x从大到小排序,左端点相同的按右端点从小到大排序,可以得到图b所示区间排序。实际题目中可能有更多的点,这里只举一个小例子。由于前面的排序选择,使得被包含的区间一定在包含它的区间的上方。如果去掉被包含的开区间,那么剩下的开区间一定满足 y 1 > y 2 > y 3 > y 4 y_1>y_2>y_3>y_4 y1>y2>y3>y4,如图b所示(提醒一下这里的y是右端点不是纵坐标)。现在考虑如何选取区间,通过观察 I 1 I_1 I1 I 2 I_2 I2可以发现, I 1 I_1 I1左边是有有一部分被 I 2 I_2 I2给包含的,而右边是没有和其他任何区间有交集的。如果我们选择 I 1 I_1 I1,就可以使得被选取区间尽量往“右”扩展,这就增大了可选择空间的范围,也增大了可选择的数量。这道理和a是相同的。因此针对如图b所示这种情况,我们总是选择左端点最大的区间。
Contest100000584 - 《算法笔记》4.4小节——算法初步->贪心_第1张图片
下面给出模板:

#include 
#include 
using namespace std;

const int maxn = 110;

struct Inteval
{
     
    int x, y; // 开区间的左右端点
};

bool cmp(Inteval a, Inteval b)
{
     
    if (a.x != b.x)
        return a.x > b.x; // 先按左端点从大到小排序
    else
        return a.y < b.y; // 左端点相同的按右端点从小到大排序
}

int main()
{
     
    int n;
    while (cin >> n, n)
    {
     
        for (int i = 0; i < n; ++i)
            cin >> I[i].x >> I[i].y;
        sort(I, I + n, cmp); // 把区间排序

        // ans记录不相交区间个数,lastX记录上一个被选中区间的左端点
        int ans = 1, lastX = I[0].x;
        for (int i = 1; i < n; ++i)
        {
     
            if (I[i].y <= lastX) // 如果该区间右端点在lastX左边
            {
     
                lastX = I[i].x; // 以I[i]作为新选中的区间
                ++ans;
            }
        }
        cout << ans << endl;
    }

    return 0;
}

值得注意的是,如果将前面的讨论反过来,总是选择右端点最小的区间的策略也是可行的。 与上述代码的区别就在于cmp函数以及lastX记录上一个被选中区间的右端点。
与这个问题类似的是区间选点问题:给出N个闭区间[x, y],求最少需要确定多少个点,才能使每个闭区间中都至少存在一个点。 换句话说就是找到一个点集,使得所有区间中都能至少包含一个在点集中的点。比如对于闭区间[1, 4]、[2, 6]、[5, 7]、[6, 9]来说,最少需要两个点才能保证每个闭区间内都至少有一个点,比如3、7就能满足。当然这答案肯定是不唯一的,但是最少需要几个点是唯一的。
事实上,这个问题和区间不相交问题的策略是一致的。请看上图a,同样的,如果闭区间 I 1 I_1 I1被闭区间 I 2 I_2 I2包含,那么在 I 1 I_1 I1中取点肯定可以保证这个点一定在 I 2 I_2 I2内。然后同样的,把所有闭区间的左端点从大到小排序,去掉区间包含的情况,就能得到图b。显然,由于每个闭区间中都需要存在一个点,因此对左端点最大的区间 I 1 I_1 I1来说,取哪个点可以让它尽可能多地覆盖其他区间?很明显,只要取左端点就可以,这样这个点就能覆盖到尽可能多的区间。区间选点问题的代码只需要把区间不想交问题代码中的“I[i].y <= lastX”改为“I[i].y < lastX”即可。
我来解释一下为什么。在区间不相交问题中,由于区间都是开区间,假设对于开区间(x1, y1)和开区间(x2, y2)已经排好序,x1 > x2,但是y2 = x1。即使是这样,我们也要将闭区间(x2, y2)选进去,因为它们尽管有端点相等,但实际上是不相交的。在区间选点问题中,由于区间都是闭区间,而我们希望找到尽可能少的点覆盖尽可能多的区间,所以如果某两个闭区间的端点相等了,我们只需要取端点就能覆盖这两个闭区间。也就没必要在某一个区间中继续选点了,这就是为什么不能用等号的原因。
总的来说,贪心是用来解决一类最优化问题,并希望由局部最优策略来推得全局最优结果的算法思想。贪心算法适用的问题一定满足最优子结构性质,即一个问题的最优解可以由它的子问题的最优解有效地构造出来。


问题 A: 看电视

题目描述

暑假到了,小明终于可以开心的看电视了。但是小明喜欢的节目太多了,他希望尽量多的看到完整的节目。
现在他把他喜欢的电视节目的转播时间表给你,你能帮他合理安排吗?

输入

输入包含多组测试数据。每组输入的第一行是一个整数n(n<=100),表示小明喜欢的节目的总数。
接下来n行,每行输入两个整数si和ei(1<=i<=n),表示第i个节目的开始和结束时间,为了简化问题,每个时间都用一个正整数表示。
当n=0时,输入结束。

输出

对于每组输入,输出能完整看到的电视节目的个数。

样例输入

12
1 3
3 4
0 7
3 8
15 19
15 20
10 15
8 18
6 12
5 10
4 14
2 9
0

样例输出

5

Note

题目要求“完整看到的电视节目的个数”,所以这是一个区间不相交问题。

#include 
#include 
using namespace std;

struct Inteval
{
     
    int x, y; // 开区间的左右端点
};

bool cmp(Inteval a, Inteval b)
{
     
    if (a.x != b.x)
        return a.x > b.x; // 先按左端点从大到小排序
    else
        return a.y < b.y; // 左端点相同的按右端点从小到大排序
}

int main()
{
     
    int n;
    while (cin >> n, n)
    {
     
        Inteval point[n];
        for (int i = 0; i < n; ++i)
            cin >> point[i].x >> point[i].y;
        sort(point, point + n, cmp);

        int ans = 1, lastSi = point[0].x;
        for (int i = 1; i < n; ++i)
        {
     
            if (point[i].y <= lastSi)
            {
     
                lastSi = point[i].x;
                ++ans;
            }
        }
        cout << ans << endl;
    }

    return 0;
}

问题 B: 出租车费

题目描述

某市出租车计价规则如下:起步4公里10元,即使你的行程没超过4公里;接下来的4公里,每公里2元;之后每公里2.4元。行程的最后一段即使不到1公里,也当作1公里计费。
一个乘客可以根据行程公里数合理安排坐车方式来使自己的打车费最小。
例如,整个行程为16公里,乘客应该将行程分成长度相同的两部分,每部分花费18元,总共花费36元。如果坐出租车一次走完全程要花费37.2元。
现在给你整个行程的公里数,请你计算坐出租车的最小花费。

输入

输入包含多组测试数据。每组输入一个正整数n(n<10000000),表示整个行程的公里数。
当n=0时,输入结束。
输出
对于每组输入,输出最小花费。如果需要的话,保留一位小数。

样例输入

3
9
16
0

样例输出

10
20.4
36

Note

利用贪心的思想,肯定是希望将总路程分成多段路程,然后每一段路程求出最小花费的方案,最后就能得到整段路上的最小花费。假设整段路上的最小花费为P,此时应该这几种方案:

  1. 0 < n ≤ 4 0 < n \leq4 0<n4时, P = 10 P = 10 P=10
  2. 4 < n ≤ 8 44<n8时, P = 4 + ( n − 4 ) × 2 P = 4+(n-4)\times2 P=4+(n4)×2
  3. 8 < n ≤ 12 88<n12时, P = 18 + ( n − 8 ) × 2.4 P=18+(n-8)\times2.4 P=18+(n8)×2.4。事实上可以验证,当 n = 13 n = 13 n=13时,采取方案2和方案3的价格是一样的。
  4. 12 < n 1212<n时,将P拆出一个8出来,因为从全局来看方案1+方案2是单价最小的(可以自己去验证一下)。在 n > 8 n>8 n>8之后,我们就将总路程n减去一个8,P加上18,,然后继续判断n的范围,选择更便宜的方案。如果还是大于12就重复之前的工作,直至 n < = 12 n<=12 n<=12为止。记住这里的比较,大于12才减去8,因为在保证 8 < n ≤ 12 88<n12时,方案1+方案2+方案3仍然是优于方案1+方案2的。
#include 
#include 
#include 
using namespace std;

double cal(double n)
{
     
    if (n <= 4)
        return 10;
    else if (n > 4 && n <= 8)
        return 10 + (n - 4) * 2;
    else
        return 18 + (n - 8) * 2.4;
}

int main()
{
     
    double n;
    while (cin >> n, n)
    {
     
        double P = 0;
        while (n > 12)
        {
     
            P += 18;
            n -= 8;
        }
        P += cal(n); // 计算剩余的路程花费

        if (P - int(P) != 0) // 减去自身的整数等于小数位,小数位不为0说明不是整数
            cout << fixed << setprecision(1) << P << endl;
        else // 转为整数输出
            cout << (int)P << endl;

    }

    return 0;
}

问题 C: To Fill or Not to Fill

题目描述

With highways available, driving a car from Hangzhou to any other city is easy. But since the tank capacity of a car is limited, we have to find gas stations on the way from time to time. Different gas station may give different price. You are asked to carefully design the cheapest route to go.

输入

Each input file contains one test case. For each case, the first line contains 4 positive numbers: Cmax (<= 100), the maximum capacity of the tank; D (<=30000), the distance between Hangzhou and the destination city; Davg (<=20), the average distance per unit gas that the car can run; and N (<= 500), the total number of gas stations. Then N lines follow, each contains a pair of non-negative numbers: Pi, the unit gas price, and Di (<=D), the distance between this station and Hangzhou, for i=1,…N. All the numbers in a line are separated by a space.

输出

For each test case, print the cheapest price in a line, accurate up to 2 decimal places. It is assumed that the tank is empty at the beginning. If it is impossible to reach the destination, print “The maximum travel distance = X” where X is the maximum possible distance the car can run, accurate up to 2 decimal places.

样例输入

59 525 19 2
3.00 314
3.00 0

样例输出

82.89

Note

该题目所要解决的问题是:给定若干加油站信息,问能否驾驶汽车行驶一定的距离。如果能够行驶完全程,则计算最小花费。若不能行驶完全程,则最远能够行驶多长距离。

拿到这一题,首先判断汽车是否能够行驶到终点。什么情况下汽车无法行驶到终点呢?两种情况:起点根本就没有加油站,汽车无法启动;或者中途两个加油站之间的距离大于加满油后汽车能够行驶的最大距离。前者汽车行驶的最大距离为0.00,而后者最大距离为当前加油站的距离加上在这个加油站加满油后能够行驶的最大距离。在这里,需要将加油站按到杭州的距离从小到大排序。

接下来在能够行驶到终点的情况下计算最小花费。我们首先从路程来考虑,如果在路上,我们能够使用最便宜的汽油,当然就在那个加油站加油了。所以从起点开始遍历每个加油站。假设遍历到了第i个加油站,我们现在来判断在加油站i应该加多少油。设当前汽车离杭州的距离为curLen,当前加油站离杭州的距离为nodes[i].dis,加满油以后汽车能够行驶的最大距离为(dis=cmax*len)。这样就有node[i].dis <= curLen <= nodes[i].dis+dis,否则的话第i个加油站的油是不起作用的。于是在第i个加油站的作用范围内寻找有没有更为便宜的加油站,如果有,则下次使用这个加油站的油(j),这次汽车应该行驶到这个加油站,即touch=nodes[j].dis。如果没有找到更为便宜的加油站则可以在第i个加油站加满油,即touch=nodes[i].dis+dis。然后判断下次应该行驶到的距离与当前距离的关系,如果下次应当行驶到的距离大于当前距离,则汽车行驶,否则不动(也就是说上个加油站的油更便宜,后一个加油站也便宜,根本就不需要在当前加油站加油)。


问题 D: Repair the Wall

题目描述

Long time ago , Kitty lived in a small village. The air was fresh and the scenery was very beautiful. The only thing that troubled her is the typhoon.
When the typhoon came, everything is terrible. It kept blowing and raining for a long time. And what made the situation worse was that all of Kitty’s walls were made of wood.
One day, Kitty found that there was a crack in the wall. The shape of the crack is
a rectangle with the size of 1×L (in inch). Luckly Kitty got N blocks and a saw(锯子) from her neighbors.
The shape of the blocks were rectangle too, and the width of all blocks were 1 inch. So, with the help of saw, Kitty could cut down some of the blocks(of course she could use it directly without cutting) and put them in the crack, and the wall may be repaired perfectly, without any gap.
Now, Kitty knew the size of each blocks, and wanted to use as fewer as possible of the blocks to repair the wall, could you help her ?

输入

The problem contains many test cases, please process to the end of file( EOF ).
Each test case contains two lines.
In the first line, there are two integers L(0 mentioned above.
In the second line, there are N positive integers. The ith integer Ai(0

输出

For each test case , print an integer which represents the minimal number of blocks are needed.
If Kitty could not repair the wall, just print “impossible” instead.

样例输入

2 2
12 11
14 3
27 11 4
109 5
38 15 6 21 32
5 3
1 1 1

样例输出

1
1
5
impossible

Note

题目的意思是有一个 1 × L 1\times L 1×L大的缺口,现在有N个砖块,每个砖块的长度在第二行给出。现在求最少需要多少个砖块就可以将缺口填满,砖块可以切割。
思路很明确,每次都找最长的砖块,如果长过缺口就直接填,没填满就继续寻找。所以我们需要对长度数组进行排序,排好序后,只需从0号元素往后取用砖块就可以。数组的下标恰好就能表示所用砖块数。要注意砖块和缺口的长度可以为 1 0 9 10^9 109,相加的话很容易超过int,所以要用long long来定义。

#include 
#include 
using namespace std;

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

int main()
{
     
    long long L, N;
    while (cin >> L >> N)
    {
     
        long long A[N];
        for (int i = 0; i < N; ++i)
            cin >> A[i];
        sort(A, A + N, cmp); // 从大到小排序

        long long sum = 0;
        int ans = 0;
        while (sum <= L && ans < N) // 长度超过缺口或到达数组末尾即砖头用完时循环结束
            sum += A[ans++]; // ans的值恰好就能表示所用砖块数

        if (sum < L)
            cout << "impossible\n";
        else
            cout << ans << endl;
    }

    return 0;
}


问题 E: FatMouse’s Trade

题目描述

FatMouse prepared M pounds of cat food, ready to trade with the cats guarding the warehouse containing his favorite food, JavaBean.
The warehouse has N rooms. The i-th room contains J[i] pounds of JavaBeans and requires F[i] pounds of cat food. FatMouse does not have to trade for all the JavaBeans in the room, instead, he may get J[i]* a% pounds of JavaBeans if he pays F[i]* a% pounds of cat food. Here a is a real number. Now he is assigning this homework to you: tell him the maximum amount of JavaBeans he can obtain.

输入

The input consists of multiple test cases. Each test case begins with a line containing two non-negative integers M and N. Then N lines follow, each contains two non-negative integers J[i] and F[i] respectively. The last test case is followed by two -1’s. All integers are not greater than 1000.

输出

For each test case, print in a single line a real number accurate up to 3 decimal places, which is the maximum amount of JavaBeans that FatMouse can obtain.

样例输入

4 2
4 7
1 3
5 5
4 8
3 8
1 2
2 5
2 4
-1 -1

样例输出

2.286
2.500

Note

题目的意思是杰瑞手上有M磅的猫食想拿去汤姆守卫的仓库换奶酪,仓库里有N个房间。这N个房间的特点是,第i个房间有J[i]磅奶酪,如果希望得到这J[i]磅的奶酪,杰瑞需要付出F[i]磅的猫食。但是他不必这么做,因为存在这么一个规矩,如果杰瑞仅仅支付 %a * F[i] 磅的猫食,也能能到 %a * J[i] 磅的奶酪。其中%a是一个实数。现在题目希望给出数据,让你求出杰瑞用这M磅猫食最多能得到多少磅奶酪。
其实仔细观察数据就能发现,第i个房间奶酪:猫食是存在一个兑换比例的,而这个比例直接决定了猫食兑换奶酪的收益。当J[i]:F[i]越大时,意味着越少的只需要一点点的猫食就能得到很多奶酪。所以我们的基本思路是:

  1. 输入数据后,确定好每个房间的兑换比例,然后进行一个排序。还是贪心的思想,从大到小排序,尽可能的将猫食多投入收益大的房间,猫食兑换完退出循环输出数据;
  2. 题目中提了一句“Here a is a real number”,即a是实数,说明要用浮点型来存储数据;
  3. 最后注意一下题目输入终止条件。
  4. 兑换比例是和J、F绑定在一起的,如果将比例单独放置在一个数组,在猫食比较多时,就不需要比例,直接减去相应的猫食支付,然后增加相应的奶啦所得即可。但因为比例单独放置在一个数组且排过序了,所以找不到对应的J、F,因此为了解决这个问题需要用到结构体。
#include 
#include 
#include 
using namespace std;

struct room
{
     
    double F, J, prop; // 即使题目说J、F是整数,用double存储也没问题的
};

bool cmp(room a, room b)
{
     
    return a.prop > b.prop;
}

int main()
{
     
    double M;
    int N;
    while (cin >> M >> N, (M != -1 && N != -1))
    {
     
        room r[N];
        for (int i = 0; i < N; ++i)
        {
     
            cin >> r[i].J >> r[i].F;
            r[i].prop = r[i].J / r[i].F;
        }
        sort(r, r + N, cmp);

        double sum = 0;
        for (int k = 0; k < N && M > 0; ++k) // 对房间进行枚举
        {
     
            if (r[k].F <= M) // 如果该房间所需猫食不大于剩余猫食
            {
     
                sum += r[k].J; // 直接兑换出所有的奶酪
                M -= r[k].F; // 减去相应所需的猫食
            }
            else // 如果该房间所需猫食大于剩余猫食
            {
     
                sum += M * r[k].prop; // 直接全部用于兑换
                break; // 退出循环
            }
        }
        cout << fixed << setprecision(3) << sum << endl;
    }

    return 0;
}


问题 F: 迷瘴

题目描述

小明正在玩游戏,他控制的角色正面临着幽谷的考验——
幽谷周围瘴气弥漫,静的可怕,隐约可见地上堆满了骷髅。由于此处长年不见天日,导致空气中布满了毒素,一旦吸入体内,便会全身溃烂而死。
幸好小明早有防备,提前备好了解药材料(各种浓度的万能药水)。现在只需按照配置成不同比例的浓度。
现已知小明随身携带有n种浓度的万能药水,体积V都相同,浓度则分别为Pi%。并且知道,针对当时幽谷的瘴气情况,只需选择部分或者全部的万能药水,然后配置出浓度不大于 W%的药水即可解毒。
现在的问题是:如何配置此药,能得到最大体积的当前可用的解药呢?
特别说明:由于幽谷内设备的限制,只允许把一种已有的药全部混入另一种之中(即:不能出现对一种药只取它的一部分这样的操作)。

输入

输入数据的第一行是一个整数C,表示测试数据的组数;
每组测试数据包含2行,首先一行给出三个正整数n,V,W(1<=n,V,W<=100);
接着一行是n个整数,表示n种药水的浓度Pi%(1<=Pi<=100)。

输出

对于每组测试数据,请输出一个整数和一个浮点数;
其中整数表示解药的最大体积,浮点数表示解药的浓度(四舍五入保留2位小数);
如果不能配出满足要求的的解药,则请输出0 0.00。

样例输入

2
1 35 68
1
2 79 25
59 63

样例输出

35 0.01
0 0.00

Note

题目很简单,搞清楚数学原理就行了。同体积不同浓度的溶液混合后的浓度值,等于浓度和除以总体积。本题问的就是,在保证这个浓度值小于给定浓度值的情况下,所能调配出的最大体积。
基本思路就是将n瓶万能药水的浓度排个序,当然是浓度越小的越好,因为题目要求不大于W。然后每次加入一瓶新的药水,更新一次浓度值,然后与W比较大小。
这题太催吐了,就

if ((double)(curP + P[maxV]) / ((maxV + 1) * 100) > ((double)W / 100))

这个位置我生生调试了将近一个小时,不能加’=’!!!

#include 
#include 
#include 
using namespace std;

int main()
{
     
	int c, n, V, W, maxV, curP;
	while (cin >> c)
	{
     
		while (c--)
		{
     
			cin >> n >> V >> W;
			int P[n];
			for (int i = 0; i < n; ++i)
				cin >> P[i];
			sort(P, P + n); // 将浓度按从小到大排序

			for (maxV = 0, curP = 0; maxV < n; ++maxV) // 将体积单位用做下标
			{
     
				if ((double)(curP + P[maxV]) / ((maxV + 1) * 100) > ((double)W / 100)) // 浓度值超过W退出循环
                    break;
                curP += P[maxV];
			}
			if (maxV == 0) // 如果配不出满足要求的解药,那就等死吧!!!
				cout << "0 0.00\n";
			else
				cout << maxV * V << ' ' << fixed << setprecision(2) << (double)curP / (maxV * 100) << endl; // 题给浓度都是百分比,输出时要除以100
		}
	}

	return 0;
}


问题 G: 找零钱

题目描述

小智去超市买东西,买了不超过一百块的东西。收银员想尽量用少的纸币来找钱。
纸币面额分为50 20 10 5 1 五种。请在知道要找多少钱n给小明的情况下,输出纸币数量最少的方案。 1<=n<=99;

输入

有多组数据 1<=n<=99;

输出

对于每种数量不为0的纸币,输出他们的面值*数量,再加起来输出

样例输入

25
32

样例输出

20 * 1+5 * 1
20 * 1+10 * 1+1 * 2
(CSDN排版问题,’*’ 号两边是没有空格的)

Note

暴力,大力出奇迹。

#include 
#include 
#include 
using namespace std;

int main()
{
     
    int n;
    while (cin >> n) // n < 100
    {
     
        int i = 0; // i用来标记是否是第一种面值
        while (n)
        {
     
            if (i > 0)
                cout << '+';
            if (n >= 50) // 50面值最多一张
            {
     
                cout << "50*1"; // 直接输出就行
                n -= 50;
            }
            else if (n >= 20 && n <50) // 20面值最多两张
            {
     
                if (n >= 40) // 大于40的话需要两张20
                {
     
                    cout << "20*2";
                    n -= 40;
                }
                else
                {
     
                    cout << "20*1";
                    n -= 20;
                }
            }
            else if (n >= 10 && n < 20) // 10面值最多一张
            {
     
                cout << "10*1";
                n -= 10;
            }
            else if (n >= 5 && n < 10) // 5面值最多一张
            {
     
                cout << "5*1";
                n -= 5;
            }
            else // 剩下的小于5的部分就全用1面值
            {
     
                cout << "1*" << n;
                break; // 直接退出循环
            }
            ++i;
        }
        cout << endl;
    }

    return 0;
}


一定要自己写一遍哦~~~

你可能感兴趣的:(#,第4章,codeup,区间不相交,区间选点,贪心算法)