算法学习——贪心(1)

    贪心法是求解一类最优化问题的方法,它总是考虑在当前状况下的局部最优解(或较优的策略),来使全局的结果达到最优(或较优)。显然,如果采用较优而非最优的策略(最优策略可能不存在或者不容易想到),得到的全局结果也无法是最优的。而要获得最优结果,则要求中间的每步策略都是最优的,因此严谨使用贪心法来求解最优化问题需要对采取的策略进行证明。证明的一般思路是使用反证法或数学归纳法,即假设策略不能导致最优解,然后通过一系列推导来得到矛盾,一次证明策略是最优的,最后用数学归纳法保证全局最优。不过对平常使用来说,也许没有时间或不太容易对想到的策略进行严谨的证明,因为贪心的证明往往比贪心更难,因此,一般来说,如果在想到某个似乎可行的策略,并且自己无法举出反例,那么就勇敢地去实现吧!下面来看几道题目。


问题 A: 看电视
时间限制: 1 Sec 内存限制: 32 MB

题目描述
暑假到了,小明终于可以开心的看电视了。但是小明喜欢的节目太多了,他希望尽量多的看到完整的节目。
现在他把他喜欢的电视节目的转播时间表给你,你能帮他合理安排吗?
输入
输入包含多组测试数据。每组输入的第一行是一个整数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

解题思路
总是先选择左端点最大的区间(Submission1) 或者 总是先选择右端点最小的区间(Submission2)。

