贪心算法

贪心算法简要介绍:https://oi-wiki.org/basic/greedy/

1.贪心算法基本原理

1)描述

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

(正确的废话,但是没用)

        贪心算法的关键:贪心策略的选择。

2)适用情况

        贪心算法在有最优子结构的问题中尤为有效。最优子结构的意思是问题能够分解成子问题来解决,子问题的最优解能递推到最终问题的最优解。

(1)适用情况

        假设只能拿一张,肯定是拿最大的,所以可以把问题拆解成那三次一张的纸币获得的最大值

(2)不适用情况

        你有一个承重为8的背包,在不超过背包承重上限的情况下,你能获得的物品最大总价值是多少?(01背包问题)

        贪心算法:考虑局部最优,先选择价值最大的,即选择物品一,此时背包最多可以装重量为2的物品,所以结果是9,显然不对。

通过以上两个例子不能说贪心算法只能解决比较的简单问题

由于贪心算法的算法框架中没有套路,因此在设计贪心算法时可用的思维抓手比较少,导致设计贪心算法时,依靠灵感。(比较需要天赋)

3)贪心算法证明

     

4)贪心与偏序关系

(1)偏序关系

符合贪心算法思想。

(2)形式定义

传递性,通常情况下,在贪心算法设计中解释了为什么贪心算法局部最优,最终保证局部最优。

(3)联系

        在贪心算法设计中,主要寻找偏序关系,最后加以证明,那么贪心算法就设计出来了。        

        eg:HZOJ-256国王游戏---https://oj.haizeix.com/problem/256

 

假设排为以下顺序:(调换i与i+1位置的大臣)

如果第二个序列优于第一个序列,则乘积越大的越往前排,它不会使得我所得到的方案变差。(偏序关系在贪心算法中的体现)

5)学习心法

        在一次又一次的贪心策略证明中掌握:贪心策略的选择。

2.练习题

1)HZOJ-505最大整数

--https://oj.haizeix.com/problem/505

        局部:A+B>B+A,则A排在B前面

        整体:按照如上策略得到的排序以后的序列就是最大整数。

        代码如下所示:

#include<bits/stdc++.h>
using namespace std;
bool cmp(const string &a,const string &b){
	return a+b>b+a;
}
int main(){
    int n;
    vector<string> arr;//字符串数组
    string s;
    cin>>n;
    for(int i=0;i<n;i++){
		cin>>s;
		arr.push_back(s);
	}
	sort(arr.begin(),arr.end(),cmp);
	for(int i=0;i<n;i++){
		cout<<arr[i];
	}
	cout<<endl;
    return 0;
}

贪心策略证明(之后的题不再做证明):

        证明1:A+B>B+A(+字符串的连接),为什么?

         情况1:A>B(基于字典序--按位进行比较)

                        eg:123,96        数字中123>96   字典序:96>123,按位比较9>1,因此96>123

                  不用证明,这是显而易见的。

          情况2:A<B且A不是B的前缀

                这种情况是不可能的。

        情况3:A是B的前缀

                         那么黄色部分全相等。

        如果A+B>B+A,则红色部分的字符<绿色部分的字符。

证明2:当A+B>B+A,为什么A排到B前面,能保证全局最优。

        假设:排序以后为:A、C、B

        反证法:假设,最优策略不是A、C、B,而是B、C、A;

 

两种字符串:

因为排序以后为:A、C、B所以红色部分字典序小于绿色部分,因此BCA一定小于ACB,所以当A排在B前面总是优于B排在A前面。

2)HZOJ-504-删数

--https://oj.haizeix.com/problem/504 

        局部:每次删除一个离最高位最近的逆序位的第一个数字1234321,其中43是离最高位最近的逆序位,删除4即可。

        整体:按照如上策略执行n次以后,得到的就是最小的整数。

代码如下所示:

#include<bits/stdc++.h>
using namespace std;
bool cmp(const char &a,const char &b){
	return a<b;
}
int main(){
    int n;
    char s[505];
    cin>>s>>n;
    for(int i=0;i<n;i++){
		int j=0;
		while(s[j+1]!='\0'&&s[j]<=s[j+1])j++;
		while(s[j])s[j]=s[j+1],j++;
	}
	for(int i=0,flag=1;s[i];i++){
		if(s[i]=='0'&&flag)continue;
		cout<<s[i];
		flag=0;
	}
	cout<<endl;
    return 0;
}

3)HZOJ-503-独木舟

--https://oj.haizeix.com/problem/503 

        局部: 每次安排,如果最重的人和最轻的人能坐一起,就坐一条独木舟,否则最重的人自己坐一条船。

        整体:按照如上策略执行,就能得到最少的独木舟。

代码如下所示:

#include<bits/stdc++.h>
using namespace std;
const int N=30010;
bool cmp(const char &a,const char &b){
	return a<b;
}
int a[N];
int main(){
    int w,n;
    cin>>w>>n;
    for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	sort(a+1,a+n+1);
	int ans=0;
	for(int i=1,j=n;i<=j;){
		if(i!=j){
			if(a[i]+a[j]<=w)ans++,i++,j--;
			else ans++,j--;
		}else{
			ans++;
			break;
		}
	}
	cout<<ans<<endl;
    return 0;
}

4)HZOJ-258-最大子阵和

--https://oj.haizeix.com/problem/258

        局部:s代表以前一个位置为结尾的最大子序和,当前值为 a则s>=0,s+=a否则s=a

        整体:按照如上策略执行,过程中s的最大值,就是最大子序和。

代码如下所示:

#include<bits/stdc++.h>
using namespace std;
const int N=110;
bool cmp(const char &a,const char &b){
	return a<b;
}
int a[N][N];
int main(){
    int n;
    cin>>n;
    for(int i=1;i<=n;i++){
		for(int j=1;j<=n;j++){
			cin>>a[i][j];
			a[i][j]+=a[i-1][j];//从1~i行第j列的和值
		}
	}
	int ans=0;
	for(int i=1;i<=n;i++){//起始行号
		for(int j=i;j<=n;j++){//终止行号
			int s=0;
			for(int k=1;k<=n;k++){
				int x=a[j][k]-a[i-1][k];//从i~j行第k列的和值
				if(s>=0)s+=x;
				else s=x;
				if(s>ans)ans=s;
			}
		}
	}
	cout<<ans<<endl;
    return 0;
}

5)HZOJ-511-最少操作次数

https://oj.haizeix.com/problem/511

        局部:ans代表最少操作次数,则ans更新策略如下:

                情况1:若 a * k <= b ,ans += 1 + b % k , b /= k; 

                情况2:若 a * k > b , ans += (b - a) , 终止;

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

        补充(进制):将 a 和 b 看成一个 k 进制的数,a * k 就是将a的 k 进制形式向左平移一位,末尾加个 0。

        结论1:使用 * k 优于 + 1 操作(eg : a=3,b=10,k=3------a*k+1=10只需要两次,a+1*7=10,需要7次)

        结论2:当 a 为 b 的数字前缀时,再使用 * k 操作。

               eg : b ( k 进制形式 ):1 4 2 3

                      a ( k 进制形式 ):      1 2

                a -> b : +1 +1      * k +1 +1      * k +1 +1 +1

              对应:      [ b - a]    [ 1 + b % k]    [ 1 + b % k ]

代码如下所示:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

void solve(){
    ll a,b,k;
    cin>>a>>b>>k;
    ll ans=0;
    if(k==0){
		if(b==0)cout<<(ll)min(a,(ll)1)<<endl;
		else cout<<(b-a)<<endl;
		return ;
	}
	if(k==1){
		cout<<(b-a)<<endl;
		return ;
	}
    while(1){
		if(a*k<=b)ans+=1+b%k,b/=k;
		else {
			ans+=(b-a);
			cout<<ans<<endl;
			return ;
		}
	}
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    int t;
    t=1;
    //cin>>t;
    while(t--){
        solve();
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值