《算法设计与分析》第四章:贪心算法

前言

本篇博客讲解《算法设计与分析》(耿国华版)第四章:贪心算法的内容,更多贪心相关内容可以查看本人其他博客:

《贪心算法-区间问题、Huffman树详解》
《贪心算法-排序不等式、绝对值不等式、推公式》

贪心相关概念

贪心算法通过做一系列的贪心选择,给出某一问题的最优解。
对算法中的每一个决策点做出当时看起来最佳的选择。
贪心算法的基本步骤:
(1)选择合适的贪心策略;
(2)根据贪心策略,写出贪心选择的算法,求得最优解;
(3)证明在此策略下,该问题具有贪心选择性质和最优子结构性质,
贪心算法经常需要排序。
贪心法不能保证问题总能得到最优解。

一、本章作业

1.股票买卖(输出时间)

L老师是超准的预测家,预测到一支股票一段时间内每天的价格,而且很准。如果买卖股票没有手续费,且可以多次买卖,你帮L老师设计一个算法,哪天买入,哪天卖出,可以获得最大收益?
输入:股票一段时间内的价格,如23 18 22 35 16 8 4 30 35
输出:多组买入时间和卖出时间,以及总体收益。

  • 找上涨区间即可
#include <bits/stdc++.h>

using namespace std;

const int MMM = 10010;
int a[MMM];
int n, ans;

void solve()
{
	int i = 1, j = 1;
	for(; j <= n; j ++ )
	{
		if(a[j+1] < a[j])
		{
			ans = ans + a[j] - a[i];
			if(i!=j)
				cout << "买入时刻:" << i << "   " << "卖出时刻:"<< j <<endl;
			i = j + 1;
		}
	}
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i ++ )
	{
		cin >> a[i];
	}
	solve();
	cout << ans << endl;
	
	return 0;
}
  • 测试:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.最小跳跃次数问题

给定一个非负整数数组(长度<=100),你最初位于数组的第一个位置。数组中的每个元素(值在1-1000之间)代表你在该位置可以跳跃的最大长度。假设总可以到达数组的最后一个位置,设计算法,如何使用最少的跳跃次数到达数组的最后一个位置。输出跳跃点及跳跃次数。

  • 从头开始,每次找可跳区间的最远达到距离,因为必然可以达到,所以直接取该距离
#include <bits/stdc++.h>

using namespace std;

const int MMM = 1e5 + 20;
int a[MMM];
int n,ans;

void solve()
{
	int i = 1, j = 1;
	while(j < n)
	{
		ans ++;
		j = i + a[i];
		int m = i;
		for(int u = i + 1; u <= j; u ++ )
		{
			if(u + a[u] > m + a[m]) m = u;
		}
		i = m;
	}
	cout << ans <<endl;
}


int main()
{
	cin >> n;
	for(int i = 1; i <= n; i ++ ) cin >> a[i];
	solve();
	return 0;
}
  • 测试:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.浇水问题

某单位购买了一些名贵的花。为了给它们浇水,每盆花之间都安装了一个喷水龙头。有n盆花,会有n+1个喷水龙头,如下图所示。但是,这些龙头用久了之后,能够喷水的范围变得乱七八糟,有的喷得远,有的喷得近,有的甚至坏掉了。园丁用一组数字记录了每个龙头的喷水距离。比如[2,3,0,0,1],以第1个喷头为例,2表示它的喷水范围是[-2,2],也就是说,它可以浇灌到前2盆花。你帮忙设计一个算法,园丁最少开多少个喷头,就可以浇到所有的花。
在这里插入图片描述
在这里插入图片描述

  • 可以转化为区间覆盖问题
  • 把喷水距离预处理为区间即可
  • 将所有区间按照左端点从小到大排序
  • 从前往后依次枚举每个区间,在所有能覆盖st的区间中,选择右端点最大的区间,满足区间最优
  • 将start更新为右端点的最大值
#include <bits/stdc++.h>

using namespace std;

const int N = 100010;

int n;
struct Range
{
	int l, r;
	bool operator< (const Range &W)const
	{
		return l < W.l;
	}
}range[N];

