本文作于2007年岁末,没有写完就决定2008年不搞OI了。此文暂且存放在这里,待我有大学上之后,将决定是否开始维护这个博客。这里以后将代替林影阁。
今年NOIP前就曾读过dd的《背包问题九讲》,时间仓促,只程序实现了前4讲。觉得混合背包这个设想,以及泛化背包这个概念很经典,有必要写点东西共享一下自己的看法。新年之际献上。Happy New Year~
混合背包
背包是个很经典而实用的问题……
总体上,背包可以分为4类。部分背包(可以用贪心有效解决),01背包,完全背包,多重背包(这三个可以用DP有效解决)。而混合背包是后三者的混合。这几个的理论在《背包问题九讲》中有过详细介绍。这里我们只通过一个实例来看混合背包是如何实现的。
例1 期中考试前夜
【问题描述】
快乐的DW今天很不高兴,因为明天要期中考试,而且她知道她复习不完了。
【问题描述】
虽然形势不很乐观,但DW还是希望明天能拿到尽可能高的分数。
DW简单分析了一下,现在可利用的时间为m,还有n个任务需要完成。第i个任务需要一定的时间t(i),但也有一定的价值v(i)。
但是好像没这么简单。她发现所有的任务可以分为两类,A类和B类。
A类是背诵类的任务。如果第i个任务是A类的,那么第一遍做这个任务获得价值v(i),再做它的价值就是0。也就是说,第i个任务做x次的价值就是v(i) (x>0)。
B类是练习类的任务。如果第i个任务是B类的,那么每做一遍这个任务获得的价值都是v(i)。也就是说,第i个任务做x次的价值就是x*v(i)。
DW希望她能利用这些时间获得最大的价值,而坐在她后面的ZS也在忙着复习(是复习NOIP,不是期中考试),所以请聪明的你来帮忙了。
【输入格式】
第一行两个整数,n,m。含义已述。
接下来有n行,第i+1行有三个量v,t,chr。V,t分别表示v(i),t(i)。chr是字符A或B,表示它所属的任务类别。
【样例输入】
5 10
2 3 A
2 4 A
2 5 A
2 6 A
5 2 B
【输出格式】
一行,一个整数,表示可以获得的最大价值。
【样例输出】
25
【数据规模】
n<=1000,m<=10000,其它数据<=maxlongint。
可以看出这是01背包和完全背包的混合。见代码段1。
泛化背包
背包中放入的物品可能存在其它限定。比如,要放入物品a,物品b也必须放入。称这种关系为依附关系。《金明的预算方案》是一个典例,不过它的依附方式很简单,对于每一种依附情况枚举即可。
泛化背包这个概念是由dd牛提出,用来解决更一般的具有依附关系的背包问题。
例2 金明的预算方案改
【问题描述】
金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。今天一早,金明就开始做预算了。
物品之间是存在依附关系的,下表就是一些例子:
如果要买一件物品,如果它有父节点,那么它的父节点也一定要买。比如。要买扫描仪就必须买电脑,那么买电脑也就必须买桌子。所有的依附关系都可以由一个森林来描述,这意味着不可能出现“依附环”。
金明想买的东西很多,肯定会超过妈妈限定的N元。于是,他把每件物品规定了一个重要度,分为5等:用整数1~5表示,第5等最重要。他还从因特网上查到了每件物品的价格(都是10元的整数倍)。他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为j1,j2,……,jk,则所求的总和为:v[j1]*w[j1]+v[j2]*w[j2]+ …+v[jk]*w[jk]。(其中*为乘号)请你帮助金明设计一个满足要求的购物单。
【输入格式】
输入文件的第1行,为两个正整数,用一个空格隔开:
N m
其中N(<32000)表示总钱数,m(<60)为希望购买物品的个数。)
从第2行到第m+1行,第j行给出了编号为j-1的物品的基本数据,每行有3个非负整数
v p q
(其中v表示该物品的价格(v<10000),p表示该物品的重要度(1~5),q表示该物品的直接依附物品。如果它没有依附的物品,那么q=0)
【样例输入】
10
500 0 1
5000 1 4
300 1 5
1000 2 2
800 2 4
10 0 1
10 6 3
10 6 2
200 0 3
800 9 3
(注:这个样例与图中物品的标记一一对应)
【输出格式】
输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值
(<200000)。
下面我们来讨论这个问题。
请设想,如果你就是那个最优解,你是否真的关心放在背包中的是哪些物品呢?可能并不是,你所关心(或担心)的是,备选物品是否利用背包的空间组合出了最优解。
我们规定节点k由一个函数fk表示(请将k理解为函数f的下标),fk(w)表示这样的含义:提供空间w,将以k为根节点的子树中的节点作为物品选择对象,组合出的最优价值。
如果要求f1,那么我们会想知道f2和f3,那么f1(w)=max{f2(k)+f3(w-k)},其中0<=k<=w。它的最优子结构很好证明。看吧,我们没有以物品为对象。这种最优解操作是在函数层面上的。也就是说,为了得到节点p的函数值fp(v),则它的孩子的f值要在这之前获得。之后我们枚举各种空间分配的方法,找到其中的最大值。
我们就把这些物品“泛化”了,节点2、3通过函数f的形式,将自己这棵自树的最优解情况汇报给节点1,节点1通过上述的状态转移方程,评测出节点1的最优解。
初始化时,所有叶子节点都做这样的操作。设节点k的价值为v,价钱为w。fk全部赋0,然后fk[w]=v,
在程序实现上,我们要把这个森林转化成二叉树,这样处理起来比较方便(同样的比如《选课》)。
程序实现见代码段2(未完成)。
代码段1
- program example1(input,output);
- var
- n,m,i:longint;
- v,t:array[1..1000]of longint;
- f:array[0..10000]of longint;
- IsZeroOne:array[1..1000]of boolean;
- chr0,chr1:char;
- function max(x,y:longint):longint;
- begin
- if x>y then exit(x) else exit(y);
- end;
- procedure ZeroOnePack(cost,value:longint);
- var
- vi:longint;
- begin
- for vi:=m downto cost do
- f[vi]:=max(f[vi],f[vi-cost]+value);
- end;
- procedure CompletePack(cost,value:longint);
- var
- vi:longint;
- begin
- for vi:=cost to m do
- f[vi]:=max(f[vi],f[vi-cost]+value);
- end;
- begin
- assign(input,'a.in'); reset(input);
- readln(n,m);
- for i:=1 to n do
- begin
- readln(v[i],t[i],chr0,chr1);
- if chr1='A' then IsZeroOne[i]:=true else IsZeroOne[i]:=false;
- end;
- fillchar(f,sizeof(f),0);
- for i:=1 to n do
- if IsZeroOne[i] then ZeroOnePack(t[i],v[i])
- else CompletePack(t[i],v[i]);
- writeln(f[m]);
- close(input);
- end.
代码段2(未完成)