【算法】贪心算法

概念&&介绍

贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解。所以说只有证明局部最优解在全局最优解序列中,才能通过贪心算法得到问题的全局最优解。也就是说选择的贪心策略必须具备无后效性,即某个状态以后的过程不会影响以前的状态,只与当前状态有关。所以对所采用的贪心策略一定要仔细分析其是否满足无后效性。

如图0.0所示,我们从 s s s点出发到达点 e e e,如果我们想要在经过的路径中累计数值最大,我们可以采取在每个分支处选择数值最大的点的贪心策略。但显然我们会选择 99 → 1 → 1 99\rightarrow1\rightarrow1 9911的路径,但实际上正解是路径 1 → 99 → 99 1\rightarrow99\rightarrow99 19999,这是由于我们的选择是具有后效性的,当前选择的点会影响到后面可选择的点,所以会导致我们不能得到全局最优解。

图0.0

如图0.1所示,我们选择三次,每次从两点中选择一个点并累加它的数值,如果我们想要使累计的数值最大,我们可以选择三次点 99 99 99,并且我们最后得出的解是正解。这是由于我们的选择不具有后效性,当前选择的点并不会影响到后面可选择的点,所以我们可以得到全局最优解。

图0.1

算法流程

  1. 把求解的问题分成若干个子问题。
  2. 对每一子问题求解,得到子问题的局部最优解。
  3. 把子问题的解局部最优解合成原来解问题的一个解。

适用问题

局部最优策略能导致产生全局最优解。或者将贪心算法进行修改以求出全局最优解

深入理解&&例题

如果前面的两个小例子不能使你透彻地理解贪心算法,那么我们可以来看看下面的几个小例题,通过它们来深入理解贪心算法以及贪心算法使用的场景。

删数问题

读入一个高精度整数 n n n,去掉其中 s s s个数字后剩下的数字按照原来的次序组成一个新的正整数。寻求一种方案,使得最后组成的数最小。

分析题目,我们可以设计一种贪心策略:进行 s s s轮删数,每轮删除最大的一个数,如果数值相同则删除靠前的一个。

我们可以对这个贪心策略进行证明:每轮删除的数字并不会影响后面可删除的数字,因此这种贪心策略不具有后效性,且 s s s次删数后剩余所有数字组成的数即为问题的可行解,因此这种贪心策略可行。

#include<bits/stdc++.h>
using namespace std;
int s,len;
char n[241];
int main(){
	cin>>n;
	cin>>s;
	len=strlen(n);
	while(s--){
		int p,q;
		p=0,q=n[0];
		for(int i=1;i<len;i++){
			if(n[i]>q){
				q=n[i];
				p=i;
			}
		}//找到目标数
		for(int i=p+1;i<len;i++){
			n[i-1]=n[i];
		}
		len--;//删数
	}
	for(int i=0;i<len;i++){
		cout<<n[i];
	}
	return 0;
}

均分纸牌

如图3.0所示,有 n n n堆纸牌,编号分别为 1 , 2 , 3 , . . . , n 1,2,3,...,n 1,2,3,...,n。每堆上有若干张,但纸牌总数必为 n n n的倍数。可以在任一堆上取若于张纸牌,然后移动。移牌规则:编号为 1 1 1堆上取的纸牌,只能移到编号为 2 2 2的堆上;编号为 n n n的堆上取的纸牌,只能移到编号为 n − 1 n-1 n1的堆上;其他堆上取的纸牌,可以移到相邻左边或右边的堆上。现在要求找出一种移动方法,用最少的移动次数使每堆上纸牌数都一样多。

分析题目,我们可以得知:要想使移动次数最少,就要把移动的浪费降到最低。例如在相邻的两堆间移动 ≥ 2 \ge2 2次就是一种浪费,因为这 ≥ 2 \ge2 2次移动都可以合并成 ≤ 1 \le1 1次。

图3.0

因此,我们可以采用移动一次使得一堆牌数达到平均值的贪心策略:先把每堆的牌数减去平均数,然后由左而右的顺序移动纸牌。若第 i i i堆纸牌的张数 a [ i ] a[i] a[i]不为 0 0 0,则将值移动到下一堆。如图3.1所示,所有堆的纸牌平均数为 6 6 6,我们可以先将每堆减掉平均数。

图3.1