int main()
{
	cin >> n;
	int st = 0,ed = n;
	for (int i = 1; i <= n; i ++ ) 
	{
		int u;
		cin >> u;
		if(u-1>=0) 
		{
			u = u-1;
			range[i].l = i - u - 1;
			range[i].r = i + u;
		}
		else
		{
			range[i].l = -1;
			range[i].r = -1;
		}
	}
	
	sort(range, range + n);
	
	int res = 0;
	bool success = false;
	//用了双指针思想(i,j)
	for (int i = 1; i <= n; i ++ )
	{
		int j = i, r = -2e9;
		while (j <= n && range[j].l <= st + 1)
		{
			r = max(r, range[j].r);
			j ++ ;
		}
		if (r < st)
		{
			res = -1;
			break;
		}
		
		res ++ ;
		if (r >= ed - 1)
		{
			success = true;
			break;
		}
		
		st = r;
		i = j - 1;
	}
	
	if (!success) res = -1;
	printf("%d\n", res);
	
	return 0;
}
  • 测试:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.乘积最大问题

给定一个无序数组,包含正数、负数和0。需要从中找出3个数字,使得它们的乘积最大,并且要求时间复杂度为O(n)(含排序时间),空间复杂度为O(1)。

  • 最大值共有四种情况:
    • 三个正数:数字本身越大则乘积越大
    • 两个负数一个正数:负负得正,所以两个负数最小,之积最大
    • 两个正数一个负数:这种情况,如果只有【正,正,负】,所有的情况都包含在第一种和第二种情况里,所以这种情况可以去掉。
    • 三个负数:
      ​​​​​​​这种情况只有【负,负,负】这一种情况,这种情况和第一种相重合,所以也可以去掉
#include<bits/stdc++.h>

using namespace std;

int main()
{
    long long int a;
    long long int max_num;
    int number;
    vector<long long int> num;
    cin>>number;
    for(int i=0;i<number;i++)
    {
        cin>>a;
        num.push_back(a);
    }
    if(num.size()==3)
    {
        cout<<num[0]*num[1]*num[2]<<endl;
    }
    else
    {
        sort(num.begin(),num.end());
        int len=num.size();
        max_num=max(num[len-1]*num[len-2]*num[len-3],num[len-1]*num[0]*num[1]);
        cout<<max_num<<endl; 
    }
    
    return 0;

}
  • 测试:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二、算法积累

1.部分背包问题

有 N 件物品和一个容量为 V 的背包。放入第 i 件物品耗费的空间是 Ci,得到的价值是 Wi,物品可以分割。求解将哪些物品装入背包可使价值总和最大。

  • 每次选择密度最高的物品
#include<bits/stdc++.h>

using namespace std;

struct albb
{
	int m;
	int v;
	double avg;
}al[101];

bool cmp(albb a,albb b)
{
	return a.avg>b.avg;
}

int main()
{
	int n,t;
	cin>>n>>t;
	for(int i=0;i<n;i++)
	{
		cin>>al[i].m>>al[i].v;
		al[i].avg = 1.0 * al[i].v/al[i].m;
	}
	sort(al,al+n,cmp);
	double ans,weight=0;
	for(int i=0;i<n;i++)
	{
		if(weight + al[i].m <= t)
		{
			weight += al[i].m;
			ans += al[i].v;
		}
		else
		{
			ans += al[i].avg * (t-weight);
			break;
		}
	}
	printf("%.2lf\n",ans);
	return 0;
}
  • 大小比较也可以用重载小于号的方法
#include<bits/stdc++.h>

using namespace std;

struct albb
{
	int m;
	int v;
	double avg;
	
	bool operator < (const albb &a) const
	{
		return a.avg < avg;
	}
}al[101];

int main()
{
	int n,t;
	cin>>n>>t;
	for(int i=0;i<n;i++)
	{
		cin>>al[i].m>>al[i].v;
		al[i].avg = 1.0 * al[i].v/al[i].m;
	}
	sort(al,al+n);
	double ans,weight=0;
	for(int i=0;i<n;i++)
	{
		if(weight + al[i].m <= t)
		{
			weight += al[i].m;
			ans += al[i].v;
		}
		else
		{
			ans += al[i].avg * (t-weight);
			break;
		}
	}
	printf("%.2lf\n",ans);
	return 0;
}

