步步为营(二) 贪心(1)理论初探

等待了一年时间,这个系列的坑终于又开始填了……

不说废话,直接开始正题。

1.何为贪心?

贪心算法实际上指的是把问题划分成一个一个的子问题,然后针对当前的子问题,求出局部最优解,然后将子问题的最优解合并,最终获得总问题的最优解。
值得注意的是,在对问题求解时,贪心算法总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它做出的仅是在某种意义上的局部最优解

P.S:贪心子问题是独立的,有区别于动态规划(这个以后讨论动规的时候再聊)。

2.如何判断贪心

从上面这段话中不难看出,一个问题能够通过贪心来获取最优解的前提是:

  1. 问题可以被划分成多个子问题。
  2. 证明可以通过子问题的最优解可以获得最终的最优解。
  3. 子问题必须具有无后效性,也就是说,当前问题的求解的过程并不会影响之前的子问题的结果。
3.贪心策略的制定

1.制定最优解策略,从最初状态开始。
2.循环解决子问题,逐步缩小问题规模。针对每一个子问题,都运用局部最优解策略获取结果。
3.对每个子问题的解进行处理,获得最终结果

贪心问题的求解代码都不会很长,但是对于贪心策略的制定确是要费点心力。
一般来说,多用点数据验证验证,就能减少很多不必要麻烦。


下面让我们来看一个问题:


问题来源:NYOJ 71

                            独木舟上的旅行
                时间限制:3000 ms  |  内存限制:65535 KB
                              难度:2

描述
进行一次独木舟的旅行活动,独木舟可以在港口租到,并且之间没有区别。一条独木舟最多只能乘坐两个人,且乘客的总重量不能超过独木舟的最大承载量。我们要尽量减少这次活动中的花销,所以要找出可以安置所有旅客的最少的独木舟条数。现在请写一个程序,读入独木舟的最大承载量、旅客数目和每位旅客的重量。根据给出的规则,计算要安置所有旅客必须的最少的独木舟条数,并输出结果。

输入
第一行输入s,表示测试数据的组数;
每组数据的第一行包括两个整数w,n,80<=w<=200,1<=n<=300,w为一条独木舟的最大承载量,n为人数;
接下来的一组数据为每个人的重量(不能大于船的承载量);

输出
每组人数所需要的最少独木舟的条数。

样例输入
3
85 6
5 84 85 80 84 83
90 3
90 45 60
100 5
50 50 90 40 60


样例输出
5
3
3

下面让我们来分析一下这道题。
n个人,每个人体重为arr[i]。船每次最多乘坐两个人,而且每次承载的体重不能超过w。求最少的渡船数。
换种描述方式,也就是说,有n个数,要求划分为多个集合,每个集合最多有两个元素,且两个元素的和不能超过w。求最小的集合数。

那么这道题能不能用贪心来做呢?
首先我们来看

1.问题可以被划分成多个子问题。

显而易见,这是可以的。整个数列的划分过程可以分解为两个数的组合过程

2.证明可以通过子问题的最优解可以获得最终的最优解。

这个也是可以理解的,对于这道题而言,两个数的组合过程的最优解就是两个数成功的组合成一个集合。而对于整个问题来说,最优解是尽可能多的让两个数进行组合。所以,通过子问题的最优解就可以得到整个问题的最优解。

3.子问题必须具有无后效性,也就是说,当前问题的求解的过程并不会影响之前的子问题的结果。

这个是显而易见的,当前进行组合的数都是未进行组合过的,所以肯定不会对之前的问题解造成任何影响。

现在我们知道了,这道题是可以通过贪心来做的,接下来的问题就是如何制定贪心策略。
从题意中得出,组合的过程有三种情况:
1. a+b <= w,那么判断a,b是不是最接近木船重量的,如果是可以划分到一个组里,从数组中删除a和b。如果不是,继续找。也就是说,优先进行a+b最大的进行组合
2. a+b >= w,那么a,b不可以放进一个组里,两个数与其他数重新进行组合。
3. 如果没有能与a进行组合的数字,则a自己成为一个集合,从数组中删除a;

那么我们可以用伪代码描述整个过程

for(从数组中遍历a)
{
    for(从数组中遍历b)
    {
        ifa+b<=w)
        {
            if(a+b最接近w)
            {
                标记b;
            }
        }
        else{
            什么也不做,继续下一个对比;
        }
    }

    if(b有标记)
    {
        从数组中删除a和b;
        总集合数+1;
    }
    if(没有能与a组合的数)
    {
        从数组中删除a;
        总集合数+1;
    }
}