然后遍历每堆纸牌,如图3.2所示,纸牌数不为 0 0 0则向下一堆移动,每次移动将移动次数 + 1 +1 +1,最后得到图3.3。

图3.2

这种贪心策略类似于“把不足平均值的责任推给下一堆,直至多于平均值的纸牌堆来弥补”。通过求解两个纸牌堆之间的最优子解,把所有子解合并得到问题的一个可行解。

#include<bits/stdc++.h>
using namespace std;
int n,a[101],sum,ans;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		sum+=a[i];
	} 
	sum/=n;
	for(int i=1;i<=n;i++){
		a[i]-=sum;
	} 
	for(int i=1;i<=n;i++){
		if(a[i]!=0){
			a[i+1]+=a[i];
			ans++;
		}
	}
	printf("%d\n",ans);
	return 0;
}

糖果传递

n n n个小朋友坐成一圈,每人有 a [ i ] a[i] a[i]个糖果。每人只能给左右两人传递糖果。每人每次传递一个糖果代价为 1 1 1。求使所有人获得均等糖果的最小代价。

和均分纸牌类似,现在假设编号为 i i i的人初始有 a [ i ] a[i] a[i]个糖果。对于 1 1 1号来说,他给了 n n n x [ 1 ] x[1] x[1]个糖果,还剩 a [ 1 ] − x [ 1 ] a[1]-x[1] a[1]x[1]个;但是因为 2 2 2号给了他 x [ 2 ] x[2] x[2]个糖果,所以最后还剩 a [ 1 ] − x [ 1 ] + x [ 2 ] a[1]-x[1]+x[2] a[1]x[1]+x[2]个糖果。根据题设,该金币数等于 m m m。换句话说,我们得到了一个方程: m = a [ 1 ] − x [ 1 ] + x [ 2 ] m=a[1]-x[1]+x[2] m=a[1]x[1]+x[2]

同理,对于第 2 2 2个人,有 m = a [ 2 ] − x [ 2 ] + x [ 3 ] m=a[2]-x[2]+x[3] m=a[2]x[2]+x[3]。最终,我们可以得到 n n n个方程,一共 n n n个变量,是不是可以直接解方程组了呢?很可惜,还不行。因为从前 n − 1 n-1 n1个方程可以推导出最后一个方程。所以,实际上只有 n − 1 n-1 n1个方程是有用的。

尽管无法直接解出答案,我们还是可以尝试着用 x [ 1 ] x[1] x[1]表示出其他的 x [ i ] x[i] x[i],则本题就变成了单变量的极值问题。

对于第 1 1 1个人, a [ 1 ] − x [ 1 ] + x [ 2 ] = m → x [ 2 ] = m − a [ 1 ] + x [ 1 ] = x [ 1 ] − c [ 1 ] a[1]-x[1]+x[2]=m\rightarrow x[2]=m-a[1]+x[1]=x[1]-c[1] a[1]x[1]+x[2]=mx[2]=ma[1]+x[1]=x[1]c[1](令 c [ 1 ] = a [ 1 ] − m c[1]=a[1]-m c[1]=a[1]m,下面类似)
对于第 2 2 2个人, a [ 2 ] − x [ 2 ] + x [ 3 ] = m → x [ 3 ] = m − a [ 2 ] + x [ 2 ] = 2 m − a [ 1 ] − a [ 2 ] + x [ 1 ] = x [ 1 ] − c [ 2 ] a[2]-x[2]+x[3]=m\rightarrow x[3]=m-a[2]+x[2]=2m-a[1]-a[2]+x[1]=x[1]-c[2] a[2]x[2]+x[3]=mx[3]=ma[2]+x[2]=2ma[1]a[2]+x[1]=x[1]c[2] c [ 2 ] = c [ 1 ] + a [ 2 ] − m c[2]=c[1]+a[2]-m c[2]=c[1]+a[2]m
对于第 3 3 3个人, a [ 3 ] − x [ 3 ] + x [ 4 ] = m → x [ 4 ] = m − a [ 3 ] + x [ 3 ] = 3 m − a [ 1 ] − a [ 2 ] − a [ 3 ] + x [ 1 ] = x [ 1 ] + c [ 3 ] a[3]-x[3]+x[4]=m\rightarrow x[4]=m-a[3]+x[3]=3m-a[1]-a[2]-a[3]+x[1]=x[1]+c[3] a[3]x[3]+x[4]=mx[4]=ma[3]+x[3]=3ma[1]a[2]a[3]+x[1]=x[1]+c[3]