Submission1

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 110;
struct perform{
    int start, end;
}I[maxn];
bool cmp(perform a, perform b) {
    if(a.start != b.start) return a.start > b.start;//先按左端点从大到小排序 
    else return a.end <= b.end;//左端点相同的按右端点从小到大排序 
}
int main()
{
    int i,n;
    while(scanf("%d", &n), n != 0) {
        for(i = 0; i < n; i++) {
            scanf("%d%d", &I[i].start, &I[i].end);
        }
        sort(I, I+n, cmp);//把区间排序 
        int ans = 1, lastS = I[0].start;//ans记录不想交区间的个数,lastX记录上一个被选中区间的左端点 
        for(i = 1; i < n; i++) {
            if(I[i].end <= lastS) {//如果该区间右端点在lastX左边 
                lastS = I[i].start;//以I[i]作为新选中的区间 
                ans++;//不相交区间个数加1 
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

Submission2

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 110;
struct perform{
    int start, end;
}I[maxn];
bool cmp(perform a, perform b) {
    if(a.end != b.end) return a.end < b.end;//先按右端点从小到大排序 
    else return a.start > b.start;//左端点相同的按左端点从大到小排序 
}
int main()
{
    int i,n;
    while(scanf("%d", &n), n != 0) {
        for(i = 0; i < n; i++) {
            scanf("%d%d", &I[i].start, &I[i].end);
        }
        sort(I, I+n, cmp);//把区间排序 
        int ans = 1, firstE = I[0].end;//ans记录不想交区间的个数,firstE记录上一个被选中区间的右端点 
        for(i = 1; i < n; i++) {
            if(I[i].start >= firstE) {//如果该区间左端点在firstE右边 
                firstE = I[i].end;//以I[i]作为新选中的区间 
                ans++;//不相交区间个数加1 
            }
        }
        printf("%d\n", ans);
    }
    return 0;
}

问题 B: 出租车费
时间限制: 1 Sec 内存限制: 32 MB

题目描述
某市出租车计价规则如下:起步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

解题思路
(1)以8的倍数进行分段,能整除8的以18的价格收费
(2)设置标志位flag,若原始的len大于8则将一次性计费和分两次计费的价格进行比较。小于8则按照一次性计费计算,跳至第三步。
(3)如果len小于4,则按起步价10元计算;如果大于4则按照10元起步价加(len-4)*2计算。

Submission

#include<stdio.h>
int main()
{
    int len;
    while(scanf("%d", &len)&&len != 0) {
        double sum = 0;
        int flag = 0;
        if(len >= 8) {
            sum = len / 8 * 18;
            len = len % 8;
            flag = 1;   
        }
        if(len == 0) {
            flag = 0;
        }
        if(len > 0) {
            if(flag && 2.4 * len <= 10){ 
                sum += 2.4 * len;
                flag = 1; //判断需不需要保留一位小数 
            }
            else if(len < 4) {
                sum += 10;
                flag = 0;
            }
            else {
                sum += (10 + (len - 4) * 2);
                flag = 0; 
            }
        }
        if(flag == 1) printf("%.1f\n", sum);
        else printf("%.0f\n", sum);
    } 
    return 0;
}

问题 C: To Fill or Not to Fill
时间限制: 1 Sec 内存限制: 32 MB

题目描述
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.
(每个输入文件包含一个测试用例。对于每一种情况,第一行包含4个正数:CMAX(<=100),储罐的最大容量;D(<=30000),杭州与目的地城市之间的距离;Davg(<=20),汽车的单位汽油可以运行的平均距离;n(<=500),总加油站数。接着,n条线路,每条都包含一对非负数:Pi,单位汽油价格,Di(<=d),这个站到杭州的距离,i=1,…N.一条线上的所有数字都用一个空格隔开。)

输出
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.
(对于每个测试用例,在一行中打印最便宜的价格,精确到小数点后两位。假设油箱在开始时是空的。如果无法到达目的地,打印“The maximum travel distance = X”,其中x是汽车可以运行的最大可能距离,精确到小数点2位。)

样例输入
59 525 19 2
3.00 314
3.00 0
样例输出
82.89

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

拿到这一题,首先判断汽车是否能够行驶到终点。什么情况下汽车无法行驶到终点呢?两种情况:起点根本就没有加油站,汽车无法启动;或者中途两个加油站之间的距离大于加满油后汽车能够行驶的最大距离。前者汽车行驶的最大距离为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。然后判断下次应该行驶到的距离与当前距离的关系,如果下次应当行驶到的距离大于当前距离,则汽车行驶,否则不动(也就是说上个加油站的油更便宜,后一个加油站也便宜,根本就不需要在当前加油站加油)。

对于当前站点S,所能到达的最大范围即满油量所能到达的距离,设满油量能前进的距离为maxToGo,则在S到S+maxToGo范围内,分如下情况进行考虑:

Ⅰ此范围内有加油站
①有比当前站点便宜的加油站,因为只从最小的局部考虑问题,如果有多个比当前便宜的,到达那个最近的而不是最便宜的(只需要在找到第一个比S便宜的站点时break即可)。
②全部比S更贵(易错点)
2.1 如果从S无法到达终点,则选择最便宜的那个,从S加满油到达那个站点。
2.2 如果从S可以直接到达终点,则从S加油至能到达终点,直接开到终点。
Ⅱ此范围内无加油站
①如果从S可以直接到达终点,则加到能到达终点,直接到达。
②如果从S无法到达终点,加满油,能跑多远跑多远。

Submission

#include <stdio.h>
#include <algorithm>
using namespace std;
typedef struct Station {
    double price;
    double dist;
    double gas_ds;
} Station;
double full_dist;//加满汽油后可以行驶的距离
double run_dist;//已经行驶的距离
Station s[501];
bool cmp(Station a, Station b) {
    return a.dist < b.dist;
}

int findnext(int a, int b) {
    if (a > b) return -1;
    int minIndex = a;
    for (int i = a; i <= b; i++) {
        if (s[i].price < s[minIndex].price) {
            minIndex = i;
        }
    }
    return minIndex;
}

int main() {
    double c_max, d, d_avg;
    int n;
    int i, j, k;
    int flag_start = 1;//能否启动的标志
    scanf("%lf %lf %lf %d", &c_max, &d, &d_avg, &n);
    for (i = 0; i < n; i++) {
        scanf("%lf %lf", &s[i].price, &s[i].dist);
        s[i].gas_ds = 0;
        if(s[i].dist == 0) {
            flag_start = 0;
        } 
    }  
    if (flag_start) { //flag_start为1,不能启动 
        printf("The maximum travel distance = 0.00\n");//起点根本没有加油站,汽车无法启动,行驶的最大距离为0.00
        return 0;
    }
    n++; //将目的地添加为第n个加油站 
    s[n - 1].dist = d;
    sort(s, s + n, cmp); //将加油站按到杭州的距离从小到大排序
    full_dist = c_max * d_avg;//加满汽油后可以行驶的距离
    run_dist = 0.0;//已经行驶的距离
    int lastmin = 0;
    double lastpos = 0.0;
    for (i = 0; i < n - 1; i++) {
        if (s[i + 1].dist - s[i].dist > full_dist) { //中途两个加油站之间的距离大于加满油后汽车能够行驶的最大距离
            run_dist = s[i].dist + full_dist;//最大距离为当前加油站的距离加上在这个加油站加满油后能够行驶的最大距离
            break;
        } 
        else {//在能够行驶到终点的情况下计算最小花费
            if (s[i].price < s[lastmin].price) {
                s[lastmin].gas_ds = s[i].dist - lastpos;
                lastmin = i;
                lastpos = s[i].dist;
            } 
            else {
                while (s[i + 1].dist - s[lastmin].dist > full_dist) {
                    s[lastmin].gas_ds = s[lastmin].dist + full_dist - lastpos; //先在最便宜的站点加满油 
                    lastpos = s[lastmin].dist + full_dist;//最后的位置为上次最便宜的油站的位置加最大行驶范围
                    lastmin = findnext(lastmin + 1, i);//寻找上次最便宜的油站和此次油站之间的更便宜的油站,否则默认上次最便宜的下一站为当前便宜站点
                } 
            }
        }
    }
    s[lastmin].gas_ds = s[n - 1].dist - lastpos;
    if (run_dist > 0.0) {
        printf("The maximum travel distance = %.2f\n", run_dist);
    } 
    else {
        double sum = 0.0;
        for (i = 0; i < n - 1; i++) {
            if (s[i].gas_ds > 0) {
                sum += s[i].gas_ds * s[i].price;
            }
        }
        sum = sum / d_avg;
        printf("%.2lf\n", sum);
    }
    return 0;
}

    总的来说,贪心是用来解决一类最优化问题,并希望由局部最优策略来推得全局最优结果的算法思路。贪心算法适用的问题一定满足最优子结构性质,即一个问题的最优解可以由它的子问题的最优解有效地构造出来。显然,不是所有问题都适合使用贪心算法,但是这不妨碍贪心算法成为一个简洁、实用、高效的算法。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值