这样的话,直到两个循环执行完毕,那么总集合数就是最终的结果。

详细代码如下:

/* 
************************************ 
    Title: NYOJ71--独木舟上的旅行
************************************ 
    Date:2015/07/18 
************************************ 
    author:刘旭 
************************************ 
Memory:232KB
Time:8ms
************************************ 
*/  
#include <stdio.h>

#define MAX 305

int main()
{
    int T = 0;
    scanf("%d", &T);    ///获取测试数据组数

    while(T--)
    {
        int weight_people[MAX]; ///记录每个人的体重的数组
        int vis[MAX];   ///记录每个人是否被删除的数据,vis[i] = 0表示这个人已经被运走,不能进行组合
        int weight = 0; ///船的最大载重数
        int num_people = 0; ///人的数量
        scanf("%d%d", &weight, &num_people);

        for(int i = 0; i < num_people; i++)
        {
            scanf("%d", &weight_people[i]); ///循环输入每个人的体重
            vis[i] = 1; ///标记每个人
        }

        int ans = 0;    ///总集合数

        for(int i = 0; i < num_people; i++)  ///循环遍历
        {
            if(0 == vis[i])     ///如果这个人被运走,计算下一个人
            {
                continue;
            }

            int key = -1;   ///判断是否有人组合
            int max = -2;   ///目前组合的体重

            for(int j = 0; j < num_people; j++)
            {
                if(0 == vis[j] || i == j)     ///如果这个人被运走或者和进行比对的人重复,计算下一个人
                {
                    continue;
                }

                if(weight_people[i] + weight_people[j] <= weight) ///如果装的下两个人
                {
                    if(weight_people[i] + weight_people[j] > max)   ///这两个人的体重最大
                    {
                        key = j;
                        max = weight_people[i] + weight_people[j];
                    }
                }
            }
            if(-1 != key) ///如果装的下两个人
            {
                vis[i] = vis[key] = 0;    ///标记这两个人
                ans ++; ///总集合数+1;
            }
            if(-1 == key)   ///没有能与a组合的人,独自上船
            {
                vis[i] = 0;
                ans++;
            }
        }

        printf("%d\n", ans);
    }
}

俗话说,生命不息,折腾不止。这道简单的题时间居然在8ms~
其实症结很简单,两个for循环闹得,时间复杂度O(n^2)。那么有没有什么方法可以简化呢,当然可以。
这道题最关键的地方在于优先选择a+b最大的组合,那么我们就从这个方面入手。
针对一个有序数列a[n],让里面元素按从小到大的顺序排列,则可知 a[1] <=a[2]<= a[3]…a[n-1]<=a[n].
则很容易推导
1. a[n]+a[1] <= a[n]+a[x] (1< x < n)
2. a[1]+a[n] >= a[1]+a[x] (1< x < n)

也就是说,
1. 对于a[n]来说,如果a[1]+a[n]都不能小于w,那么他就不能与任何数相加小于w,只能一个数组成集合。
2. 对于a[1]来说,如果a[n]不行,那就查看a[n-1]是否可以,如果这样能找到一个数a[x],那么a[1]+a[x]一定是最接近w的值

所以我们可以写代码了

/* 
********************************** 
    Title: NYOJ71--独木舟上的旅行
********************************** 
    Date:2015/07/18 
**********************************
    author:刘旭
**********************************
Memory:232KB
Time:0ms
**/
#include <cstdio>
#include <algorithm>

using namespace std;

#define MAX 305

int main()
{
    int T = 0;
    scanf("%d", &T);

    while(T--)
    {
        int weight_people[MAX];
        int vis[MAX];
        int weight = 0;
        int num_people = 0;
        scanf("%d%d", &weight, &num_people);

        for(int i = 0; i < num_people; i++)
        {
            scanf("%d", &weight_people[i]);
        }

        sort(weight_people, weight_people+num_people);

        int ans = 0;
        int pos_start = 0;
        int pos_end = num_people-1;
        while(pos_start <= pos_end)
        {
            if(weight_people[pos_start] + weight_people[pos_end] <= weight)
            {
                pos_end -= 1;
                pos_start += 1;
                ans += 1;
            }
            else
            {
                pos_end -= 1;
                ans += 1;
            }
        }

        printf("%d\n", ans);
    }
}

时间复杂度O(nlogn),是不是很棒~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值