算法基础8 —— 详解贪心算法(部分背包问题 + 区间调度问题)

黑盒测试

黑盒测试法把程序看作一个黑盒子,完全不考虑程序的内部结构和处理过程。也就是说,黑盒测试是在程序接口进行的测试,它只检查程序功能是否能按照规格说明书的规定正常使用,程序是否能适当地接收输人数据并产生正确的输出信息,程序运行过程中能否保持外部信息的完整性。黑盒测试又称为功能测试
—— 张海藩《软件工程导论》
算法基础8 —— 详解贪心算法(部分背包问题 + 区间调度问题)_第1张图片

注:海南大学软件工程专业课考点。此外还有白盒测试,又称结构测试

引言

贪心算法总是做出当前最好的选择,也就是说,它期望通过局部最优选择从而得到全局最优的解决方案。——《算法导论》
算法基础8 —— 详解贪心算法(部分背包问题 + 区间调度问题)_第2张图片

贪心算法(greedy algorithm)的特点
  • 贪心算法在解决问题时只根据当前已有的信息做出选择,而且一旦做出了选择,这个选择都不会改变
  • 贪心算法并不是从整体最优考虑,它所做出的选择只是在某种意义上的局部最优
  • 贪心算法能得到许多问题的整体最优解或整体最优解的近似解
  • 选择什么样的贪心策略,直接决定算法的好坏。即贪心策略不唯一

算法基础8 —— 详解贪心算法(部分背包问题 + 区间调度问题)_第3张图片

贪心选择性质

所谓贪心选择性质是指原问题的整体最优解可以通过一系列的局部最优选择得到。应用同一规则,将原问题变为一个相似的但规模更小的子问题,而后的每一步都是当前最佳的选择。这种选择依赖于已做出的选择,但不依赖于未做出的选择。运用贪心策略解决的问题在程序的运行过程中无回溯过程

最优子结构性质

当一个问题的最优解包含其子问题的最优解时,称此问题具有最优子结构性质。问题的最优子结构性质是该问题是否可用贪心算法求解的关键。例如原问题S={a1,a2,…,ai,…,an},通过贪心选择选出一个当前最优解{ai}之后,转化为求解子问题S−{ai}的最优解

贪心算法 —— 部分背包问题

描述:有n个物体,第i的物体的重量为wi,价值为vi,在背包重量不超过c的情况下让选取的总价尽量的高,每一个物体可以支取一部分,价值和重量按比例计算

贪心策略:首先计算出每个物体的单位价值(v/w),然后从大到小排序。按照排好的顺序选择物品放入背包,由于物品可以部分装入,所以装完后背包的重量一定为c

算法基础8 —— 详解贪心算法(部分背包问题 + 区间调度问题)_第4张图片

结构体解析及本题代码实现

算法基础8 —— 详解贪心算法(部分背包问题 + 区间调度问题)_第5张图片

实现代码:

#include 
#include 

using namespace std;

const int N = 110;

struct node
{
    int v;//表示物品的价值value
    int w;//表示物品的重量weight
    double vdw;//表示物品的单位价值 
};
struct node p[N];//结构体数组

bool cmp(node a,node b)
{
    return a.vdw > b.vdw;//按照单位价值从大到小排序
}

int main()
{
    int n,c;//n和c分别表示物品的数量以及背包的容量
    cin >> n >> c;
    
    for (int i = 0;i < n;i++)
    {
        cin >> p[i].w >> p[i].v;//输入每个物品的重量以及价值
        p[i].vdw = (double)p[i].v / p[i].w;//计算其单位价值(转化为浮点数技巧:*1.0)
    }
    
    sort(p,p + n,cmp);//对结构体数组p按照性价比从大到小排序
    
    double sum_v = 0;//sum用来记录可以得到的总价值
    for (int i = 0;i < n;i++)
    {
        if (c >= p[i].w)//当前背包的容量大于可以选择的物品的重量
        {
            sum_v += p[i].v;//总价值加上其价值
            c -= p[i].w;//背包容量减去其重量
        }
        else 
        {
            sum_v += p[i].vdw * c;//目前背包的容量不能放下整个物品 -> 按照单位价值计算
            break;//背包已满,结束装包
        }
    }
    
    cout << sum_v << endl;
    return 0;
}

测试数据:

5 100
30 65
10 20
20 30
50 60
40 40

输出结果

163

另一个背包:01背包

区间调度问题

问题描述

有n项工作,每项工作分别在si开始,ti结束。对每项工作,你都可以选择参加或不参加,但选择了参加某项工作就必须至始至终全程参与,即参与工作的时间段不能有重叠

输入

n = 5
S = {1,2,4,6,8}
T = {3,5,7,9,10}

输出

3(选择工作1-3-5)

算法基础8 —— 详解贪心算法(部分背包问题 + 区间调度问题)_第6张图片

可考虑以下几种贪心策略:

  • 每次选取开始时间最早的
  • 每次选取结束时间最早的
  • 每次选取用时最短的

对于以上四种贪心策略,只有第二种是正确的
反例排除第一种贪心策略。如下图,如果每次选取开始时间最早的工作,则一共可以选择2个工作。如果每次选取结束时间最早的,则一共可以选择3个工作

S = {1,2,4,6,9}
T = {3,5,10,8,10}

算法基础8 —— 详解贪心算法(部分背包问题 + 区间调度问题)_第7张图片

反例排除第三种贪心策略。如下图,如果每次选取用时最短的工作,则一共可以选择2个工作。但如果每次选取结束时间最早的,则一共可以选择3个工作

S = {1,3,4,6,7}
T = {3,4,6,7,10}

算法基础8 —— 详解贪心算法(部分背包问题 + 区间调度问题)_第8张图片

代码如下

#include 
#include 

using namespace std;

const int N = 110;

struct node 
{
    int s;//表示工作的开始时间
    int t;//表示工作的结束时间
}a[N];//结构体数组

bool cmp(node p,node q) {return p.t < q.t;}

int main()
{
    int n;
    cin >> n;//n表示一共有几个工作
    
    for (int i = 0;i < n;i++) cin >> a[i].s >> a[i].t;
    
    sort(a,a + n,cmp);
    
    int tem = a[0].t;//暂存第一个工作的结束时间
    int cnt = 1;
    for (int i = 1;i < n;i++)
        if (a[i].s > tem)//当前工作的开始时间大于上一个选择的工作的结束时间
        {
            cnt++;
            tem = a[i].t;
        }
        
    cout << cnt << endl;
    return 0;
}

第一张图的测试用例:

5
1 3
2 5
4 7
6 9
8 10

第二张图的测试用例:

5
1 3
2 5
4 10
6 8
9 10

第三张图的测试用例:

5
1 3
3 4
4 6
6 7
7 10

贪心算法练习题
POJ 2376 Cleaning Shifts
HDOJ 2037 今年暑假不AC
HDOJ 1009 FatMouse’ Trade

你可能感兴趣的:(算法,贪心算法,算法)