学习笔记——贪心算法

本文介绍了贪心算法,即在问题求解时总做当前最优选择,关键在于贪心策略的选择,并给出适用与不适用的情况。还列举了删数、独木舟、最大子阵和等多个例题,详细阐述各题的贪心策略及证明,最后给出代码实现思路。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

贪心算法

什么是贪心算法

贪心算法(greedy algorithm,又称贪婪算法)是指:在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。
贪心算法关键是:贪心策略的选择。

适用情况举例

适用情况:桌面上有6张纸币,面额分别为:100、100、50、20、20、10现在你可以拿走3张纸币,要求总面额最大。你该怎么拿?能得到的最大总面额是多少?
答:每次都取面值最大的,最后的总面额一定最大。

不适用情况:桌面上有3件物品,每个物品的重量和价值情况如下:
物品一:重量 6,价值 9
物品二:重量 5,价值 7
物品三:重量 3,价值 3
你有一个承重为 8 的背包,在不超过背包承重上限的情况下,
你能获得的物品最大总价值是多少?
答:这里无法保证步步最优,不能使用贪心算法,但是可以使用回溯遍历和动态规划来解决。

例题

1.删数

题目描述
​ 输入一个高精度的正整数 n(长度不大于 240 位),去掉其中任意 s 个数字后剩下的数字按原左右次序将组成一个新的正整数,现求一种方案,使得新的正整数数值最小。

输入
​ 第一行一个整数 n。

​ 第二行一个正整数 s。

输出
​ 输出一个数表示最小值,输出时忽略数字的前导零。

样例输入1

179566
4

样例输出1

15

样例输入2

903071
3

样例输出2

1

贪心策略:

局部:
每次删除一个离最高位最近的逆序位的第一个数字1234321,其中43是离最高位最近的逆序位,删除4即可
整体:
按照如上策略执行 n 次以后,得到的就是最小的整数

贪心策略的证明:

证明1
假设:A、B 是只有一位不同的 n 位整数,其中 A ≤ B
证明:在同时删除 m 位的情况下,A 始终可以得到一个数字 A ,使得 A ≤ B
在这里插入图片描述
假设图中的红绿位置为两个整数不同的位置,若删除其之前与之后相同位置的数,A总是<B,若删除不同位置的那个数,A=B,故在同时删除 m 位的情况下,A 始终可以得到一个数字 A ,使得 A ≤ B。

证明2
n 位数字删除一个离最高位最近的逆序位的第一个数字,得到的是 n-1 位数字的最小值
在这里插入图片描述
假设图中标红的位置第k位是第一个逆序位,若删除在其之前的第i位上的数,得到的n-1位数第i位一定比原数的第i位大;若删除第k位,得到的n-1位数的第k位比原数要小;若删除其之后的第i位数,那么前k位数和原数的前k位相同。由此得出,删除第k位能够保证得到最小的n-1位数。

结论1:A、B 是若只有一位不同的,且 A ≤ B,A/B 采用相同删除策略时,总能保证 A ≤ B
结论2:n 位数字删除一个离最高位最近的逆序位的第一个数字,得到的是 n-1 位数字的最小值

结论二给出了得到最小n-1位数的策略,该最小的n-1位数就是结论1中的A,A小于其他删除策略得到的n-1位数,所以依据结论1,在A的基础上删数一定不会比在B的基础上删数得到的结果差,因此通过局部最优保证了全局最优。

代码实现

#include<iostream>
using namespace std;
char n[300];
int main()
{
	int s;
	cin>>n;
	cin>>s;
	for(int i=0;i<s;i++)//删数 
	{
		int j=0;
		while(n[j+1]&&n[j]<=n[j+1])j++;
		while(n[j])n[j]=n[j+1],j++;
	}
	for(int i=0,flag=1;n[i];i++)//去除前导0 
	{
		if(n[i]=='0'&&flag)continue;
		flag=0;
		cout<<n[i];
	}
	return 0;
 } 

