贪心教学(内附题目)

我们按三个部分来学:

     贪心算法概念
02 贪心算法的例题与应用
01 贪心算法思路
        基本概念
        所谓贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的仅是在某种意义上的局部最优解。
        贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算
        法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
        所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。

注意

无后效性:某阶段的状态一旦确定,则此后过程的决策不再受此前各种状态及决策的影响。

有后效性:就是某个状态之后要做的决策会受之前的状态及决策的影响。(摘自“思维的深度”)

如果把条件改为:可以往前后左右走但是不能走重复的格子,那么接下来要做的决策就需要考虑之前的决策,故此时是有后效性。

若仍不懂请看例题

    生活中的找零问题: 假如老板要找给我99分钱,他有有的面值分别为25分,10分,5分,1分(现实生活中没有)的硬币数,为 了找给我最少的硬币数,老板应该怎么做?

很显然这一题的解是3枚25分,2枚10分和4枚1分。
这一题用贪心就是面额大小按从大到小分配。
但一旦将题目改一改,后效性可以看出来。
例:假如老板要找给我60分钱,他有有的面值分别为19分,15分,1分(现实生活中没有)的硬币数,为 了找给我最少的硬币数,老板应该怎么做?

 用贪心解出为:3枚19分,0枚15分和3枚1分。(共计6枚)

而最优解是:4枚15分。(共计6枚)

这就是后效性。那我们继续教学

基本思路
1. 建立数学模型来描述问题
2. 把求解的问题分成若干个子问题
3. 对每一子问题求解,得到子问题的局部最优解
4. 把子问题的解局部最优解合成原来解问题的一个解
贪心算法适用于局部最优策略可以导出全局最优策略
可以看到这里“子问题”这几个字加粗了
说明这非常重要(解题的关键)
例题 排队接水
题目描述

有n个人在一个水龙头前排队接水,假如每个人接水的时间为Ti,请编程找出这n个人排队的一种顺序, 使得n个人的平均等待时间最小。 n<=1000,等待的时间小于等于1000

输入

输入文件共两行,第一行为n;第二行分别表示第 1 个人到第n个人每人的接水时间T1,T2,…,Tn,每 个数据之间有 1 个空格。

输出

输出有一行,最少平均排列方案下的平均等待时间(输出结果精确到小数点后两位)。

样例输入 Copy
10
56 12 1 99 1000 234 33 55 99 812
样例输出 Copy
532.00

你可以这么想:

每个人都很贪心,都想先接水,打了起来,现在最优方案是接水的时间短的先接,这样后面等待时间较少,而子问题就是这两两比较的过程。(是不是感觉自己行了呢,去吧)

分析:由于排队时,越靠前面的计算的次数越多,显然越小的排在越前面得出的结果越小(可以用
数学方法简单证明,这里就不再赘述),所以这道题可以用贪心法解答,基本步骤:
(1)将输入的时间按从小到大排序
(2)将排序后的时间按顺序依次放入每个水龙头的队列中
(3)统计,输出答案
代码样例
#include<bits/stdc++.h>
using namespace std;
int a[1010];
int main(){
    int n;
    int cnt=0;
    cin>>n;
    for(int i=1;i<=n;i++) {
        cin>>a[i];
         
    }
    sort(a+1,a+1+n);
    for(int i=1;i<=n;i++) {
        cnt+=a[i]*(n-i+1);
         
    }
     
    printf("%.2lf",cnt*1.0/n );
    return 0;
}
例题 删数问题
题目描述

键盘输入一个高精度的正整数n(n<10^240),去掉其中任意s个数字后剩下的数字按照原来的次序将组成一个新的非负整数。编程对给定的n和s,寻求一种方案,使得剩下组成的新数最小。

输入

第1行:一个正整数n;

第2行:s(s<=n的位数).

输出

最后剩下的最小数。

