题目来源:
HihoCoder1364题目要求:
小Hi在游乐园中获得了M张奖券,这些奖券可以用来兑换奖品。
可供兑换的奖品一共有N件。第i件奖品需要Wi张奖券才能兑换到,其价值是Pi。
小Hi使用不超过M张奖券所能兑换到的最大奖品总价值是多少?
输入输出格式:
输入:
第一行两个整数N,M。
接下来N行,每行两个整数Wi,Pi。
对于 50%的数据: 1≤N,M≤1000
对于 100%的数据: 1≤N,M≤105,1≤Pi,Wi≤10。
输出:
一行一个整数,表示答案。
解答:
·概述:
本题猛的一看就是一个典型的01背包问题,但是笔者亲测发现,由于数据量比较大,直接使用01背包的动态规划方式会超时,因此需要在原有背包问题的基础上进行一些优化。考虑到Wi和Pi的大小范围均为[1,10],因此,根据Wi和Pi的不同组合,不同种类的商品数目最多为100种。对于商品个数N的最大值为105,其中一定包含重复的商品,我们通过统计每一种商品出现的次数来对本问题进行优化。对于某一个价格为Wi,价值为Pi的商品,如果其出现次数为C次,则可以利用如下的二进制优化法来进行优化。
·整数的二进制拆分法:
在说明二进制优化法前,首先介绍一个定理,如下:对于任意的整数X,均可以将其表示为多个2的连续幂值之和再加一个余项的形式,即:
X = 2^0 + 2^1 + ... + 2^k + R
例如,当X=100时,可以表示为:
100 = X = 1 + 2 + 4 + 8 + 16 + 32 + 37
其中,余项R的值为37。
接下来,为了便于描述,这里记2的k次幂为M[k],因此,对于任意的整数X,都可表示为如下的形式:
X = M[0] + M[1] + ... + M[k] + R,
显然,这里的k值满足这样的条件:M[0] + M[1] + ... + M[k] ≤ X,且M[0] + M[1] + ... + M[k] + M[k+1] > X,通过简单的计算,可以得到k的值为floor(log(x+1)) - 1,对于上面X=100的例子,k的值为5。
此时,有如下的结论:
对于任意的不大于X的非负整数Y,均可从M[0], M[1], ... M[k], R中的k+1个数中选择若干数字,其和恰好为Y。
对于这个结论,可以用分两种情况加以证明:
如果Y≤M[k+1],那么Y一定可以通过M[0], M[1], ... M[k]中的若干数字的和来表示,只要将Y转换为二进制格式,将各个值为1的对应位的Mi值相加即可。例如,对于上面的例子,当Y的值为35时,转换为二进制为:100011,其中第0位,第1位和第5位为1,所以Y = M[0] + M[2] + M[5] = 1 + 2 + 32 = 35;
对于Y>M[k+1]的情况,则可以知道Y-R一定是一个不大于M[k+1]的数字,可以用反证法证明:如果Y-R>M[k+1],那么Y-R+R> M[k+1]+R > M[0] + M[1] + ... + M[k] + R = X,这和Y≤X的条件是矛盾的,因此Y-R≤M[k+1],根据前面的结论,Y-R一定可以用M[0], M[1], ... M[k]中的若干数字的和来表示,那么,Y就可以表示为R以及M[0], M[1], ... M[k]中的若干数字的和。
综上所述,对于任意的Y≤X,均可以用R,M[0], M[1], ... M[k]中的若干数字的和来表示。
·01背包问题的二进制优化法:
有了以上结论后,在回到本题,对于某一个价格为Wi,价值为Pi的商品,如果其出现次数为C次,根据上面的定理,出现次数C可以表示为如下的形式:C = M[0] + M[1] + ... + M[k] + R
此时,我们将原题中C个商品替换为如下的k+1个商品:
1个价格为M[0]*Wi,价值为M[0]*Pi的商品;
1个价格为M[1]*Wi,价值为M[1]*Pi的商品;
1个价格为M[2]*Wi,价值为M[2]*Pi的商品;
...
1个价格为M[k]*Wi,价值为M[k]*Pi的商品;
1个价格为R*Wi,价值为R*Pi的商品;
根据前面的结论,对于任意的小于C的非负整数,均可以用R,M[0], M[1], ... M[k]中的若干数字的和来表示,所以,不论问题的最优方案中选择了多少个当前类型的商品,将C个原来的商品替换为现在的k+1个商品后,均可以找到等价的方案,因此,替换过程不会影响本题的结果。
由于k的值为 floor(log(x+1)) - 1,因此,当C的值很大时,把C个商品替换为k+1个商品时,可以大幅度地缩减问题规模,从而提高程序效率。
将输入中的每一种商品都做一次这样的优化后,商品数目将大大较少,再使用01背包问题的解法进行求解,就可以得到本题的正确解法。
程序代码:
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* This is the ACM problem solving program for hihoCoder 1364.
*
* @version 2018-03-24
* @author Zhang Yufei.
*/
public class Main {
/**
* Record the commodity information.
*/
private static class Commodity {
int price;
int value;
}
/** For input. */
private static Scanner scan;
/** For output. */
private static PrintWriter writer;
/** Input data */
private static int M, N;
/** Commodity list */
private static List<Commodity> commodities;
/**
* Used for DP, in i-th iterator, dp[j] means the maximum
* prize given i commodities and j token.
*/
private static int[] dp;
/**
* Used for optimization, matrix[i][j] records the amount
* of commodities whose price is i and value is j.
*/
private static int[][] matrix;
/**
* The main program.
*
* @param args
* The command-line parameters list.
*/
public static void main(String[] args) {
initIO();
input();
dp = new int[M + 1];
for(int j = 0; j <= M; j++) {
dp[j] = 0;
}
for(int i = 0; i < N; i++) {
for(int j = M; j >= 0; j--) {
int r1 = dp[j];
int r2 = 0;
if(j - commodities.get(i).price >= 0) {
r2 = dp[j - commodities.get(i).price] + commodities.get(i).value;
}
dp[j] = r1 > r2 ? r1 : r2;
}
}
System.out.println(dp[M]);
closeIO();
}
/**
* Deal with input.
*/
private static void input() {
N = scan.nextInt();
M = scan.nextInt();
matrix = new int[11][11];
for(int i = 0; i < 11; i++) {
for(int j = 0; j < 11; j++) {
matrix[i][j] = 0;
}
}
for(int i = 0; i < N; i++) {
int W, P;
W = scan.nextInt();
P = scan.nextInt();
matrix[W][P]++;
}
commodities = new ArrayList<>();
for(int i = 1; i <= 10; i++) {
for(int j = 1; j <= 10; j++) {
int count = matrix[i][j];
if(count > 0) {
int s = 1;
while(count >= s) {
Commodity c = new Commodity();
c.price = s * i;
c.value = s * j;
commodities.add(c);
count -= s;
s *= 2;
}
if(count > 0) {
Commodity c = new Commodity();
c.price = count * i;
c.value = count * j;
commodities.add(c);
}
}
}
}
N = commodities.size();
}
/**
* Initial the input and output.
*/
private static void initIO() {
try {
scan = new Scanner(System.in);
writer = new PrintWriter(System.out);
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* Close the input and output.
*/
private static void closeIO() {
scan.close();
writer.close();
}
}