2.独木舟

题目描述
​ 一群人去旅行,要租用独木舟,每艘独木舟最多乘两人,且所有独木舟有一个统一的载重限度。给出独木舟的载重限度和每个人的体重,现求最少需要租用多少独木舟。

输入
​ 第一行一个整数 w,表示独木舟的载重量。(80≤w≤200)
​ 第二行一个整数 n,表示旅游人数。 (1≤n≤30000)
​ 接下来 n 行,每行一个数表示 ai,即每个人的重量 (5≤ai≤w)

输出
​ 输出一个数表示最少需要的独木舟数量。

样例输入

100
9
90
20
20
30
50
60
70
80
90

样例输出

6

贪心策略:
局部:
每次安排,如果最重的人和最轻的人能坐一起,就坐一条独木舟,否则最重的人自己坐一条船。
整体:
按照如上策略执行,就能得到最少的独木舟数量。

证明1
假设:独木舟承重 w,人员全集是 A,子集分别为 X 1 与X 2 ,且 X 1 + X 2 = A,F 函数返回最少的独木舟数量
证明:F(A) ≤ F(X 1 ) + F(X 2 )

证明相等关系:由于X 1 + X 2 = A,按照 X 1 与 X 2 的分配方案,也是 A 的某一种合法的分配方案,所以相等关系可以成立。

证明小于关系
情况1: X 1 中存在某个一人拆分 a 1 ,以及 X 2 中存在另外一个一人拆分a 2 ,其中a 1 + a 2 ≤w
在这里插入图片描述
在这里插入图片描述

情况2: X 1 或 X 2 中存在一个两人拆分(a 1 ,a 2 ),并且存在另外两个一人拆分 a 3 ,a 4 ,使得,a 1 +a 3 ≤w 且a 2 +a 4 ≤w

在这里插入图片描述
在这里插入图片描述
证明2
证明:
F(x 1 ~x n ) = MIN[ F(x n ) + F(x 1 ~x n-1 ) , F(x 1 x n ) + F(x 2 ~x n-1 ) ]

结论1:F(A) ≤ F(X 1 ) + F(X 2 )
根据结论1,等价于证明:小于关系在此场景中不存在

证明:
F(x 1 ~x n ) 与 F(x n ) + F(x 1 ~x n-1 ) 不存在小于关系

在这里插入图片描述
证明:
F(x 1 ~x n ) 与 F(x 1 x n ) + F(x 2 ~x n-1 ) 不存在小于关系

在这里插入图片描述
综上,可以通过每一步选择MIN[ F(x n ) + F(x 1 ~x n-1 ) , F(x 1 x n ) + F(x 2 ~x n-1 ) ]来保证全局最优,贪心策略成立。

代码实现

#include<iostream>
#include<algorithm>
using namespace std;
#define MAX_N 30000
int arr[MAX_N+5];
int main()
{
	int w,n;
	cin>>w>>n;
	for(int i=1;i<=n;i++)
	cin>>arr[i];
	sort(arr+1,arr+n+1);
	int p1=1,p2=n;
	int boat=0;
	while(p1<=p2)
	{
		if(p1==p2)
		{
			boat++;
			break;
		}
		if(arr[p1]+arr[p2]<=w)
		{
			boat++;
			p1++;
			p2--;
		}
		else
		{
			boat++;
			p2--;
		}
	}
	cout<<boat;
	return 0;
 } 

3.最大子阵和

题目描述
​ 给定一个矩阵,在其中找一个子矩阵,使得子矩阵中所有元素的和加在一起最大。

输入
​ 第一行输入一个整数 N 表示矩阵的大小为 N∗N。

​ 接下来 N 行,每行 N 个数,表示矩阵中的元素的值 C。(−128≤C≤127)
输出
​ 输出一个整数,表示最大子阵和。

样例输入

4
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2

样例输出

15

贪心策略