样例输入 Copy
175438
4
样例输出 Copy
13
题目大意:输入一个高精度的正整数N,去掉其中任意S个数字后剩下的数字按原左右次序组成一
个新的正整数。编程对给定的N和S,寻找一种方案使得剩下的数字组成的新数最小。
输出新的正整数。(N不超过240位)
分析:由于正整数n的有效数位为240位,所以很自然地采用字符串类型存贮n。那么如何决定哪s
位被删除呢?是不是最大的s个数字呢?显然不是,大家很容易举出一些反例。
比如:819,删除一个数字的情况下,显然删除的是8(删除8后是19),而不是9(删除9后是81)
分析:为了尽可能逼近目标,我们选取的贪心策略为:每一步总是选择一个 使剩下的数最小的数字
删去 ,即按高位到低位的顺序搜索,若各位数字递增,则删除最后一个数字;
否则删除第一个递减区间的首字符,这样删一位便形成了一个新数字串。然后回到串首,按上述规
则再删下一个数字。重复以上过程s次为止,剩下的数字串便是问题的解了
代码样例
#include<bits/stdc++.h>
using namespace std;
int main(){
    string s;
    int n;
    cin>>s>>n;
    while(n--) {
        for(int i=0;i<s.size();i++) {
            if(i==s.size()-1||s[i]>s[i+1]) {
                s.erase(i,1);
                break;
            }
        }
    }
    while(s[0]=='0'&&s.size()>1) s.erase(0,1);
    cout<<s;
    return 0;
}
例题 活动选择
题目描述

学校在最近几天有n个活动,这些活动都需要使用学校的大礼堂,在同一时间,礼堂只能被一个活动使用。由于有些活动时间上有冲突,学校办公室人员只好让一些活动放弃使用礼堂而使用其他教室。   

现在给出n个活动使用礼堂的起始时间bi和结束时间ei(bi < ei<=32767),请你帮助办公室人员安排一些活动来使用礼堂,要求安排的活动尽量多。

输入

第一行一个整数n(n<=1000); 接下来的n行,每行两个整数,第一个bi,第二个是ei(bi < ei<=32767)

输出

输出最多能安排的活动个数

样例输入 Copy
11
3 5
1 4
12 14
8 12
0 6
8 11
6 10
5 7
3 8
5 9
2 13
样例输出 Copy
4
提示

注:每个活动占用礼堂的时间为左闭右开区间[bi,ei).

题目大意:学校在最近几天有n个活动,这些活动都需要使用学校的大礼堂,在同一时间,礼堂只
能被一个活动使。由于有些活动时间上有冲突,学校办公室人员只好让一些活动放弃使用礼堂而使
用其他教室。
现在给出n个活动使用礼堂的起始时间begin[i]和结束时间end[i](begin[i] < end[i]),请你帮助办
公室人员安排一些活动来使用礼堂,要求安排的活动尽量多
贪心策略:题目可以概括为给n个开区间(begin[i],end[i]), 选择尽量多的区间, 使得两两不交。
首先按照end[1]<=end[2]<…<=end[n]的顺序排序,依次考虑各个活动, 如果没有和已经选择的活
动冲突, 就选; 否则就不选
贪心算法的正确性:如果不选end[1], 假设第一个选择的是end[i],则如果end[i]和end[1]不交叉则
多选一个end[1]更划算; 如果交叉则把end[i]换成end[1]不影响后续选择
#include<bits/stdc++.h>
using namespace std;
struct Activity{
    int start,end;
} act[1010];
 
bool cmp(Activity a, Activity b) {
    return a.end<b.end;
}
int main(){
    int n;
    cin>>n;
    for(int i=0;i<n;i++){
        cin>>act[i].start>>act[i].end;
    }
    sort(act, act + n, cmp);
    int ans=1,last=act[0].end;
    for(int i=1;i<n;i++){
        if(act[i].start>=last){
            ans++;
            last = act[i].end;
        }
    }
    cout<<ans;
    return 0;
}
例题 整数区间
题目描述

给n个区间,形式为[a, b],a和b均为整数,且a < b。求一个最小的整数点的集合,使得每个区间至少 1 个元素属于这个集合。求这个集合的元素个数。

输入

第1行:1个整数n(1 <= n <= 10000)接下来n行,每行2个整数,表示区间的左右端点a, b(0 <=a < b <= 10000)

输出

第1行:1个整数,表示集合的元素的个数

样例输入 Copy
4
3 6
2 4
0 2
4 7
样例输出 Cop
2
贪心策略:给n个闭区间[a[i], b[i]], 在数轴上选尽量少的点, 使每个区间内至少有一个点。首先按
b[1]<=b[2]<=...<=b[n]排序。每次标记当前区间的右端点x,并右移当前区间指针,直到当前区
间不包含x, 再重复上述操做
贪心策略:如下图,如果选灰色点,移动到黑色点更优
#include<bits/stdc++.h>
using namespace std;
struct human{
    int l,r;
} a[10010];
bool cmp(human x,human y) {
    return x.r<y.r;
}
int main(){
    int n;
    cin>>n;
    int cnt=1;
    for(int i=1;i<=n;i++) {
        cin>>a[i].l>>a[i].r;
    }
    sort(a+1,a+n+1,cmp);
    int x=a[1].r;
    for(int i=2;i<=n;i++) {
        if(a[i].l>x) {
            cnt++;
            x=a[i].r;
        }
    }
    cout<<cnt;
    return 0;
}
扩展内容
题目描述

