目录
前言
本文介绍了贪心算法的原理,并且我们会基于贪心的思想去解决一些题目
电子版教材链接
:
我通过百度网盘分享的文件:深入浅出程序设计…pdf
链接:https://pan.baidu.com/s/1kmF8wZLnK3Zci7s1ffjRzw
提取码:Ra3Q
复制这段内容打开「百度网盘APP即可获取」
例12-1 部分背包问题
什么叫做背包问题?
现在有一个容量为T
的背包,有N
堆的金币,第i
堆金币的总重量和总价值分别是m_i
和v_i
如果金币是随意可以分割的(即可以使用金币重量价值比来当做单价),那么久可以使用贪心的思想进行解决,即按照单价把所有金币进行从大到小排序,依次放进背包,直至塞满背包(有的金币进行切割后再放进去)
但是如果不可以分割,那么这道题就不只是简单的寻求单最优策略,需要结合多个条件进行考虑,此时会用到动态规划或者搜索的思想,这些思想我们会在后面遇到,这里先不进行详细讲解
#include <bits/stdc++.h>
using namespace std;
struct coin{
int m,v; // 分别对应着金币堆的重量和价值
}a[110];
bool cmp(coin x,coin y){
return x.v * y.m > y.v * x.m; // 按照单价进行从大到小的排序
}
int main()
{
int n,t,c,i;
float ans = 0;
scanf("%d%d",&n,&t);
c = t; // 背包的剩余容量
for(i = 0;i<n;i++)
scanf("%d%d",&a[i].m,&a[i].v);
sort(a,a+n,cmp);
for(i = 0;i<n;i++){
if(a[i].m > c) break ; // 不能完整装下就跳出
c -= a[i].m;
ans += a[i].v;
}
if(i<n) // 如果 i == n,说明前 n 堆金币全部能整堆装入背包,已经没有剩余金币堆了,自然也不需要尝试“部分装入”,这里说明上面的循环是自然结束的,此时这个判断就没有必要了
ans += 1.0 * c / a[i].m * a[i].v ; // 剩余空间装下部分金币
printf("%.2lf",ans);
return 0;
}
例12-2 排队接水
第一个人是不需要进行等待的
第二个人需要等待第一个人接水的时间
第三个人需要等待第一个人加上第二个人接水的时间
…
第n个人需要等待第一个人一直加到第n-1个人接水的时间
则所有打水人的等待时间总和为
s
=
(
n
−
1
)
t
1
+
(
n
−
2
)
t
2
+
.
.
.
+
1
×
t
n
−
1
+
0
×
t
n
s = (n-1)t_1 + (n-2)t_2 + ...+ 1×t_{n-1} + 0×t_n
s=(n−1)t1+(n−2)t2+...+1×tn−1+0×tn
我们发现t1的系数大,tn的系数小
从贪心的角度来讲,我们需要将t1到tn进行从小到大排序,这样的排序会使时间总和s最小
#include <bits/stdc++.h>
using namespace std;
struct water {
int num, time;
} p[1010];
// 比较函数:先按时间升序排,如果时间相同按编号升序
bool cmp(water a, water b) {
if (a.time != b.time)
return a.time < b.time;
return a.num < b.num;
}
int main() {
int n;
double sum = 0;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> p[i].time; // 读取时间
p[i].num = i; // 记录原始输入的序号
}
sort(p + 1, p + n + 1, cmp);
// 输出排序后的执行顺序编号
for (int i = 1; i <= n; i++) {
cout << p[i].num << " ";
}
cout << endl;
// sum = a1*(n-1) + a2*(n-2) + ... + an*0
for (int i = 1; i <= n; i++) {
sum += p[i].time * (n - i);
}
sum /= n;
cout << fixed << setprecision(2) << sum << endl;
return 0;
}
例12-3 混乱的yyy
我们想要刷的比赛越多,那就尽量让每个比赛的时间早结束,为接下来的比赛腾出时间,所以我们的目的就变成了,按照比赛的时长进行从小到大排序,最后统计最后一场比赛结束前可以去参加多少场比赛即可
#include <bits/stdc++.h>
using namespace std;
int n,ans = 0,finish = 0;
struct contest{
int l,r;
}con[1000010];
bool cmp(contest a,contest b)
{
return a.r <= b.r;
}
int main()
{
cin >> n;
for(int i = 1;i<=n;i++)
cin >> con[i].l >> con[i].r;
sort(con+1,con+n+1,cmp);
for(int i = 1;i<=n;i++)
if(finish <= con[i].l)
ans++,finish = con[i].r;
cout << ans << endl;
return 0;
}
例12-5 合并果子
本题使用到了哈夫曼树的精炼思想,我们会在树的相关章节进行详细的讲解(讲解为什么这种贪心合并可以得到最优解,以及书上例12-4
贪心合并的逆向分卷
为什么也是最优的?)
每次查找最小的两个果堆进行合并即可,但是每次都排序进行最小的两个值的查找显然是很低效的,所以我们另外开一个数组来进行操作
第一个数组存储从小到大的数组排序,第一个数组中取出前两个就是最小的两堆果子,把这两堆果子取出,原数组的信息被划掉,合并一次成为新的一堆,记录消耗的体力并且更新在第二个数组中;接下来继续查找最小堆的果子,需要比较两个数组中没有划掉的部分的最前面的元素即可,取出,如果使用同样的办法找到最小的另外一堆,合并并且放在第二个数组中。由于两个数组都是从小到大排列的,所以两个数组中最小的那一堆一定就在两个数组没有被划掉的元素的最头部,重复这样的操作,直到最后两堆果子被合并
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5;
int n,n2=0,a[N],b[N],sum = 0;
int main()
{
memset(a,127,sizeof(a));
memset(b,127,sizeof(b));
cin >> n;
for(int i = 0;i<n;i++)
cin >> a[i];
sort(a,a+n); // a是第一个数组,我们对a进行排序
int i = 0,j = 0,k,w; // 初始化两个指针,分别指向两个数组
for(k = 1;k<n;k++) // 进行n-1次合并
{
// 先比较两个数组除去被划去元素的头元素大小,如果发现数组一的元素比数组二的小就合并同一数组的两元素,否则就合并两数组的头
if(a[i]<b[j]) w = a[i++]; // 取最小值
else w = b[j++];
if(a[i]<b[j]) w += a[i++]; // 取第二次最小值,并且进行合并
else w += b[j++];
b[n2++] = w; // 加入结果在第二个数组中(刚开始进行时都是数组1进行合并,得到的结果放在数组2中)
sum += w; //计算价值
}
cout << sum << endl;
return 0;
}
小技巧
:我们使用一个接近int最大值的数进行数组的初始化可以提升效率,并且刚开始我们初始化b的值要大一些,不然可能无法使得第一次合并都是在数组一完成的(导致总体的思路都会错误)
memset(a,127,sizeof(a));
memset(b,127,sizeof(b));
习题12-1 小A的糖果
对于第一个盒子和第二个盒子,吃掉第一个盒子的糖只能影响到第二个盒子,影响贡献为1,但是第二个盒子却可以影响第一个盒子和第三个盒子的贡献,所以我们构建这样一个贪心策略:
我们每次都向右边的盒子吃糖,每次都吃到右边的盒子和该盒子之和恰好等于 x 时,总共的累计就是我们吃的最少的糖 我们每次都向右边的盒子吃糖,每次都吃到右边的盒子和该盒子之和恰好等于x时,总共的累计就是我们吃的最少的糖 我们每次都向右边的盒子吃糖,每次都吃到右边的盒子和该盒子之和恰好等于x时,总共的累计就是我们吃的最少的糖
#include<bits/stdc++.h>
using namespace std;
int a[1000005]; // 在全局初始化数组,则数组的所有值都会被初始化为0
int main(){
long long n,x,ans=0;
cin>>n>>x;
for(int i=1;i<=n;i++){
cin>>a[i];
}
a[0] = 0;
for(int i=1;i<=n;i++){
if(a[i-1]+a[i]>x){ // 如果a[1] > x就在总和上加上a[1],a[1]表示第一个元素,这里没有越界,因为a[0] = 0,刚好还隐含了判断第一个数是不是就比x大的判断因子了
ans+=a[i-1]+a[i]-x;
a[i]-=a[i-1]+a[i]-x;
}
}
cout<<ans;
return 0;
}
习题12-2 删数问题
#include<bits/stdc++.h>
using namespace std;
string n;
int s,len,k;
int main(){
cin>>n>>s;
len=n.length();
while(s>0){
s--;
k=len-1;
for(int i=0;i<len-1;i++){
if(n[i]>n[i+1]){
k=i;
break;
}
}
n.erase(k,1);
len--;
}
while(n[0]=='0') n.erase(0,1);
if(n!="") cout<<n<<endl;
else cout<<"0"<<endl;
return 0;
}
习题12-3 陶陶摘苹果(升级版)
我们规定这样一个贪心策略:
高度低且用力少的优先拿
高度低且用力少的优先拿
高度低且用力少的优先拿
所以我们进行两次排序
- 第一次排序用于后面筛出高度<=y+b的苹果
- 第二次排序按照力气从小到大摘取苹果,可以保证摘的最多
#include <bits/stdc++.h>
using namespace std;
int n, y, b, s;
struct Apple {
int hi, li;
} a[5005];
bool cmp1(Apple a, Apple b) {
return a.hi < b.hi;
}
bool cmp2(Apple a, Apple b) {
return a.li < b.li;
}
int main() {
cin >> n >> s;
cin >> y >> b;
for (int i = 1; i <= n; i++) {
cin >> a[i].hi >> a[i].li;
}
sort(a + 1, a + n + 1, cmp1); // 先按照高度排序
int ans = n + 1;
for (int i = 1; i <= n; i++) { // 拿取第一个比y+b大的数的索引
if (a[i].hi > y + b) {
ans = i;
break;
}
}
sort(a + 1, a + ans, cmp2); // 对能摘的部分按力气升序排
int cnt = 0;
for (int i = 1; i < ans; i++) {
if (s >= a[i].li) {
s -= a[i].li;
cnt++;
}
else break;
}
cout << cnt << endl;
return 0;
}
习题12-4 铺设道路
这道题我们可以使用模拟加递归进行解决
4 3 2 5 3 5
全覆盖
3 2 1 4 2 4
全覆盖
2 1 0 3 1 3 此时使用0划分两个区间
左区间为 2 1 右区间为3 1 3
2 + 1 < 3 + 1 + 3
我们先对左区间进行全覆盖
2 1 - > 1 0
此时全局变成了
1 0 0 3 1 3
显然我们还是对左区间进行覆盖
1 -> 0
此时全局变成了
0 0 0 3 1 3
我们此时对右区间进行全覆盖
得到 0 0 0 2 0 2
此时由于左右两边的值相等,那我们就默认先处理左区间
显然,此时对左右区间各自进行两次全覆盖就行
所以我们的总次数就是 2 + 2 + 1 + 4 = 9
接下来我们来实现上述策略的代码(我们可以使用一个递归来进行实现)
// 法一: 递归
#include <bits/stdc++.h>
using namespace std;
int n,ans = 0,arr[100005];
void nihao(int* arr,int l,int r) // 区间全覆盖函数
{
for(int i = l;i<=r;i++)
{
if(arr[i] == 0)
nihao(arr,l,i-1);
nihao(arr,i+1,r);
arr[i]--;
ans++;
}
}
int main()
{
cin >> n;
for(int i = 1;i<=n;i++)
{
cin >> arr[i];
}
dfs(arr,1,n);
cout << ans;
return 0;
}
这道题我们还可以构建如下的贪心策略进行解决:
假设现在有一个坑,但旁边又有一个坑。你肯定会选择把两个同时减1;那么小的坑肯定会被大的坑“带着”填掉。大的坑也会减少a[i]-a[i-1]的深度,可以说是“免费的”;所以这样贪心是可以实现的
即
若
a
[
i
]
>
a
[
i
−
1
]
,
计数器
s
u
m
+
=
a
[
i
]
−
a
[
i
−
1
]
;
若a[i]>a[i-1],计数器sum+=a[i]-a[i-1];
若a[i]>a[i−1],计数器sum+=a[i]−a[i−1];
// 法二 纯贪心
#include<bits/stdc++.h>
using namespace std;
int n,a[100005];
long long ans=0;
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
cin>>a[i];
for(int i=2;i<=n;i++)
if(a[i]>a[i-1])
ans+=a[i]-a[i-1];
cout<< ans+a[1]; return 0;
}
习题12-5 混合牛奶
我们构建这样的一种贪心策略
按照单价进行从小到大的排序,每次都累加数量直到达到要求为止 按照单价进行从小到大的排序,每次都累加数量直到达到要求为止 按照单价进行从小到大的排序,每次都累加数量直到达到要求为止
#include <bits/stdc++.h>
using namespace std;
int n,m,sum;
struct nihao{
int pi,ai; // 其中pi表示单价,ai表示数量
}a[5005];
bool cmp(nihao a,nihao b)
{
return a.pi < b.pi;
}
int main()
{
cin >> n >> m;
for(int i = 1;i<=m;i++)
cin >> a[i].pi >> a[i].ai;
sort(a+1,a+m+1,cmp);
for(int i = 1;i<=m;i++)
{
if(a[i].ai<=n)
sum += a[i].pi * a[i].ai;
else
{
sum += a[i].pi * n;
break;
}
n -= a[i].ai;
if(n == 0) break;
}
cout << sum;
return 0;
}
习题12-6 纪念品分组
我们考虑这样的一种贪心策略:
尽可能的让每个分组里面的数之和接近临界值 尽可能的让每个分组里面的数之和接近临界值 尽可能的让每个分组里面的数之和接近临界值
- 如果找不到另一个数和当前数之和小于于临界值,该数单成一组,bool数组状态为1
- 如果找到了有数符合和当前数小于临界值,这些数里面拿出最大的那个数出来喝当前数组成一组,两个被使用的数使用bool数组进行标记
#include <bits/stdc++.h>
using namespace std;
int n, w, a[30005], sum;
bool b[30005]; // 标记某个数是否已经被放入分组
int nihao(int idx) //不超过临界值 w 的数中,找“最大的那个数”的下标。当前数的下标若找到返回下标;否则返回 -1
{
int pos = -1; // 保存最优下标
for (int j = 1; j <= n; j++)
{
if (j == idx || b[j]) // 自己或已用过的数跳过
continue;
if (a[idx] + a[j] > w) // 和超过临界值也跳过
continue;
if (pos == -1 || a[j] > a[pos]) // 记录更大的可行值
pos = j;
}
return pos; // -1 代表没有可配对的数
}
int main()
{
cin >> w;
cin >> n;
if (n == 1) // 只有一个数必然占一组
{
cout << 1 << endl;
return 0;
}
for (int i = 1; i <= n; i++)
cin >> a[i];
// 逐个尝试把每个数放入最优的二元组
for (int i = 1; i <= n; i++)
{
if (b[i]) continue; // 已经被分过组的跳过
int j = nihao(i); // 找能与它配对的“最大”数
if (j == -1) // 找不到可配对的,单独成组
{
sum++;
b[i] = 1;
}
else // 找到,二者成组
{
sum++;
b[i] = b[j] = 1; // 标记两数已用
}
}
cout << sum << endl;
return 0;
}
习题12-7 跳跳!
我们构建这样的一个贪心策略:
我们的目的其实就是平方和最大,首先我们要找到最大的一个数作为起点,把整个数组进行从大到小的排序,每次都从最大的跳向最小的,再从最小的跳向次大的,下一个次大的跳向前一个次小的,重复这样就可以使得花费的力气最大
我们可以使用双指针来更新我们每次的极大值和极小值
#include <bits/stdc++.h>
using namespace std;
int n, a[305];
long long sum; // 改成 long long 防止溢出
bool cmp(int a, int b) // 从大到小排序
{
return a > b;
}
int main()
{
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> a[i];
}
sort(a + 1, a + n + 1, cmp);
int l = 1, r = n;
int cur = a[l];
sum += 1LL * cur * cur;
++l;
while (l <= r)
{
// 跳到当前最小值
sum += 1LL * (cur - a[r]) * (cur - a[r]);
cur = a[r];
--r;
if (l > r) break;
// 跳到当前最大值
sum += 1LL * (cur - a[l]) * (cur - a[l]);
cur = a[l];
++l;
}
cout << sum << endl;
return 0;
}
感谢您的观看,您的关注是我持续更新下去的动力,代码如有错误或者更优解,欢迎评论区讨论
总结
我们在学习贪心算法的过程中了解了贪心解决的最优解问题是无数个片段最优解(局部最优解)的可连续性组合而成的,如果中间有其它情况导致这种局部最优不可以扩展到全局最优,就说明此时我们贪心的策略不成立,此时就需要考虑使用动态规划或者搜索的思想进行解决