局部:
s 代表以前一个位置为结尾的最大子序和,当前值为 a,则 s≥0,s += a 否则 s = a。
整体:
按照如上策略执行,过程中 s 的最大值,就是最大子序和。

代码实现

#include<iostream>
#include<cinttypes>
using namespace std;
#define MAXSIZE 100
int arr[MAXSIZE+5][MAXSIZE+5]={0};
int main()
{
	int n;
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		for(int j=1;j<=n;j++)
		{
			cin>>arr[i][j];
			arr[i][j]+=arr[i-1][j];//维护前缀和数组
		}
	}
	int ans=INT32_MIN;
	for(int i=1;i<=n;i++)//枚举高度 
	{
		for(int j=1;j<=n-i+1;j++)//枚举起始行号 
		{
			int S=0;
			int l=j+i-1;//结束行号 
			for(int k=1;k<=n;k++)//枚举列号 
			{
				if(S>=0)S+=(arr[l][k]-arr[j-1][k]);
				else S=arr[l][k]-arr[j-1][k];
				ans=max(S,ans);
			}
		}
	}
	cout<<ans;
	return 0;
}

4.最少操作次数

题目描述
​ 给出两个整数 a,b,每次操作可以把 a 变成 a+1 或 a∗k 。求把 a 变成 b 至少需要几步操作。

输入
​ 第一行三个数 a,b,k。(0≤a,b,k≤1018)
输出
​ 输出最少操作次数。

样例输入

2 10 2

样例输出

3

贪心策略

局部:
ans 代表最少操作次数,则 ans 更新策略如下:
情况1:若 ak ≤ b,ans+=1+b%k,b/=k
情况2:若 a
k>b,ans+=(b-a),终止

整体:
按照如上策略执行,最终得到的 ans,就是最少操作次数

证明
基础知识:进制的理解

在这里插入图片描述
在这里插入图片描述
最优操作顺序
在这里插入图片描述
在这里插入图片描述
将a变成b,等价于将b变成a的过程,若ak<=b,即b的长度大于a,此时删除b的最后一位(b%k步),b/k(1步)。
若a
k>b,此时a和b的长度相同,此时直接(b-a),执行完毕。

代码实现

#include<iostream>
using namespace std;
int main()
{
	long long a,b,k;
	long long ans=0;
	cin>>a>>b>>k;
	while(1)
	{
		if(k==1)
		{
			cout<<b-a;
			return 0;
		}
		if(k==0)
		{
			if(b==0)
			{
				cout<<1;
				return 0;
			}
			else 
			{
				cout<<b-a;
				return 0;
			}
		}
		if(a*k<b)
		{
			ans++;
			ans+=b%k;
			b/=k;
		}
		else
		{
			ans+=(b-a);
			break;
		}
	}
	cout<<ans;
	return 0;
}

5.安装雷达

题目描述
​ 地图 x 轴的上方为海,下方为陆地,海中有 n 个小岛,坐标分别为 (Xi,Yi)。有一种雷达,能探测到的范围为以 d 为半径的圆。问海岸线上至少造多少雷达可以把所有的小岛都处在探测范围内。注意雷达是建在海岸线上的,也就是x轴上的。

输入
​ 第一行输入两个数 n,d。(1≤n≤1000)
​ 接下来 n 行,每行两个数代表小岛的坐标。(−1000≤Xi,Yi≤1000)
输出
​ 输出一个数表示答案,无解时输出 −1。

样例输入

3 2
1 2
-3 1
2 1

样例输出

2

问题转换

原问题的场景比较复杂,可以对问题进行转换,每个小岛映射为一段区间。
在这里插入图片描述

贪心策略
局部:
按照区间 [l i , r i] 结束位置从小到大排序,雷达放置在区间末尾,pos 代表最后一个雷达的位置,若 pos < li , 雷达数量+1,pos = ri。
整体:
按照如上策略执行,最终得到的雷达数量,就是最少数量。

代码实现