有 N 堆纸牌,编号分别为 1,2,…, N。每堆上有若干张,但纸牌总数必为 N 的倍数。可以在任一堆上取若干张纸牌,然后移动。  

移牌规则为:在编号为 1 堆上取的纸牌,只能移到编号为 2 的堆上;在编号为 N 的堆上取的纸牌,只能移到编号为 N-1 的堆上;其他堆上取的纸

牌,可以移到相邻左边或右边的堆上。  

现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。

例如 N=4,4 堆纸牌数分别为:① 9 ② 8 ③ 17 ④ 6  移动3次可达到目的:  从 ③ 取 4 张牌放到 ④ (9 8 13 10) -> 从 ③ 取 3

张牌放到 ②(9 11 10 10)-> 从 ② 取 1 张牌放到①(10 10 10 10)。

输入

N(N 堆纸牌,1 <= N <= 100)  

A1 A2 … An (N 堆纸牌,每堆纸牌初始数,l<= Ai <=10000)

输出

所有堆均达到相等时的最少移动次数。

样例输入 Copy
4
9 8 17 6
样例输出 Copy
3
题目大意:有 N 堆纸牌,编号分别为 1,2,…, N。每堆上有若干张,但纸牌总数必为 N 的倍数。
可以在任一堆上取若干张纸牌,然后移动。
移牌规则为:在编号为 1 堆上取的纸牌,只能移到编号为 2 的堆上;在编号为 N 的堆上取的纸牌,
只能移到编号为 N-1 的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。
题目大意:现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。
例如 N=4,4 堆纸牌数分别为: ① 9 ② 8 ③ 17 ④ 6
移动3次可达到目的:
从 ③ 取4张牌放到④(9 8 13 10)->从③取3张牌放到 ②(9 11 10 10)-> 从②取1张牌放到①
(10 10 10 10)。
分析:如果你想到把每堆牌的张数减去平均张数,题目就变成移动正数,加到负数中,使大家都变
成0,那就意味着成功了一半!例子中看平均张数为10, 原张数9,8,17,6,变为-1,-2,7,-4,
其中没有为0的数,我们从左边出发:要使第1堆的牌数-1变为0,只须将-1张牌移到它的右边(第
2堆)-2中;结果是-1变为0,-2变为-3,各堆牌张数变为0,-3,7,-4;同理:要使第2堆变为0,
只需将-3移到它的右边(第3堆)中去,各堆牌张数变为0,0,4,-4;要使第3堆变为0,只需将
第3堆中的4移到它的右边(第4堆)中去,结果为0,0,0,0,完成任务。每移动1次牌,步数加
1。
分析:负数张牌怎么移,不违反题意吗?其实从第i堆移动-m张牌到第i+1堆,等价于从第i+1堆移
动m张牌到第i堆,步数是一样的,这样就解决了左右移动的问题
如果张数中本来就有为0的,怎么办呢?如0,-1,-5,6,还是从左算起(从右算起也完全一样),
第1堆是0,无需移牌,余下与上相同;再比如-1,-2,3,10,-4,-6,从左算起,第1次移动的
结果为0,-3,3,10,-4,-6;第2次移动的结果为0,0,0,10,-4,-6,现在第3堆已经变为
0了,可节省1步,余下继续。
#include<bits/stdc++.h>
using namespace std;
int main(){
    int a,p=0,js=0; cin >>a;
    int q[a];  
    for (int y=0;y<a;y++){
        cin >>q[y]; p+=q[y];
    } 
    p/=a;  
    for (int y=0;y<a;y++) q[y]-=p;  
    for (int y=0;y<a;y++) {
        if (q[y]==0)  continue; 
        q[y+1]+=q[y]; 
        js++;
    }  
    cout<<js;  
    return 0;
}

最大整数

题目描述

设有 n 个正整数(n≤20),将它们联接成一排,组成一个最大的多位整数。 例如:n=3 时,3 个整数 13,312,343 联接成的最大整数为:34331213 又如:n=4 时,4 个整数 7,13,4,246 联接成的最大整数为:7424613

输入

n 个数

输出

联接成的多位数

样例输入 Copy
3
13  312  343 
样例输出 Copy
34331213 
#include<bits/stdc++.h>
using namespace std;
bool cmp(string x,string y){
    return x+y>y+x;
};
string s[100];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>s[i];
    sort(s+1,s+n+1,cmp);
    for(int i=1;i<=n;i++) cout<<s[i];
    return 0;
}