2.汽车加油

在这里插入图片描述

  • 唯一的度量标准就是我们加满油后已经走过的路程与即将所走的一段路程之和是否超过汽车的最大行驶路程,如果超过,则在该加油站加油,反之则出发
#include <bits/stdc++.h>

using namespace std;

const int maxn = 100;
int a[maxn];//加油站之间的距离
int n, k;

int found(int a[])
{
	int count = 0;
	int road = 0;
	
	for (int i = 0; i <= k; i++)
	{
		if (a[i] > n)
		{
			cout << "地点不可及" << endl;
			return -1;
		}
	}
	
	cout << "需要加油的的加油站为:" ;
	for (int i = 0; i <= k; i++)
	{
		road += a[i];//已经走的路程与即将要走的路程相加
		if (road > n)
		{//若结果超过汽车的最大限度
			count++;//需要加油
			road = a[i];//油箱加满油
			cout << i << " ";//输出加油的加油站
		}
	}
	cout << endl;
	return count;
}

int main()
{
	cout << "请输入汽车满油可行驶的距离和旅途中加油站的个数:" << endl;
	cin >> n >> k;
	cout << "请依次输入各个加油站之间的距离:" << endl;
	for (int i = 0; i <= k; i++)
		cin >> a[i];
	
	int count = found(a);
	if (count >= 0)
		cout << "最少加油次数为:" << count << endl;
	
	return 0;
}

3.活动安排(区间选点/最大不相交区间问题)

假设n个活动都需要使用某一场地,但场地在任何时刻只能被一个活动所占用, 且一旦开始不能被中断。 活动i有一个开始时间si和结束时间ei(si<ei),对于活动i和活动j,如果si≥ej或sj≥ei,则称这两个活动兼容。设计算法求一种最优活动安排方案,使得在某时间段内安排的活动个数最多。

  • 将每个区间按照右端点从小到大排序
  • 从前往后枚举每个区间:(1)如果当前区间已经包含点,则pass(2)否则,选择当前区间右端点
  • 选择右端点是为了覆盖尽可能多的区间
  • 每次都是一个局部最优解
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int n;
struct Range
{
    int l, r;
    bool operator< (const Range &W)const //重载小于号
    {
        return r < W.r;
    }
}range[N];

int main()
{
    scanf("%d", &n);
    for (int i = 0; i < n; i ++ ) scanf("%d%d", &range[i].l, &range[i].r);

    sort(range, range + n);

    int res = 0, ed = -2e9;
    for (int i = 0; i < n; i ++ )
        if (range[i].l > ed)
        {
            res ++ ;
            ed = range[i].r;
        }

    printf("%d\n", res);

    return 0;
}

4.农场畜栏(区间分组问题)

农场有n头牛,每头牛会有一个特定的时间区间[s,e]在蓄栏里挤牛奶,并且一个蓄栏里任何时刻只能有一头牛挤奶。现在农场主希望知道最少需要多少蓄栏能够满足上述要求,并给出每头牛被安排的方案。对于多种可行方案,输出一种即可。

  • 将所有区间按照左端点从小到大排序
  • 从前往后处理每个区间,判断能否将其放到某个现有的组中去(1)如果不存在这样的组,则开一个新组,将其放进去(2)如果存在这样的组,则将其放进去,并更新右端点的最大值
#include <bits/stdc++.h>

using namespace std;

const int N = 100010;

int n;
struct Range
{
	int l, r;
	bool operator< (const Range &W)const //重载小于号
	{
		return l < W.l;
	}
}range[N];

int main()
{
	cin >> n;
	for (int i = 0; i < n; i ++ )
	{
		int l, r;
		cin >> l >> r;
		range[i] = {l, r};
	}
	
	sort(range, range + n);
	
	priority_queue<int, vector<int>, greater<int>> heap;//小根堆
	
	for (int i = 0; i < n; i ++ )
	{
		auto r = range[i];
		if (heap.empty() || heap.top() > r.l) heap.push(r.r);
		else
		{
			heap.pop();
			heap.push(r.r);
		}
	}
	
	cout << heap.size() <<endl;
	
	return 0;
}