#include<iostream>
#include<vector>
#include<cmath>
#include<algorithm>
using namespace std;
#define MAXSIZE 1000
struct Data{
	double l,r;
};
bool cmp(Data&a,Data&b)
{
	return a.r<b.r;
}
double SQ(double a,double b)
{
	return sqrt(a*a-b*b);
}
int main()
{
	int n;
	double d;
	cin>>n>>d;
	vector<Data>arr(n);
	double x,y;
	for(int i=0;i<n;i++)
	{
		cin>>x>>y;
		if(y>d)
		{
			cout<<-1;
			return 0;
		}
		arr[i].l=x-SQ(d,y);
		arr[i].r=x+SQ(d,y);
	}
	sort(arr.begin(),arr.end(),cmp);
	int cnt=0,k=0,flag=1;
	double pos=-2000;
	while(k<n)
	{
		while(pos>=arr[k].l)
		{
			k++;
			if(k==n)
			{
				flag=0;
				break;
			}	
		}
		if(flag==0)break;
		pos=arr[k].r;
		cnt++;
	}
	cout<<cnt;
	return 0;
 } 

6.挤奶

题目描述
​ 有 C 头奶牛需要挤奶,每头奶牛需要在规定的时间开始挤奶,并在规定的时间结束挤奶,每头奶牛挤奶时会占据一台机器。求 C 头奶牛在规定的时间挤奶至少需要多少台挤奶机。

输入
​ 第一行输入一个数 C。(1≤C≤50000)
​ 接下来 C 行,每行两个数表示每头奶牛开始挤奶的时间和结束挤奶的时间。(均小于 1,000,000)

输出
​ 第一行输出最少需要的机器数量。

​ 接下来 C 行,每行输出一个数,表示第 i 头奶牛使用的挤奶机编号。(奶牛优先使用编号小的机器)

样例输入

5
1 10
2 4
3 6
5 8
4 7

样例输出

4
1
2
3
2
4

贪心策略

局部:
按照挤奶开始时间,安排每一头奶牛,将当前奶牛安排给可以安排的编号最小的挤奶机,当无法安排时,增加挤奶机数量。
整体:
按照如上策略执行,得到的数量,就是最少挤奶机数量

代码实现

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
struct Data
{
	int l,r,id;
};
bool cmp(Data&a,Data&b)//sort是非稳定排序,加入对id的排序保证排序稳定 
{
	if(a.l!=b.l)return a.l<b.l;
	return a.id<b.id;
}
int main()
{
	int n;
	cin>>n;
	vector<Data>arr(n);
	int x,y;
	for(int i=0;i<n;i++)
	{
		scanf("%d%d",&arr[i].l,&arr[i].r);
		arr[i].id=i;
	}
	sort(arr.begin(),arr.end(),cmp);
	int cnt=0;
	vector<Data>item(n);//挤奶机 
	vector<int>ans(n); 
	for(int i=0;i<n;i++)
	{
		int flag=0;
		for(int j=0;j<cnt;j++)
		{
			if(item[j].r>=arr[i].l)continue;//当前挤奶机不能使用 
			flag=1;//使用了之前存在的挤奶机 
			item[j].r=arr[i].r;//更新结束使用的时间 
			ans[arr[i].id]=item[j].l;//对对应位置记录答案 
			break;
		}
		if(flag==1)continue;
		cnt++;//添加新的挤奶机 
		Data dd;
		dd.l=cnt;
		dd.r=arr[i].r;
		item[cnt-1]=dd;//新的挤奶机信息 
		ans[arr[i].id]=cnt;
	}
	cout<<cnt<<endl;
	for(int i=0;i<n;i++)
	cout<<ans[i]<<endl;
	return 0;
}

7.奶牛晒太阳

