当前位置:   article > 正文

【趣学算法】贪心算法_没有后悔药。一旦做出选择,不可以反悔。 选择什么样的贪心策略,直接决定算法的好

没有后悔药。一旦做出选择,不可以反悔。 选择什么样的贪心策略,直接决定算法的好

14天阅读挑战赛
努力是为了不平庸~
算法学习有些时候是枯燥的,这一次,让我们先人一步,趣学算法!欢迎记录下你的那些努力时刻(算法学习知识点/算法题解/遇到的算法bug/等等),在分享的同时加深对于算法的理解,同时吸收他人的奇思妙想,一起见证技术er的成长~

一、贪心算法

贪心算法总是做出当前最好的选择,期望通过局部最优选择从而得到全局最优的解决方案。
在贪心算法中,注意以下几个问题:
(1)没有后悔药。一旦做出选择,不可以反悔。
(2)有可能得到的不是最优解,而是最优解的近似解。
(3)选择什么样的贪心策略,直接决定算法的好坏。
那么,贪心算法需要遵循什么原则呢?

1.贪心算法求解的问题往往具有两个重要的特性:贪心选择性质和最优子结构性质。

(1)贪心选择
贪心选择性质是指原问题的整体最优解可以通过一系列局部最优的选择得到。将原问题变为一个相似的但规模更小的子问题,而后的每一步都是当前最佳的选择。这种选择依赖于已做出的选择,但不依赖于未做出的选择。
(2)最优子结构
如果问题的最优解包含其子问题的最优解,称此问题具有最优子结构性质。问题的最优子结构性质是该问题是否可用贪心算法求解的关键。例如原问题 S = a 1 , a 2 , … , a i , … , a n S={a_1,a_2,…,a_i,…,a_n} S=a1a2aian,通过贪心选择选出一个当前最优解 a i {a_i} ai之后,转化为求解子问题 S S S−{ a i a_i ai},如果原问题的最优解包含子问题的最优解,则该问题满足最优子结构性质。
在这里插入图片描述

2.具有贪心选择和最优子结构性质就可以使用贪心算法,如何使用呢?

(1)贪心策略
首先确定贪心策略,选择当前最好的一个方案。例如,挑选苹果,如果要求个大的是最好的,那每次都从苹果堆中拿一个最大的,作为局部最优解,贪心策略就是选择当前最大的苹果;如果要求最红的苹果是最好的,那每次都从苹果堆中拿一个最红的,贪心策略就是选择当前最红的苹果。因此根据求解目标不同,贪心策略也会不同。
(2)局部最优解
根据贪心策略,一步一步地得到局部最优解。例如,第一次选一个最大的苹果放起来,记为 a 1 a_1 a1,第二次再从剩下的苹果堆中选择一个最大的苹果放起来,记为 a 2 a_2 a2,以此类推。
(3)全局最优解
把所有的局部最优解合成为原来问题的一个最优解 ( a 1 , a 2 , … ) (a_1,a_2,…) a1a2

二、贪心算法实例——最优装载问题

1.问题描述:

在北美洲东南部,有一片神秘的海域,那里碧海蓝天、阳光明媚,这正是传说中海盗最活跃的加勒比海(Caribbean Sea)。有一天,海盗们截获了一艘装满各种各样古董的货船,每一件古董都价值连城,一旦打碎就失去了它的价值。虽然海盗船足够大,但载重量为 c c c,每件古董的重量为 w i w_i wi,海盗们该如何把尽可能多数量的宝贝装上海盗船呢?

2.问题分析:

贪心策略:本题要求物品不可分割,要求装载的物品的数量尽可能多,而船的容量是固定的,那么优先把重量小的物品放进去,在容量固定的情况下,装的物品最多。
贪心选择策略:重量最小者先装
从局部最优达到全局最优,从而产生最优装载问题的最优解。

古董重量清单表

在这里插入图片描述

3.算法设计:

(1)当载重量为定值 c c c时, w i w_i wi越小时,可装载的古董数量 n n n越大。只要依次选择最小重量古董,直到不能再装为止。
(2)把 n n n个古董的重量从小到大(非递减)排序,然后根据贪心策略尽可能多地选出前 i i i个古董,直到不能继续装为止,此时达到最优。

4.程序代码:
//最优装载问题 
#include<iostream>
#include<algorithm> 
const int N=10005;
using namespace std;
double w[N]; //古董的重量数组

int solve1(int n,double W){
	double tmp=0.0;//tmp为已装载到船上的古董重量
    int ans=0; //ans为已装载的古董个数,初始化为0
    for(int i=0;i<n;i++){
        tmp+=w[i];
        if(tmp<=W)
            ans++;
        else
            break;
    }
    return ans;
} 

int solve2(int n,double W){//优化算法 
	double tmp=0.0;//tmp为已装载到船上的古董重量
	int ans=n; //ans为已装载的古董个数,初始化为n
    for(int i=0;i<n;i++){
        tmp+=w[i];
        if(tmp>=W){//最后一次才满足条件 
        	if(tmp==W)
        		ans=i+1;
        	else
        		ans=i;
        	break;
		}     
    }
    return ans;
}