5.股票买卖

在这里插入图片描述

  • 第一问:从前往后遍历双指针,每次后指针比前指针所指元素小,则前指针赋值为后指针,反之,则计算此时的收益和ans做比较
#include <bits/stdc++.h>

using namespace std;

const int MMM = 10010;
int a[MMM];
int n, ans;

void solve()
{
	int i = 1, j = 2;
	for(; j <= n; j ++ )
	{
		if(a[i] > a[j])
		{
			i = j;
		}
		else
		{
			ans = max(ans, a[j] - a[i]);
		}
	}
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i ++ )
	{
		cin >> a[i];
	}
	solve();
	cout << ans << endl;
	
	return 0;
}
  • 第二问:找上涨区间即可
#include <bits/stdc++.h>

using namespace std;

const int MMM = 10010;
int a[MMM];
int n, ans;

void solve()
{
	int i = 1, j = 1;
	for(; j <= n; j ++ )
	{
		if(a[j+1] < a[j])
		{
			ans = ans + a[j] - a[i];
			i = j + 1;
		}
	}
}

int main()
{
	cin >> n;
	for(int i = 1; i <= n; i ++ )
	{
		cin >> a[i];
	}
	solve();
	cout << ans << endl;
	
	return 0;
}
  • 进阶:如果手续费需要2
#include <iostream>
#include <vector>

using namespace std;

int maxProfit(vector<int>& prices, int fee) 
{
	int n = prices.size();
	int profit = 0;
	int buyPrice = prices[0];  // 初始买入价格设为第一天的价格
	
	for (int i = 1; i < n; ++i) {
		// 如果当前价格低于之前的买入价格,则考虑在这一天买入
		if (prices[i] < buyPrice) 
		{
			buyPrice = prices[i];
		}
		
		// 如果当前价格减去买入价格大于交易费用,则考虑卖出
		if (prices[i] > buyPrice + fee) 
		{
			profit += prices[i] - buyPrice - fee;  // 计算这次交易的利润
			buyPrice = prices[i] - fee;  // 更新买入价格为当前价格减去交易费用
			// 这一步是为了防止在价格小幅波动时重复交易
		}
	}
	
	return profit;
}

int main() 
{
	vector<int> prices = {1, 3, 2, 8, 4, 9};
	int fee = 2;
	cout << "最大利润: " << maxProfit(prices, fee) << endl;
	return 0;
}

6.摇摆序列

一个整数序列,如果两个相邻元素的差恰好正负(或负正)交替出现,则该序列称为摇摆序列,一个小于2个元素的序列直接为摇摆序列 。给一个随机序列,求这个序列的最长摇摆子序列长度。

  • 算有几个转折点即可
#include<bits/stdc++.h>

using namespace std;

int n;

int wig(vector<int> &nums)
{
	if(nums.size() < 2) return nums.size();
	
	static const int BEGIN = 0;
	static const int UP = 1;
	static const int DOWN = 2; //扫描时候的三种状态
	
	int STATE = BEGIN;
	int max_length = 1;//摇摆序列最大长度至少为1,0个和1个元素就返回了,现在至少两个元素 
	
	for(int i = 1 ; i < nums.size() ; i++)
	{
		switch(STATE){ //当前状态-起始状态 
		case BEGIN:
			if(nums[i-1] < nums[i]){ //上升时候 
				STATE = UP; //状态转移 
				max_length++; 
			}else if(nums[i-1] > nums[i]){ //下降时候 
				STATE = DOWN;
				max_length++;
			}
			break; 
			
		case UP:
			if(nums[i-1] > nums[i]){
				STATE = DOWN;
				max_length++;
			}
			break;
			
		case DOWN:
			if(nums[i-1] < nums[i]){
				STATE = UP;
				max_length++;
			}
			break;
		} //switch 
	} //for 
	return max_length;	
} 

int main()
{
	vector<int> v;
	cin >> n;
	while(n--)
	{
		int a;
		cin >> a;
		v.push_back(a);
	}
	cout<<wig(v);
} 

