一,概述
1.问题描述
女主角多多,有N部想看的电影,她爷爷限制她只能看L分钟,她将每部电影按喜好标明了价值V[i],同时每部电影也有自己的播放时间T[I],i from 1 to N,每部电影要么看完要么不看,现要求叔叔去帮她买回来,可是商店中只有卖N种中的M种,请问叔叔如何在L,M的限制下买到价值最大的物品呢?
问题抽象:
N个物品,每个物品,容量T[i],价值V[i],背包容量L,若最多装M件且M<=N,求如何在L,M限制下挑选出最大价值的物品组合?
2.问题链接
HDU -- Watch The Movie(ACM Step: 3.3.4)
3.问题截图
图1.1 问题截图
二,算法思路
乍一看下,似乎是普通的“二维费用的01背包问题”,此术语来自书籍《背包九讲》,只需要在M或者M件之内选出最大价值的物品组合即可,费用一是物品的容量(背包最大容量为L,每件物品若装入,会消耗一定的背包容量),费用二是物品的件数(背包最多装M件,每件物品若装入,会消耗一件)。
上述思路实现的代码无法得到正确的结果,于是参考了网上的思路,主要有两种说法:
1)每件物品的价值可能是负的,所以初始化应该把某些值设为负数(有设-1,有设负无穷的)。
2)需要求出装满M件的结果。
对于第一种说法,首先题目有声明每件物品的价值是大于0的,并且如果说法一正确,那么为什么要设负数呢??没有想明白,个人觉得如果价值是负数,应该简单的不装这件物品,装它只会把总价值变小。
在参考了第二个思路下,总结了两种算法,差别在于空间复杂度,差别不大,一个常数系数,但是对思考的方式有显著的改善,现说明如下。
1.空间复杂度O(2LM)的算法
首先,对于二维费用的01背包问题,需要使用二维数组保存最后的结果,即可以得到的最大价值,由于题目有对件数的限制,因此最后的结果不仅记录了价值,还记录了当前是否装满的信息,这相当于用了两个二维数组来记录答案。所以复杂度为O(2LM)。假设保存价值的二维数组为F[L][M],保存装满信息的二维数组是G[L][M]。
具体实现思路是:初始化时,将F所有元素初始化为0,因为此时还没有物品装入;对于G,将G[L][0]初始化为0,其他初始化为-1,表示在最大件数为0件时,此时无论L是多少,都可以看作是装满的,最大件数大于0时,-1表示装不满。
最后,按照求解二维费用的01背包问题的一般思路,即方程F[l][m] = max(F[l][m], F[l-T[i]][m-1] + V[i])
只需要在求每一个最大价值时,首先判断两个候选表达式的G数组值,即只有在它们装满的情况下,才进行比较大小。
2.空间复杂度O(LM)的算法
在参考了一些作者的代码后,并且写完上述代码后总感觉好像多了点什么,《背包九讲》的关于“装满物品的初始化”在脑海里一直回响,里面说的是“对于一开始就装满的状态的初始化(通常是0),初始化为0,对于装不满的状态的初始化,初始化为负无穷“,最后肯定了这个想法:“由于仅要求件数装满,所以对于物品件数的维度,在装0件的那些状态,初始化为0,表示装满,对于其他的初始化为负无穷“,由于题目有规定所有物品的价值和不会超过能表示的最大正数,所以可以初始化为最大负数,这样在全部都装并且价值最大的情况下还是可以保持最后的结果时负数,即装不满,代码的通过验证了此想法的正确性,致敬《背包九讲》的作者,将问题分析的十分透彻。
所以在之后的装满问题中,若要求将某些维度装满,则将那些对应维度的初始化工作做好即可。
三,算法实现
#include <iostream> // for cin, cout, endl
#include <climits> // for INT_MIN
using std::cin;
using std::cout;
using std::endl;
const int MAX_MOVIES = 100; // the max num of movies that can select
const int MAX_TIMES = 1000; // the max num of time used for watching movie
void input(int&, int&, int&);
void compute(int&, int&, int&);
void output(int&, int&);
struct Movie{
int t; // length of time of movie
int v; // value of movie
}; // input data, information about movie
Movie movies[MAX_MOVIES+1]; // hold input
int ans[MAX_TIMES+1][MAX_MOVIES+1]; // +1 for time from 0 to 1000 and movie num from 0 to 100
int main()
{
int t, M, N, L;
cin >> t;
for (int i=0; i<t; ++i){
input(M, N, L);
compute(M, N, L);
output(M, L);
}
}
int max2(int a, int b)
{
if (a > b)
return a;
else
return b;
}
void input(int& M, int& N, int&L)
{
cin >> N >> M >> L;
for (int i=1; i<=N; ++i)
cin >> movies[i].t >> movies[i].v;
}
void compute(int& M, int& N, int& L)
{
int m, n, t;
// initialize the ans array, ans[t][0] to 0 and others to INT_MIN,
// because when 0 movie be selected only the m==0 can be filled and value is 0, others can not be filled and using -INT_MIN to indication
for (t=L; t>=0; --t)
for (m=M; m>=0; --m)
ans[t][m] = INT_MIN;
for (t=0; t<=L; ++t)
ans[t][0] = 0;
for (n=1; n<N; ++n)
for (t=L; t>=movies[n].t; --t)
for (m=M; m>=1; --m)
ans[t][m] = max2(ans[t][m], ans[t-movies[n].t][m-1]+movies[n].v);
// for movie N
if (movies[N].t > L)
return;
ans[L][M] = max2(ans[L][M], ans[L-movies[N].t][M-1]+movies[N].v);
}
void output(int& M, int& L)
{
if (ans[L][M] < 0)
cout << 0 << endl;
else
cout << ans[L][M] << endl;
}