对于第 1 1 1个人, a [ n ] − x [ n ] + x [ 1 ] = m a[n]-x[n]+x[1]=m a[n]x[n]+x[1]=m。这是一个多余的等式,并不能给我们更多的信息。我们希望所有的 x [ i ] x[i] x[i]的绝对值之和尽量小,即 ∣ x [ 1 ] ∣ + ∣ x [ 1 ] − c [ 1 ] ∣ + ∣ x [ 1 ] − c [ 2 ] ∣ + . . . + ∣ x [ 1 ] − c [ n ] − 1 ∣ |x[1]|+|x[1]-c[1]|+|x[1]-c[2]|+...+|x[1]-c[n]-1| x[1]+x[1]c[1]+x[1]c[2]+...+x[1]c[n]1要最小。注意到 ∣ x [ i ] − c [ i ] ∣ |x[i]-c[i]| x[i]c[i]的集合意思是数轴上点 x [ i ] x[i] x[i] c [ i ] c[i] c[i]的距离,所以问题变成了:给定数轴上的 n n n个点,找出一个到它们的距离之和尽量小的点。

结论:给定数轴上的 n n n个点,在数轴上的所有点中,中位数离所有顶点的距离之和最小。凡是能转化为这个模型的题目都可以用中位数求解。

#include<bits/stdc++.h>
#define maxn 1000001
using namespace std;
int n,a[maxn];
long long c[maxn],sum,ans;
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]),sum+=a[i];
	} 
	sum/=n;
	for(int i=1;i<=n;i++){
		c[i]+=sum-a[i]+c[i-1];
	} 
	sort(c+1,c+n+1);
	int mid=c[n/2];
	for(int i=1;i<=n;i++){
		ans+=abs(mid-c[i]);
	} 
	printf("%lld",ans);
	return 0;
}

进阶习题

参考资料

  1. 贪心算法 百度百科
  2. 贪心策略取得最优解的条件_常用算法之贪心算法 CSDN @weixin_39799825
  3. 贪心算法的最优解条件 CSDN @逆羽飘扬
  4. 贪心算法-例题讲解 博客园 @In’f

原文地址 https://oiermikec.fun/index.php/2021/01/09/38.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Prim算法是一种求加权无向连通图的最小生成树的算法。其基本思想是从一个点开始,每次选择一个与当前生成树相邻且权值最小的边,直到生成一棵包含所有节点的树为止。具体步骤如下: 1. 任选一个起始点,将其加入生成树中。 2. 找到与生成树相邻的所有边中权值最小的那条边,将其加入生成树中。 3. 重复第二步,直到生成一棵包含所有节点的树为止。 下面是一个使用Prim算法最小生成树的Python代码示例: ```python def prim(graph): # 初始化 nodes = list(graph.keys()) visited = [nodes[0]] edges = [] # 循环直到所有节点都被访问 while len(visited) < len(nodes): min_edge = None # 找到与已访问节点相邻的所有边中权值最小的那条边 for node in visited: for neighbor, weight in graph[node].items(): if neighbor not in visited: if min_edge is None or weight < min_edge[2]: min_edge = (node, neighbor, weight) # 将该边加入生成树中 edges.append(min_edge) visited.append(min_edge[1]) return edges ``` 贪心算法是一种在求最优解问题时采用贪心策略的算法。其基本思想是每次选择当前看来最优的决方案,直到达到全局最优解贪心算法通常适用于满足最优子结构性质的问题,即问题的最优解可以通过子问题的最优解推导得到。具体步骤如下: 1. 将问题分为若干个子问题。 2. 对每个子问题求,得到子问题的最优解。 3. 将所有子问题的最优解合并成原问题的。 下面是一个使用贪心算法背包问题的Python代码示例: ```python def knapsack(items, capacity): # 按照单位价值排序 items = sorted(items, key=lambda x: x[1]/x[0], reverse=True) # 初始化 total_value = 0 knapsack = [] # 循环直到背包装满 for item in items: if capacity >= item[0]: knapsack.append(item) capacity -= item[0] total_value += item[1] else: knapsack.append((capacity, item[1]/item[0]*capacity)) total_value += item[1]/item[0]*capacity break return knapsack, total_value ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值