若在求解一个问题时,能根据每次所得到的局部最优解,推导出全局最优或最优目标。那么,我们可以根据这个策略,每次得到局部最优解答,逐步而推导出问题,这种策略称为贪心法。
贪心算法没有固定的算法框架,算法设计的关键是贪心策略的选择。必须注意的是,贪心算法不是对所有问题都能得到整体最优解,选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。
所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。
基本思路:
1.建立数学模型来描述问题。
2.把求解的问题分成若干个子问题。
3.对每一子问题求解,得到子问题的局部最优解。
4.把子问题的解局部最优解合成原来解问题的一个解。
贪心算法适用的问题
贪心策略适用的前提是:局部最优策略能导致产生全局最优解。
实际上,贪心算法适用的情况很少。一般,对一个问题分析是否适用于贪心算法,可以先选择该问题下的几个实际数据进行分析,就可做出判断。
贪心策略的选择
因为用贪心算法只能通过解局部最优解的策略来达到全局最优解,因此,一定要注意判断问题是否适合采用贪心算法策略,找到的解是否一定是问题的最优解。
模板:
均分纸牌(必做) https://www.luogu.com.cn/problem/P1031
独木桥(必做) https://www.luogu.com.cn/problem/P1007
导弹拦截 (必做)https://www.luogu.com.cn/problem/P1020
货币系统 (必做)https://www.luogu.com.cn/problem/P5020
肮脏的牧师(选做) https://www.luogu.com.cn/problem/P3944
隐藏口令(选做) https://www.luogu.com.cn/problem/P1709
下面通过各个模板的解析来具体学习怎样贪心
👉均分纸牌
通过对题目的分析简化问题,题目意思是通过调整第i+1堆,去确保第i堆的纸牌数达到平均数,若果第i堆比均值小,那么从第i+1堆调取牌数,反之,将i堆多出来的放入第i+1堆。从而保证每一项都等于均值。
average=sum_num/n;//找到纸牌的均值
for(int i=0;i<n;i++){//遍历所有堆
if(s[i]!=average){//判断是否等于均值
s[i+1]+=s[i]-average;//s[i]-average 可能是正可能是负,正就从第i堆多余的调入第i+1,反之。
s[i]=average;//更新是第i堆的均值
count++;//累计调动次数
}
}
完整代码:
#include<iostream>
using namespace std;
int main()
{
int n;
cin >> n;
int s[100];
int sum = 0;
int average;
int count = 0;
for (int i = 0; i < n; i++)
{
cin >> s[i];
sum += s[i];
}
average = sum / n;
for (int i = 0; i < n; i++) {
if (s[i] != average) {
s[i + 1] += s[i] - average;
s[i] = average;
count++;
}
}
cout << count;
return 0;
}
👉独木桥
通过对题目的分析简化问题,题目大致意思为创建一个直线坐标系,刻度从1-L,士兵之在整数点上,且每次只能移动一个单位,不允许一个点出现两个士兵的情况,士兵都有初始方向,如果两人相遇,分别转身继续行走,士兵撤出需要离着桥头较近的先撤出里面的才能撤出。
运用贪心思想,假如士兵在中点左边则往左走时间最短,往右走时间最长;士兵在中点右边则相反。然后是可以输入一次数据刷新一次答案。
#include<iostream>
using namespace std;
int main()
{
int num, L, post;
int max_move = 0, min_move = 0;
//初始化最少和最多撤出时间
cin >> L >> num;//输入独木桥长度,总人数
//每次输入一个坐标判断该点撤出的最短和最长时间
for (int i = 1; i <= num; i++)
{
cin >> post;
//获取所有坐标对应最长时间中撤出的最长时间作为最终最长撤出时间
max_move = max(max_move, max(L - post + 1, post));
//获取所有坐标对应最短时间中撤出最长的时间作为最终最短撤出时间
min_move = max(min_move, min(L - post + 1, post));
}
cout << min_move << " " << max_move;
return 0;
}
【模板 导弹拦截】
题目链接:P1020 导弹拦截
这个题目有两个问题,第一问就是求一串数中的最长非递增序列的长度,第二问有一点费解,但是其实无异于求最长的递增序列,但是出题者竟然一定要用n*log(n)的做法(贪心)。
在解决这两个问题之前,在代码中会多次用到二分,这里使用了C++中的lower_bound( )和upper_bound( )两个方法,可以方便使用。
在从小到大的排序数组中,
lower_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num):从数组的begin位置到end-1位置二分查找第一个大于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
在从大到小的排序数组中,重载lower_bound()和upper_bound()
lower_bound( begin,end,num,greater() ):从数组的begin位置到end-1位置二分查找第一个小于或等于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
upper_bound( begin,end,num,greater() ):从数组的begin位置到end-1位置二分查找第一个小于num的数字,找到返回该数字的地址,不存在则返回end。通过返回的地址减去起始地址begin,得到找到数字在数组中的下标。
for (int i = 2; i <= n; i++) {
//若比最大的长度也就是最小高度那个导弹还小,则加入一颗, 并且此序列是递减序列
if (down[len1] >= a[i]) down[++len1] = a[i];
//若大过最小的那颗,则从此递减序列中找到第一颗比它小的保证到第 i 颗 导弹前的导弹都比它大,
//且它是所有第i颗导弹中最大的,因此可以保证后面的也是最优
else *upper_bound(down + 1, down + 1 + len1, a[i], greater<int>()) = a[i];
//若比当前发射器的高度还要高,则添加一个新的发射器,并且此序列是递增序列
if (cnt[len2] < a[i]) cnt[++len2] = a[i];
//若低过最大的发射器高度,则在现在有的发射器中选第一个大于它的发射器,并且修改其高度
else *lower_bound(cnt + 1, cnt + 1 + len2, a[i]) = a[i];
}
AC代码:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1e5 + 5;
int a[N], down[N], cnt[N], n;
int main() {
while (cin >> a[++n]); n--;
int len1 = 1, len2 = 1; //len1的长度为一次性可以发射的最多导弹,len2的长度为最多需要的发射器数量
down[1] = cnt[1] = a[1];
for (int i = 2; i <= n; i++) {
//若比最大的长度也就是最小高度那个导弹还小,则加入一颗, 并且此序列是递减序列
if (down[len1] >= a[i]) down[++len1] = a[i];
//若大过最小的那颗,则从此递减序列中找到第一颗比它小的保证到第 i 颗 导弹前的导弹都比它大,
//且它是所有第i颗导弹中最大的,因此可以保证后面的也是最优
else *upper_bound(down + 1, down + 1 + len1, a[i], greater<int>()) = a[i];
//若比当前发射器的高度还要高,则添加一个新的发射器,并且此序列是递增序列
if (cnt[len2] < a[i]) cnt[++len2] = a[i];
//若低过最大的发射器高度,则在现在有的发射器中选第一个大于它的发射器,并且修改其高度
else *lower_bound(cnt + 1, cnt + 1 + len2, a[i]) = a[i];
}
cout << len1 << "\n" << len2 << endl;
return 0;
}
【模板 货币系统】
题目链接:P5020 货币系统
这个题目就是一个贪心+完全背包,很容易想到,如果想要用化简会被其他货币凑出的货币,那么就必须从小到大凑(因为大的一定凑不出小的,而小的有可能凑出大的)。如何进行判断呢?我们就可以写一个完全背包判断。
思路:将num数组从小到大排序,因为最小的数必须要选,然后利用完全背包的思想,从num[i]到最大值筛选一遍,将可以组成的打上标记,再判断后面的数字时,如果已经被标记过了,就不再选,没有被标记过就标记一下,再筛选一次数(即一次完全背包)。
#include<iostream>
#include<algorithm>
#include<cmath>
#include<string>
#include<cstring>
using namespace std;
int T;
int num[109];
int n,ans;//ans记录答案
bool dp[25009];//记录在某货币之前的所有货币可能凑出的值(false为不能,true为能)
int maxn;//记录整个货币系统中最大的面值
bool judge(int m)//判断num[m]是否已经凑出并计算加上这个面值之后可以凑出的值
{
if(dp[num[m]]==true)return true;//已经凑出了就返回true
dp[num[m]]=true;//那么就需要将其标记为true
for(int j=num[m];j<=maxn;j++)//完全背包模板但是已经知道了这个数是多少就不用去枚举了
{
if(dp[j-num[m]]==true)dp[j]=true;
}
return false;//直接返回false
}
void shaishu()
{
for(int i=1;i<=n;i++)
{
if(judge(i)==true)continue;//如果是可以被凑出ans就不用增加
else ans++;
}
}
int main()
{
cin>>T;
while(T--)
{
memset(num,0,sizeof(num));
memset(dp,false,sizeof(dp));//一定要清空dp不然上几组数据的值都会残留在其中使答案不对
ans=0;//同时将ans归零
cin>>n;
for(int i=1;i<=n;i++)cin>>num[i],maxn=max(maxn,num[i]);//在读入的同时计算出最大的面值
sort(num+1,num+n+1);//排序将小面值放在前面
shaishu();//开始筛选
cout<<ans<<endl;//最后输出结果
}
}
肮脏的牧师(选做)
#include<bits/stdc++.h>
using namespace std;
int datas[30001] = { 0 };//随从攻击力将数据初始化为0
int main() {
int num, health;
cin >> num >> health;
for (int i = 0; i < num; i++)
{
int reg;//采用水桶排序输入数据
scanf("%d", ®);
datas[reg]++;
}
int one = 0;//记录攻击为1,2,3的随从数量
int two = 0;
int three = 0;
int ans_num = 0;//记录实用缩小药水的数量
int ans_spend = 0;//记录花费费用
int ans_attack = 0;//记录总攻击力
while (1)
{
one += datas[1];//将攻击力为1,2,3的进行整合,计算总费用和攻击力
ans_spend += datas[1];
ans_attack += datas[1];
datas[1] = 0;
two += datas[2];
ans_spend += datas[2];
ans_attack += datas[2] * 2;
datas[2] = 0;
three += datas[3];
ans_spend += datas[3] * 4;
ans_attack += datas[3] * 3;
datas[3] = 0;
if (ans_attack >= health || one + two + three == num)break;//如果攻击力大于生命值或者已经审查完则退出
ans_num++;
for (int i = 4; i < 29998; i++)//使用疯狂药水之后所有随从减少3点攻击力
{
if (datas[i] != 0)
{
datas[i - 3] = datas[i];
datas[i] = 0;
}
}
}
if (ans_attack < health)//如果发现无论如何不能攻击力大于生命值
{
cout << "Human Cannot Win Dog";
return 0;
}
while (ans_attack - 3 >= health && three > 0)//优先去除攻击为3的随从相当于减少4费减少3攻击
{
ans_spend -= 4;
three--;
ans_attack -= 3;
}
while (ans_attack - 1 >= health && one > 0)//其次减少1攻的,相当于减少1费减1攻
{
ans_spend--;
one--;
ans_attack--;
}
while (ans_attack - 2 >= health && two > 0)//最后去除2攻的相当于减少1费减2攻
{
ans_spend--;
two--;
ans_attack -= 2;
}
cout << ans_num << ' ' << ans_spend + ans_num;//计算结果
}
隐藏口令(选做)
#include<bits/stdc++.h>
using namespace std;
char s[5000005];
int main()
{
int num;
cin >> num;
for (int i = 0; i < num; i++)cin >> s[i];//读入数据
int i = 0, j = 1, k = 0;
while (1)
{
int ik = (i + k) % num;//如果超出范围则从头开始
int jk = (j + k) % num;
if (s[ik] == s[jk])k++;//相等则比较后面的字母
else if (s[ik] > s[jk])//大于代表前面的后面的字母排序靠前,故更改大的
{
i = i + 1 + k;
k = 0;
}
else
{
j = j + 1 + k;
k = 0;
}
if (j == i)j++;//如果相同会陷入死循环所以更改其中一个
if (i >= num || j >= num || k >= num)break;//如果有其中一个跑完了就退出循环,如果k值超出范围则代表循环
}
if (i >= j)cout << j;//输出未超出范围的
else cout << i;
}