零件分组

题目描述

某工厂生产一批棍状零件,每个零件都有一定的长度Li和重量Wi 。现在为了加工需要,要将它们分成若干组,使每一组的零件都能排成一个长度和重量都不下降(若 i<j,则 Li<=Lj,Wi<=Wj)的序列。请问至少要分成几组?

输入

第一行为一个整数 N(N<=1000),表示零件的个数。第二行有N对正整数,每对正整数表示这些零件的长度和重量,长度和重量均不超过 10000。

输出

仅一行,即最少分成的组数。

样例输入 Copy
5
8 4 3 8 2 3 9 7 3 5 
样例输出 Copy
2 
#include<bits/stdc++.h>
using namespace std;
struct node{
    int h,w;
}   a[1010];
bool vis[1010];
bool cmp(node x,node y){
    if(x.h==y.h) return x.w<y.w;
    return x.h<y.h;
};
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++) cin>>a[i].w>>a[i].h;
    sort(a+1,a+n+1,cmp);
    int g=0;
    for(int i=1;i<=n;i++) {
        if(vis[i]==0) {
            g++;
            vis[i]=1;
            int fr=a[i].w;
            for(int j=i+1;j<=n;j++) {
                if(vis[j]==0&&a[j].w>=fr) {
                    vis[j]=1;
                    fr=a[j].w;
                }
            }
        }
    }
    cout<<g;
    return 0;
}

纪念品组合

题目描述

元旦快到了,校学生会让乐乐负责新年晚会的纪念品发放工作。为使得参加晚会的同学所获得的纪念品价值相对均衡,他要把购来的纪念品根据价格进行分组,但每组最多只能包括两件纪念品,并且每组纪念品的价格之和不能超过一个给定的整数。为了保证在尽量短的时间内发完所有纪念品,乐乐希望分组的数目最少。你的任务是写一个程序,找出所有分组方案中分组数最少的一种,输出最少的分组数目。

输入

输入文件包含n+2 行: 第 1 行包括一个整数w ,为每组纪念品价格之和的上限。 第 2 行为一个整数n ,表示购来的纪念品的总件数。 第 3~ n +2 行每行包含一个正整数 p i (5 <= p i <= w ),表示所对应纪念品的价格。

输出

输出文件仅一行,包含一个整数,即最少的分组数目。

样例输入 Copy
100
9
90
20
20
30
50
60
70
80 
90 
样例输出 Copy
6
提示

50%的数据满足:1 <= n <= 15 100%的数据满足:1 <= n <= 30000, 80 <= w <= 200

#include<bits/stdc++.h>
using namespace std;
int a[10000010];
int main(){
    int n,m;
    cin>>m>>n;
    for(int i=1;i<=n;i++) {
        cin>>a[i];
    }
    sort(a+1,a+n+1);
    int l=1,r=n;
    int sum=n;
    while(l<r) {
        if(a[l]+a[r]<=m) {
            l++;
            r--;
            sum--;
        }
        else if(a[l]+a[r]>m) r--;
    }
    cout<<sum;
}

乘积最大 2

题目描述

要求将n写成若干个正整数之和,并且使这些正整数的乘积最大。

输入

读入一个正整数n(10≤n≤31000)。

输出

第1行输出一个整数,为最大乘积的位数。 第2行输出最大乘积的前100位,如果不足100位,则按实际位数输出最大乘积。

样例输入 Copy
13
样例输出 Copy
3
108
提示

数据范围及提示

在给定的范围内,最大乘积的位数不超过5000位。

#include<bits/stdc++.h>
using namespace std;
int a[99999];
int th,tw,len=1,n;
void mul(int x) {
    for(int i=1;i<=len;i++) {
        a[i]=a[i]*x+a[i-1]/10;
        a[i-1]%=10;
    }
    if(a[len]>=10) {
        a[len+1]=a[len]/10;
        a[len]%=10;
        len++;
    }
}
int main(){
    cin>>n;
    while(n>=5) {
        n-=3;
        th++;
    } 
    a[1]=1;
    for(int i=1;i<=th;i++) mul(3);
    mul(n);
    cout<<len<<endl;
    for(int i=len;i>=max(len-99,1);i--) {
        cout<<a[i];
    }
    return 0;
}

独木桥

请跳转至《洛谷经典题目——独木桥》

后面没来得及写分析。

最后祝大家元旦快乐

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值