int main(){
    int t,n;//t为测试用例个数,n为古董数量
    double W;//重量约束 
	cin>>t;
    while(t--){
        cin>>W>>n;
        for(int i=0;i<n;i++)//输入每个物品重量
            cin>>w[i]; 
        sort(w,w+n); //按古董重量升序排序
        cout<<solve1(n,W)<<endl;
    }
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
5.算法复杂度分析:

(1)时间复杂度:首先需要按古董重量排序,调用sort函数,其平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),输入和贪心策略求解的两个for语句时间复杂度均为 O ( n ) O(n) O(n),因此时间复杂度为 O ( n + n l o g n ) O(n + nlogn) O(n+nlogn)
(2)空间复杂度:程序中变量tmp、ans等占用了一些辅助空间,这些辅助空间都是常数阶的,因此空间复杂度为 O ( 1 ) O(1) O(1)

三、贪心算法实例——会议安排问题

1.问题描述:

某跨国公司总裁为一大堆会议时间表焦头烂额,希望秘书能做出合理的安排,能在有限的时间内召开更多的会议。会议安排的目的是能在有限的时间内召开更多的会议(任何两个会议不能同时进行)。在会议安排中,每个会议 i i i都有起始时间 b i b_i bi和结束时间 e i e_i ei,且 b i < e i b_i<e_i bi<ei,即一个会议进行的时间为半开区间 [ b i , e i ) [b_i, e_i) [bi,ei)。如果 [ b i , e i ) [b_i, e_i) [bi,ei) [ b j , e j ) [b_j, e_j) [bj,ej)均在“有限的时间内”,且不相交,则称会议 i i i与会议 j j j相容的。

2.问题分析:

会议安排问题要求在所给的会议集合中选出最大的相容活动子集,即尽可能在有限的时间内召开更多的会议。

会议时间表

在这里插入图片描述
在这里插入图片描述
{1,4,6,8,9},{2,4,7,8,9}都是能安排最多的会议集合。

会议数最多,需要选择最多的不相交时间段。可以尝试以下贪心策略:
(1)最早开始时间且与已安排的会议相容的会议。
(2)持续时间最短且与已安排的会议相容的会议。
(3)最早结束时间且与已安排的会议相容会议的。

最好选择那些开始时间要早,而且持续时间短的会议,即最早开始时间+持续时间最短,就是最早结束时间。
贪心策略:每次从剩下的会议中选择具有最早结束时间且与已安排的会议相容的会议安排。

3.算法设计:

(1)初始化:将 n n n个会议的开始时间、结束时间存放在结构体数组中然后按结束时间非递减排序,结束时间相等时,按开始时间非递增;
(2)根据贪心策略选择第一个具有最早结束时间的会议,用 l a s t last last记录刚选中会议的结束时间;
(3)选择第一个会议之后,依次从剩下未安排的会议中选择,如果会议 i i i开始时间大于等于 l a s t last last,那么会议 i i i可以安排,更新 l a s t last last为刚选中会议的结束时间;否则,舍弃会议 i i i,检查下一个会议是否可以安排。

原始会议时间表

在这里插入图片描述

排序后的会议时间表

在这里插入图片描述

4.程序代码:
//会议安排问题
#include<iostream>
#include<algorithm>
using namespace std;
struct Meet{
    int beg;   //会议的开始时间
    int end;   //会议的结束时间
    int num;   //记录会议的编号
}meet[10005];   //会议的最大个数为10000

bool cmp(Meet x,Meet y){//自定义排序优先级 
    if(x.end==y.end)
        return x.beg>y.beg;
    return x.end<y.end;
}

void init(int n){//读入数据
    int s,e;
    for(int i=0;i<n;i++){
        cin>>s>>e;
        meet[i].beg=s;
        meet[i].end=e;
        meet[i].num=i+1;
    }
}


int solve(int n){
	sort(meet,meet+n,cmp); //对会议按结束时间排序
    int ans=1;
    int last=meet[0].end;  //记录刚刚被选中会议的结束时间
    for(int i=1;i<n;i++){
        if(meet[i].beg>=last){ //如果会议i开始时间大于等于刚选中的会议的结束时间
        	ans++;
        	last=meet[i].end;
        }
    }
    return ans;
}

int main(){
    int t,n;//t为测试用例数,n为会议数
    cin>>t;
    while(t--){
    	cin>>n;
		init(n);
		cout<<solve(n)<<endl;
    }
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
5.算法复杂度分析:

(1)时间复杂度:输入 n n n个会议,共执行 n n n次。sort排序函数的平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。选择会议,贡献最大的为 i f ( m e e t [ i ] . b e g > = l a s t ) if(meet[i].beg>=last) if(meet[i].beg>=last)语句,时间复杂度为 O ( n ) O(n) O(n),总时间复杂度为 O ( n + n l o g n ) = O ( n l o g n ) O(n+nlogn)= O(nlogn) O(n+nlogn)=O(nlogn)
(2)空间复杂度:辅助空间有 i 、 n 、 a n s i、n、ans inans等变量,则该程序空间复杂度为常数阶,即 O ( 1 ) O(1) O(1)

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号