题目描述
​ 有 C 头奶牛去晒太阳,每头奶牛各自能够忍受的阳光强度有一个最小值和一个最大值,太大就晒伤了,太小奶牛没感觉。而刚开始的阳光的强度非常大,奶牛都承受不住,奶牛得涂抹防晒霜,防晒霜的作用是让阳光照在身上的阳光强度固定为某个值。那么为了不让奶牛烫伤,又不会没有效果。给出了 L 种防晒霜固定的阳光强度和数量,每头奶牛只能抹一瓶防晒霜,求能够享受晒太阳的奶牛最多有几头。

输入
​ 第一行输入两个数 C,L。(1≤C,L≤2500)
​ 接下来 C 行,每行两个数表示每头奶牛能接受的阳光强度的最小值和最大值。(均小于 1000)

​ 再接下来 L 行,每行两个数表示每种防晒霜固定的阳光强度和数量。(均小于 1000)

输出
​ 输出能晒太阳的奶牛的最多数量。

样例输入

3 2
3 10
2 5
1 5
6 2
4 1

样例输出

2

贪心策略
局部:
将所有点从小到大排序,所有线段按右端点从小到大排序,右端点相同,按照左端点从小到大排序。依次判断点与线段是否匹配。
整体:
按照如上策略执行,得到的就是最大匹配数量。

代码实现

#include<iostream>
#include<algorithm>
using namespace std;
#define MAXSIZE 2500
struct Data{
	int l,r;
};
bool cmp1(Data&a,Data&b){
	if(a.r!=b.r)return a.r<b.r;
	return a.l<b.l;
}
bool cmp2(Data&a,Data&b){
	if(a.l!=b.l)return a.l<b.l;
	return a.r<b.r;
}
int main()
{
	int C,L;
	cin>>C>>L;
	Data cow[MAXSIZE+5],item[MAXSIZE+5];
	for(int i=1;i<=C;i++)
	cin>>cow[i].l>>cow[i].r;
	for(int i=1;i<=L;i++)
	cin>>item[i].l>>item[i].r; 
	sort(cow+1,cow+C+1,cmp1);
	sort(item+1,item+L+1,cmp2);
	int ans=0;
	for(int i=1;i<=C;i++)
	{
		for(int j=1;j<=L;j++)
		{
			if(item[j].r>0&&item[j].l<=cow[i].r&&item[j].l>=cow[i].l)
			{
				item[j].r--;
				ans++;
				break;
			}
		}
	}
	cout<<ans;
	return 0;
 } 

8.公司的任务

题目描述
​ 公司有 M 个任务需要完成,每个任务有一个难度系数 Yi 并且需要一定的时间 Xi 完成。现在有 N 台机器,每台机器有最大工作时间和最大工作难度,只有当机器的最大工作时间和最大工作难度大于等于任务的时间和任务的难度时,机器才能完成这个任务。每天机器每天只能完成一个任务,一个任务只能被一台机器完成。当完成一个任务时,公司能获得 500∗Xi+2∗Yi 的报酬。求今天公司最多能获得的报酬。

输入
​ 第一行输入两个整数 N,M。

​ 接下来 N 行,每行两个数,表示机器的最大工作时间和最大工作难度。

​ 接下来 M 行,每行两个数,表示任务需要的时间和任务的难度。

输出
​ 输出一行两个数,第一个数为能完成的最大任务数,第二个数为今天能获取的最高报酬。

样例输入

1 2
100 3
100 2
100 1

样例输出

1 50004

数据规模与约定
​ 时间限制:1 s

​ 内存限制:256 M

​ 100% 的数据保证 1≤N,M≤100000,0<Xi<1440,0≤Yi≤100

贪心策略

局部:
将所有任务以及机器按照时间从大到小排序,时间相同的按照任务难度从大到小排序,维护能处理当前任务的所有机器,选择其中
难度系数最小的。
整体:
按照如上策略执行,得到的就是最大报酬。
理解:为什么找到了能够处理当前任务的机器就可以去处理,而不是说省下来给后面的用?因为一个机器只能处理一个任务,就算省下也就多处理一个,所以尽量在前面使用是效益最高的。

代码实现

