首先相信你已经看过《背包九讲》。对于每一次决策后,我们都能得到一组值:F[ i, j] I 表示进行了i次决策,j表示占用了j 的体积。最终获得了F[i,j]的收益。这么考虑的话,很显然,就能得到最优子结构的性质
:如果最终能得到Fmax[ n, v ] ,那对于每一组i,j 必定 F[i,j]=Fmax[i,j]。因此,在遍历树的时候,如果两种决策有相同的i,j 那我们可以取出两者中的最大值,另外一种就被无情的抛弃了-_- 。这样,对于每一组的i,j 我们可以方便的按如下方程求出:
F[i,j]=max( F[i-1,j] , f[i-1][j-volume[i]] + value[i] ) 按照这个方程,就能方便地求出最优解
再分析一下这个方程。他符合之前说的两种决策:选取物品i,或不选。最终的结果,是我们能在O(1)的时间内,判定对于体积j,是否应当选取第i件物品。 我们在这里作出了最优的选择。那被我们抛弃的选择呢?他很可能是次优解,第三优解,无论怎样,他都对我们本题求前K优解,起到了重要的作用!
那么,本题的算法,就水到渠成了。设数组[ i , j , p ]。表示对于一组 i,j, F[i,j,p] 为其第P优的决策。那么,上文的max操作,就需要稍加扩展,变为对两个队列的合并(一种决策的第a1优解很可能是总体的第a2优解)。毋庸置疑,这两个队列是有序的,将其合并即可完成本次决策。代价,则从O(1) 涨到了O(k) ,总的时间复杂度由O(vn) 升至O(vnk) 刚好多了一维。RQNOJ上有前人写好的代码,可读性非常高,我就不贴我自己写的无比粗糙的代码了。
用个形象的比喻吧:如果我想知道学年最高分,那么,我只要知道每个班级的最高分,然后统计一遍就可以了。如果我想知道学年前十呢?我必须要知道每个班的前十名。大家在心里模拟一下,对,这就是本题核心的算法。两种决策,就可以看作这个学年只有两个班。
由于我们在选的时候把次优解已经丢掉了,其实次优解也可以和次优解可以合成次优解(这就是为什么不能丢掉次优解),这就是k循环的一个 的目的,再合并找出前k个最大的,为什么呢?(比喻以说明了)。
#include<stdio.h> #include<stdlib.h> void ZeroOnePack( int N,int V, int K, int val[], int volumn[] ) { int f[1024][32]={0},A[32]={0},B[32]={0}; for( int i=1; i<= N; i++ ) { for( int j=V; j>=volumn[i]; j-- ) { int t=j-volumn[i]; for( int k=1;k<=K; k++ )//寻找体积j的前k个最优解合成的最优解 { A[k]=f[t][k]+val[i]; B[k]=f[j][k]; } int a=1,b=1,c=1; while( c<=K&&( a<=K||b<=K ) )//合并重新生成前k个最优解 { if( A[a]>B[b] ) f[j][c]=A[a],a++; else f[j][c]=B[b],b++; if( f[j][c]!=f[j][c-1] ) c++; } } } printf( "%d\n",f[V][K] ); } int main() { int n,N,V,k,val[124],volumn[124]; scanf( "%d",&n ); for( int i=1; i<=n ; i++ ) { scanf( "%d%d%d",&N,&V,&k ); for( int j=1; j<=N; j++ ) scanf( "%d",&val[j] ); for( int j=1; j<=N; j++ ) scanf( "%d",&volumn[j] ); ZeroOnePack( N,V,k,val,volumn ); } return 0; }