等待了一年时间,这个系列的坑终于又开始填了……
不说废话,直接开始正题。
1.何为贪心?
贪心算法实际上指的是把问题划分成一个一个的子问题,然后针对当前的子问题,求出局部最优解,然后将子问题的最优解合并,最终获得总问题的最优解。
值得注意的是,在对问题求解时,贪心算法总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,它做出的仅是在某种意义上的局部最优解。
P.S:贪心子问题是独立的,有区别于动态规划(这个以后讨论动规的时候再聊)。
2.如何判断贪心
从上面这段话中不难看出,一个问题能够通过贪心来获取最优解的前提是:
- 问题可以被划分成多个子问题。
- 证明可以通过子问题的最优解可以获得最终的最优解。
- 子问题必须具有无后效性,也就是说,当前问题的求解的过程并不会影响之前的子问题的结果。
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)
{
if(a+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),是不是很棒~