该文章是算法设计与分析(第二版)中各章的课后习题 主编 : 李春葆
在本文中,主要是对该书第七章-贪心算法章节中的上机实验题以及在线编程题进行代码解答。
代码运行环境是:DEVc++
用贪心法求解的问题应具有的性质:
贪心法总是做出在当前看来最好的选择,这个局部最优选择仅依赖以前的决策,不依赖于以后的决策。由于贪心法一般不会测试所有可能路径,而且容易过早做决定,因此有些问题可能不会找到最优解,能够采用贪心法求解的问题一般具有两个性质——贪心选择性质和最优子结构性质,所以贪心算法一般需要证明满足这两个性质。
1.贪心选择性质:
所谓贪心选择性质是指所求问题的整体最优解可以通过一系列局部最优的选择(即贪心选择)来达到。也就是说,贪心法仅在当前状态下做出最好选择,即局部最优选择,然后再去求解做出这个选择后产生的相应子问题的解。它是贪心法可行的第一个基本要素,也是贪心算法与后面介绍的动态规划算法的主要区别。
对于一个具体问题,要确定它是否具有贪心选择性质,必须证明每一步所做的贪心选择最终导致问题的整体最优解。这通常采用数学归纳法来证明,先考虑问题的一个整体最优解﹐并证明可以修改这个最优解,使其从贪心选择开始,在做出贪心选择后原问题转化为规模较小的类似问题,通过每一步的贪心选择,最后可得到问题的整体最优解。
2、最优子结构性质:
如果一个问题的最优解包含其子问题的最优解,则称此问题具有最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法或贪心法求解的关键特征。
在证明问题是否具有最优子结构性质时通常采用反证法来证明,先假设由问题的最优解导出的子问题的解不是最优的,然后证明在这个假设下可以构造出比原问题的最优解更好的解,从而导致矛盾。
3、贪心法的一般求解过程:
用贪心法求解问题的基本思路如下:
(1) 建立数学模型来描述问题。
(2)把求解的问题分成若干个子问题。
(3) 对每一个子问题求解,得到子问题的局部最优解。
(4)把子问题的局部最优解合成原来解问题的一个解。
上机实验题:
问题一:求解一个序列中出现次数最多的元素问题
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
//7
//10 1 10 20 30 20 20
int main(){
int max = 0;
int result;
int n;
cin>>n;
int a[n];
for(int i = 0 ; i < n;i++){
cin>>a[i];
}
//排序
sort(a,a+n);
result = a[0];
for(int i = 0 ; i < n;i++){
int tp = 1;
while(i+1 < n && a[i] == a[i+1]){
tp++;
i++;
}
if(tp > max ){
max = tp;
result = a[i];
}
}
cout<<result<<endl;
return 0;
}
运行截图:
问题二:求解删数问题
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
//178643 4 ---> 13
//141519 2 --->1119
int main(){
string n;
int k;
int len = n.size();
cin>>n>>k;
//当删除元素为整数的长度时
if(len == k){
cout<<0<<endl;
}else{
//按照高位到低位的顺序搜索,若各位数字递增则删除最后一个数字,
//若存在递减的区间,则删除递减区间的第一个数字
int j = 0;
while(j++ < k){
for(int i = 0 ; i < n.size()-1;i++){
if(n[i] > n[i+1]){
n.erase(i,1);
break;
}else if(i == n.size()-2){
n.erase(n.size()-2,1);
}
}
cout<<"删除 "<<j<<" 位 "<<n<<endl;
}
}
cout<<"最小结果为: "<<n<<endl;
return 0;
}
运行截图:
问题三:求解汽车加油问题
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
//思路:要使得加油次数最少,我们可以每次选择跑到能够到达的最远的
//加油站去加满油箱 ,最终的加油次数即为最少加油次数
int main(){
// int a[] = {2,7,3,6};
int a[] = {1,2,3,4,5,1,6,6};
int sum = 0;
int temp = 7;
int len = sizeof(a)/sizeof(a[0]);
for(auto h : a){
cout<<h<<" ";
}
cout<<endl;
for(int i = 0 ;i < len;i++){
if(i+1<len && temp - a[i] -a[i+1] < 0){
cout<<"在第 "<<i+1<<" 个加油站加油"<<endl;
sum++;
temp = 7;
continue;
}
if(i == len - 1 && temp < a[len-1]){
sum++;
}
temp -= a[i];
}
cout<<"总共加油"<<sum<<"次"<<endl;
return 0;
}
运行截图:
问题四:求解磁盘驱动调度问题
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
//98,183,37,122,14,124,65,67
//c == 53;
//思路:先排序请求序列,再找到距离磁头最近的序列位置,然后比较当前位置左右序列,将较近的序列放入结果数组中,
//更新当前位置,重复上述操作,直到达到数组边界,将剩余序列加入结果数组即可
int c= 53;
int main(){
int line[] = {98,183,37,122,14,124,65,67};
int len = sizeof(line)/sizeof(line[0]);
vector<int> a(line,line+len);
//result保存调度方案
vector<int> result;
sort(a.begin(),a.end());
int k = 0,sum = 0;
int left = 0,right = 0;
//法二 : 双指针法
if(1){
int min = 1000;
//找到与当前 磁道最近的磁道
for(int i = 0 ;i < a.size();i++){
if(abs(a[i] - c) < min){
min = abs(a[i] - c);
k = i;
}
}
// 加入结果数组
result.push_back(a[k]);
sum += abs(a[k]-c);
//更新当前磁道
c = a[k];
if(k-1 >= 0){
left = k-1;
}
if(k + 1 < len){
right = k+1;
}
while(1){
if(c-a[left] < a[right]-c){ // 向左找
result.push_back(a[left]);
c = a[left];
left--;
}else{
result.push_back(a[right]); //向右找
c = a[right];
right++;
}
if(left == -1 || right == len){
break;
}
}
while(left != -1){
result.push_back(a[left]);
left--;
}
while(right < len){
result.push_back(a[right]);
right++;
}
}
//法一:遍历
// while(len--){
//
// int min = 1000;
// //找到与当前 磁道最近的磁道
// for(int i = 0 ;i < a.size();i++){
// if(abs(a[i] - c) < min){
// min = abs(a[i] - c);
// k = i;
// }
// }
// // 加入结果数组
// result.push_back(a[k]);
// sum += abs(a[k]-c);
// //更新当前磁道
// c = a[k];
// //将已经经过的磁道置为最大值
// a[k] = 10000;
// }
for(auto h:result){
cout<<h<<" ";
}
cout<<endl<<"总路径为:"<<sum<<endl;
return 0;
}
运行截图:
在线编程题
问题一:求解最大乘积问题
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <algorithm>
using namespace std;
//思路: 将元素加入优先队列, qu 保存最大的3个数,fu保存最小的3个数。
// 最大值为,qu中3个元素相乘 ,或者最小的两个数相乘再乘以以最大的数
int main(){
priority_queue<int,vector<int>,greater<int>> qu;
//fu处理负数的情况
priority_queue<int,vector<int>,less<int>> fu;
int k = 3;
int a[] = {3,4,1,2,13,2,2,-32,-13};
int len = sizeof(a)/sizeof(a[0]);
int max = 1;
int i = 0,n = len;
while(i < n){
qu.push(a[i]);
fu.push(a[i]);
if(i >= 3){
fu.pop();
qu.pop();
}
i++;
}
int max2;
while(!qu.empty()){
max *= qu.top();
max2 = qu.top();
// cout<<qu.top()<<" ";
qu.pop();
}
fu.pop();
while(!fu.empty()) {
max2 *= fu.top();
// cout<<fu.top()<<endl;
fu.pop();
}
if(max < max2){
max = max2;
}
cout<<"3个数的最大乘积 = "<<max<<endl;
return 0;
}
运行截图:
问题二:区间覆盖问题
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
using namespace std;
//5 3 1 3 8 5 11
//6 3 1 3 7 8 9 10
//思路: 先对元素进行排序,求出元素之间的距离,如果 距离大于 1 则 ans 加上该距离,
// 当间隙的个数 j大于 n 时 ,ans需要加上(j - n)个长度为 1 的区间
//从大到小排序
static bool cmp(int a,int b){
return a > b;
}
int main()
{
int m,n,i,j = 0,a[202];
int ans = 0;
scanf("%d%d",&m,&n);
for(i=0; i<m; i++)
{
scanf("%d",&a[i]);
}
//对时段进行排序
sort(a,a+m);
cout<<"间隙值为: "<<endl;
for(i=0; i<m-1; i++)
{
if(a[i+1]-a[i]-1){
j++; // j 存储的是间隙的个数
ans += a[i+1]-a[i]-1;
}
cout<<a[i+1]-a[i]-1<<" ";
}
cout<<endl;
// 当间隙的个数 j大于 n 时 需要加上(j - n)个长度为 1 的区间
if(j > n){
ans += j-n;
}
cout<<"画线段的长度之和最小为:"<<ans<<endl;
return 0;
}
运行截图:
问题三:求解 Wooden Sticks问题
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
//5 4 9 5 2 2 1 3 5 1 4
//6 4 9 5 4 2 1 3 5 1 4 3 4
//3 2 2 1 1 2 2
//3 1 3 2 2 3 1
//思路:先将w按照升序排列,如果w相同,再将L按照升序排列。遍历L,其中逆序区间的个数+1,
//就是所需时间
struct NodeType{
int L;
int W;
//重载运算符 <
bool operator <(const NodeType &s)const{
if(W == s.W){
return L < s.L;
}
return W < s.W;
}
};
int main(){
int n;
cin>>n;
NodeType a[n];
int i = 0;
while(i < n){
NodeType e;
cin>>e.L>>e.W;
a[i] = e;
i++;
}
sort(a,a+n);
for(auto h : a){
cout<<h.L<<" "<<h.W<<endl;
}
int time = 1;
for(int i = 1 ; i < n; i++){
if(a[i].L < a[i-1].L){
time++;
}
}
cout<<"所需的时间为:"<<time<<endl;
return 0;
}
运行截图:
问题四:求解奖学金问题
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
//4 80 5 70 2 90 3 60 1 100 92.5
//思路: 先对元素根据di进行排序,每次先复习di较小的科目,直到考100分
struct NodeType{
int common;
int di;
bool operator <(const NodeType &s){
return di <= s.di;
}
};
int main(){
int n;
cin>>n;
NodeType a[n];
int i = 0;
int point = 0;
while(i < n){
NodeType e;
cin>>e.common>>e.di;
a[i] = e;
point += e.common;
i++;
}
int r ;
double avg;
cin>>r>>avg;
//平均分乘以课程数,得到总分avg ,当总分超过avg即可
avg *= n;
sort(a,a+n);
for(auto h : a){
cout<<h.common<<" "<<h.di<<endl;
}
//time 为复习时间
int time = 0;
i = 0;
while(point < avg){
if(point + r-a[i].common > avg){
int k = avg - point;
point += k;
time += a[i].di * k;
}else{
point += r-a[i].common;
time += (r-a[i].common)*a[i].di;
}
i++;
}
cout<<"需要的总时长为:"<<time<<endl;
return 0;
}
运行截图:
问题五:求解赶作业问题
#include <iostream>
#include <vector>
#include <string>
#include <unordered_map>
#include <algorithm>
using namespace std;
//3 3 3 3 10 5 1
//3 1 3 1 6 2 3
//思路: 对于同一天提交的作业,做最大惩罚的作业,其他作业接受处罚
struct NodeType{
int day;
int punish;
int num;
bool operator <(const NodeType &s)const{
if(punish == s.punish){
return day < s.day;
}
return punish > s.punish;
}
};
int main(){
int n;
cin>>n;
//扣分数
int score = 0;
//用哈希表进行去重,对于同一天提交的作业,做最大惩罚的作业,其他作业接受处罚
unordered_map<int,int>mp;
NodeType a[n];
int i = 0 ;
vector<int> c,b;
//输入天数
while(i++ < n){
int x ;
cin>>x;
c.push_back(x);
}
i = 0;
//输入处罚
while(i++ < n){
int x ;
cin>>x;
b.push_back(x);
}
i = 0;
//将天数、处罚、编号统一到结果体中
while(i < n){
NodeType e;
e.day = c[i];
e.punish = b[i];
e.num = i+1;
a[i] = e;
i++;
}
//对结构体进行排序,按照,处罚的力度大小进行排序。
sort(a,a+n);
for(auto h : a){
cout<<h.punish<<" "<<h.day<<endl;
}
i = 1;
cout<<"执行作业顺序:"<<endl;
for(auto h : a){
if(mp[h.day]){
score += h.punish;
}else{
mp[h.day] = h.num;
cout<<h.num<<" ";
}
}
cout<<endl;
cout<<"总共处罚的分数为:"<<score<<endl;
return 0;
}
运行截图:
文章中若出现错误,请道友不吝指出