#include<iostream>
#include<algorithm>
using namespace std;
#define MAXSIZE 100000
struct Data{
	int l,r;
};
int vis[MAXSIZE+5]={0};
bool cmp(const Data&a,const Data&b)
{
	if(a.l!=b.l)return a.l>b.l;
	return a.r>b.r;
}
int main()
{
	int N,M;
	cin>>N>>M;
	Data machine[MAXSIZE+5];
	Data task[MAXSIZE+5];
	for(int i=1;i<=N;i++)
	scanf("%d%d",&machine[i].l,&machine[i].r);
	for(int i=1;i<=M;i++)
	scanf("%d%d",&task[i].l,&task[i].r);
	sort(machine+1,machine+N+1,cmp);
	sort(task+1,task+M+1,cmp);
	long long task_cnt=0,ans=0;
	int cnt[101]={0};
	/*注意下面的处理,能够在时间上处理第i个task的machine一定能够
	在时间上处理第i+1个task,所以只需要关注难度就可*/ 
	for(int i=1,j=1;i<=M;i++)//前j个为在时间上符合第i个task的machine 
	{
		while(j<=N&&machine[j].l>=task[i].l)//均摊时间复杂度为O(1) 
		{
			cnt[machine[j].r]++;//记录能够处理某个难度的machine的数量 
			j++;
		}
		for(int k=task[i].r;k<=100;k++)//最大只有一百次 
		{
			if(cnt[k]==0)continue;//当前机器能够处理的难度不符合 
			cnt[k]-=1;
			task_cnt++;
			ans+=(500*task[i].l+2*task[i].r);
			break;
		}
	}
	cout<<task_cnt<<" "<<ans;
	return 0;
}

9.树的颜色

题目描述
​ 有一棵树,它的所有节点都需要染色,每个节点都有一个代价基础值 Ci。第一个染色的是根节点,其余节点染色的时候其父节点必须已染色。染色一个节点会用掉一个时间单位,每个节点染色的代价是染完此节点时的总时间 T 乘上这个节点的基础值 Ci。求染完所有节点所需的最小代价。

输入
​ 第一行包含两个整数 N,R 其中,N 是树中节点个数,R 是根节点编号。

​ 第二行输入 N 个整数,编号为 i 的节点的代价基础值 Ci。(1≤Ci≤500)
​ 接下来 N−1 行为边的信息,每行两个数分别表示父节点编号和子节点编号。

输出
​ 输出一个整数,表示最小代价。

样例输入

5 1
1 2 1 2 4
1 2
1 3
2 4
3 5

样例输出

33

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
代码实现

#include <iostream>
using namespace std;

#define MAX_N 1000

int C[MAX_N + 5] = {0};
int fa[MAX_N + 5] = {0};
int vis[MAX_N + 5] = {0};
int cnt[MAX_N + 5] = {0};
double w[MAX_N + 5] = {0};
int n, r;
long long ans = 0;

int find_x() {
    int x = -1;
    for (int i = 1; i <= n; i++) {
        if (i == r || vis[i] == 1) continue;
        if (x == -1 || w[x] < w[i]) x = i;
    }
    return x;
}

int find_father(int x) {
    if (vis[fa[x]]) return find_father(fa[x]);
    return fa[x];
}

int main() {
    cin >> n >> r;
    for (int i = 1; i <= n; i++) {
        cin >> C[i];
        ans += C[i];
        fa[i] = i;
        w[i] = C[i];
        cnt[i] = 1;
    }
    for (int i = 1, a, b; i < n; i++) {
        cin >> a >> b;
        fa[b] = a;
    }
    for (int i = 1; i < n; i++) {
        int x = find_x();
        int father_x = find_father(x);
        ans += cnt[father_x] * C[x];
        C[father_x] += C[x];
        cnt[father_x] += cnt[x];
        w[father_x] = 1.0 * C[father_x] / cnt[father_x];
        vis[x] = 1;                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
    }
    cout << ans << endl;
    return 0;
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值