7.多机调度

设有n个独立的作业{1,2,…,n},由m台相同的机器{1,2, …,m}进行加工处理,作业i所需的处理时间为ti (1≤i≤n),每个作业均可在任何一台机器上加工处理,但未完工前不允许中断,任何作业也不能拆分成更小的子作业。要求给出一种作业调度方案,使所给的 n个作业在尽可能短的时间内由m台机器加工处理完成。(贪心求近似解)

  • 每次取最长作业,但是只是近似解
#include<bits/stdc++.h>

using namespace std;

bool compare(int a,int b)
{
	return a>b;
	
}

int main(){
	int n,m; //作业个数为n, 机器个数为m
	
	cout<<"请输入作业和机器的个数:"<<endl; 
	cin>>n>>m;
	
	vector<int> time(n);
	vector<int> sumTime(m,0); //0表示初始化值为0 
	
	cout<<"请输入每个作业的处理时间:"<<endl; 
	for(int i=0;i<n;i++)
	{
		cin>>time[i];
	}
	sort(time.begin(),time.end(),compare); //对time进行排序,从大到小。
	
	for(int i=0;i<n;i++)
	{
		int select=0;
		for(int j=0;j<m;j++)
		{
			if(sumTime[j]<sumTime[select])
			{
				select=j;
			}
		}
		
		//machine[select].push_back(time[i]);
		sumTime[select]=sumTime[select]+time[i];	
	}
	
	int maxTime=sumTime[0];
	for(int j=0;j<m;j++)
	{
		if(sumTime[j]>maxTime)
		{
			maxTime=sumTime[j];
		}
	}
	for(int j=0;j<m;j++)
	{
		cout<<"第"<<j+1<<"台机器所需处理总时间为: "<<sumTime[j]<<endl; 
	}
	
	cout<<"处理所有作业时间共需: "<<maxTime;
	return 0;
}

8.移除k个数字

在这里插入图片描述

  • 用堆实现,放入元素小于堆顶元素则清除堆顶
  • 要注意清除前导0
    在这里插入图片描述
#include <bits/stdc++.h>

using namespace std;

int main()
{
	string num;
	int k;
	cin>>num>>k;
	string res="0"; // 避免处理边界
	for(int i=0; i<num.size(); i++)
	{
		while(k&&res.back()>num[i]) // 处理逆序
		{
			res.pop_back();
			k--;
		}
		res+=num[i]; 
	}
	while(k) res.pop_back(),k--; // 如果还剩余k,将后面删掉
	int i=0;
	while(i<res.size()&&res[i]=='0') i++; // 定位 去掉前导零的位置
	
	if(i==res.size()) cout<<"0"<<endl;
	else cout<<res.substr(i)<<endl;
	
	return 0;
}

9.跳跃游戏

在这里插入图片描述

  • 思路:
    在这里插入图片描述
  • 其实就是从前往后遍历,不断取最大能及距离,判断是否可以抵达最终点即可
#include <bits/stdc++.h>

using namespace std;

bool canJump(vector<int>& nums) 
{
	int max_len = 0;//最远可到达长度
	int len = 0;//遍历当前下标元素可到达的长度
	int sign = 0;//标志符
	
	//判断数组长度
	if (nums.size() == 1) {
		return true;
	} else {
		
		//遇到0时,进行判断
		for (int i = 0; i < nums.size(); i++) {
			
			if (nums[i] == 0) {
				if (max_len <= i) {
					sign = 1;
					break;
				}
			}
			
			//更新max_len
			len = i + nums[i];
			if (len > max_len) {
				max_len = len;
				if (max_len >= nums.size()-1) {
					break;
				}
			}
		}
		
		//最后根据标识符判断结果
		if (sign == 0) {
			return true;
		} else {
			return false;
		}
	}
}

int main()
{
	vector<int> a;
	int n;
	cin >> n;
	while(n -- )
	{
		int i;
		cin >> i;
		a.push_back(i);
	}
	bool b = canJump(a);
	if(b) cout << "true" << endl;
	else cout << "false" << endl;
	return 0;
}
  • 19
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值