第九讲 背包问题问法的变化

以上涉及的各种背包问题都是要求在背包容量(费用)的限制下求可以取到的最大价值,但背包问题还有很多种灵活的问法,在这里值得提一下。但是我认为,只要深入理解了求背包问题最大价值的方法,即使问法变化了,也是不难想出算法的。

例如,求解最多可以放多少件物品或者最多可以装满多少背包的空间。这都可以根据具体问题利用前面的方程求出所有状态的值(f数组)之后得到。

还有,如果要求的是“总价值最小”“总件数最小”,只需简单的将上面的状态转移方程中的max改成min即可。

下面说一些变化更大的问法。

输出方案

一般而言,背包问题是要求一个最优值,如果要求输出这个最优值的方案,可以参照一般动态规划问题输出方案的方法:记录 下每个状态的最优值是由状态转移方程的哪一项推出来的,换句话说,记录下它是由哪一个策略推出来的。便可根据这条策略找到上一个状态,从上一个状态接着向前推即可。

还是以01背包为例,方程为f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。再用一个数组g[i][v],设g[i][v]=0表示推出f[i][v]的值时是采用了方程的前一项(也即f[i][v]=f[i-1] [v]),g[i][v]表示采用了方程的后一项。注意这两项分别表示了两种策略:未选第i个物品及选了第i个物品。那么输出方案的伪代码可以这样写(设 最终状态为f[N][V]):

i=N
v=V
while(i>0)
    if(g[i][v]==0)
        print "未选第i项物品"
    else if(g[i][v]==1)
        print "选了第i项物品"
        v=v-c[i]

另外,采用方程的前一项或后一项也可以在输出方案的过程中根据f[i][v]的值实时地求出来,也即不须纪录g数组,将上述代码中的g[i] [v]==0改成f[i][v]==f[i-1][v],g[i][v]==1改成f[i][v]==f[i-1][v-c[i]]+w[i]也可。

输出字典序最小的最优方案

这里“字典序最小”的意思是1..N号物品的选择方案排列出来以后字典序最小。以输出01背包最小字典序的方案为例。

一般而言,求一个字典序最小的最优方案,只需要在转移时注意策略。首先,子问题的定义要略改一些。我们注意到,如果存在一个选了物品1的最优方案, 那么答案一定包含物品1,原问题转化为一个背包容量为v-c[1],物品为2..N的子问题。反之,如果答案不包含物品1,则转化成背包容量仍为V,物品为2..N的子问题。不管答案怎样,子问题的物品都是以i..N而非前所述的1..i的形式来定义的,所以状态的定义和转移方程都需要改一下。但也许更简 易的方法是先把物品逆序排列一下,以下按物品已被逆序排列来叙述。

在这种情况下,可以按照前面经典的状态转移方程来求值,只是输出方案的时候要注意:从N到1输入时,如果f[i][v]==f[i-1][i-v]及f[i][v]==f[i-1][f-c[i]]+w[i]同时成立,应该按照后者(即选择了物品i)来输出方案。

求方案总数

对于一个给定了背包容量、物品费用、物品间相互关系(分组、依赖等)的背包问题,除了再给定每个物品的价值后求可得到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的方案总数。

对于这类改变问法的问题,一般只需将状态转移方程中的max改成sum即可。例如若每件物品均是完全背包中的物品,转移方程即为

f[i][v]=sum{f[i-1][v],f[i][v-c[i]]}

初始条件f[0][0]=1。

事实上,这样做可行的原因在于状态转移方程已经考察了所有可能的背包组成方案。

经典命题:http://acm.hdu.edu.cn/showproblem.php?pid=1284

AC代码:

#include<iostream>
using namespace std;

int f[33333],val[4]={0,1,2,3};

int sum(int a,int b){
    return a+b;
}

int main()
{
    int i,j,n;
    f[0]=1;
    for(i=1;i<=3;i++)
        for(j=i;j<32768;j++)
            f[j]=sum(f[j],f[j-val[i]]);//可以直接写成f[j]+=f[j-val[i]],这样写只是突出背包的思想;
    while(scanf("%d",&n)==1)
        printf("%d\n",f[n]);
    return 0;
}

最优方案的总数

这里的最优方案是指物品总价值最大的方案。以01背包为例。

结合求最大总价值和方案总数两个问题的思路,最优方案的总数可以这样求:f[i][v]意义同前述,g[i][v]表示这个子问题的最优方案的总数,则在求f[i][v]的同时求g[i][v]的伪代码如下:

for i=1..N
   for v=0..V
        f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}
        g[i][v]=0
        if(f[i][v]==f[i-1][v])
            inc(g[i][v],g[i-1][v])
        if(f[i][v]==f[i-1][v-c[i]]+w[i])
            inc(g[i][v],g[i-1][v-c[i]])

如果你是第一次看到这样的问题,请仔细体会下面的伪代码。

c++代码如下:

fill(kry, kry+maxm, INF );    
memset(dp, 0, sizeof(dp));  
for( int i=1; i<=n; i++ )  
{  
    for( int j=m; j>=w[i]; j-- )  
    {  
        if( dp[j]<dp[j-w[i]]+1)  
        {  
            dp[j] = dp[j-w[i]] + 1;  
            kry[j] = kry[j-w[i]];  
        }  
        else if( dp[j]==dp[j-w[i]]+1)  
            kry[j] = kry[j] + kry[j-w[i]];  
    }  
} 

经典命题:http://acm.hdu.edu.cn/showproblem.php?pid=2955

AC代码:

#include <iostream>  
using namespace std;  
struct Bank   
{  
    int money;  
    double pro;  
};  
  
double max(double a,double b)  
{  
    double c =a>b?a:b;  
    return c;  
}  
  
double p=0;  
double f[11000];  
  
Bank bank[111];   
int main()  
{  
    int n,N,sum,m;  
  
      
    scanf("%d",&n);  
      
        while (n--)  
        {  
            sum=0;  
            m=0;  
  
            scanf("%lf%d",&p,&N);  
            for (int i=0;i<N;i++)  
            {  
                scanf("%d%lf",&bank[i].money,&bank[i].pro);  
                sum+=bank[i].money;  
            }  
              
              
            int t=sum;  
            f[0]=1;  
            for (i=1;i<=sum;i++)  
                f[i]=-1;  
            for (int k=0;k<N;k++)  
            {  
                for (t=sum;t>=bank[k].money;t--)  
                {  
                    if (f[t-bank[k].money]!=-1)  
                    {  
                        f[t]=max(f[t],f[t-bank[k].money]*(1-bank[k].pro));  
                    }                 
                    if (f[t]>=1-p&&t>m)m=t;  
                }  
            }  
            printf("%d/n",m);  
        }  
      
  
    return 0;  
}  


求次优解、第K优解

对于求次优解、第K优解类的问题,如果相应的最优解问题能写出状态转移方程、用动态规划解决,那么求次优解往往可以相同的复杂度解决,第K优解则比求最优解的复杂度上多一个系数K。

其基本思想是将每个状态都表示成有序队列,将状态转移方程中的max/min转化成有序队列的合并。这里仍然以01背包为例讲解一下。

首先看01背包求最优解的状态转移方程:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。如果要求第K优解,那么状态f[i][v]就应该是一个大小为K的数组f[i][v][1..K]。其中f[i][v][k]表示前i个物品、背包大小为 v时,第k优解的值。“f[i][v]是一个大小为K的数组”这一句,熟悉C语言的同学可能比较好理解,或者也可以简单地理解为在原来的方程中加了一维。 显然f[i][v][1..K]这K个数是由大到小排列的,所以我们把它认为是一个有序队列。

然后原方程就可以解释为:f[i][v]这个有序队列是由f[i-1][v]和f[i-1][v-c[i]]+w[i]这两个有序队列合并得到的。有序队列f[i-1][v]即f[i-1][v][1..K],f[i-1][v-c[i]]+w[i]则理解为在f[i-1][v-c[i]] [1..K]的每个数上加上w[i]后得到的有序队列。合并这两个有序队列并将结果的前K项储存到f[i][v][1..K]中的复杂度是O(K)。最后的答案是f[N][V][K]。总的复杂度是O(VNK)。

为什么这个方法正确呢?实际上,一个正确的状态转移方程的求解过程遍历了所有可用的策略,也就覆盖了问题的所有方案。只不过由于是求最优解,所以其 它在任何一个策略上达不到最优的方案都被忽略了。如果把每个状态表示成一个大小为K的数组,并在这个数组中有序的保存该状态可取到的前K个最优值。那么, 对于任两个状态的max运算等价于两个由大到小的有序队列的合并。

另外还要注意题目对于“第K优解”的定义,将策略不同但权值相同的两个方案是看作同一个解还是不同的解。如果是前者,则维护有序队列时要保证队列里的数没有重复的。

两种不同的K优

经典命题:http://acm.hdu.edu.cn/showproblem.php?pid=2639

AC代码:

#include<stdio.h> 
#include<string.h> 
#define max(a , b) a > b ? a : b 
int main() 
{ 
    int t; 
    int v[101], w[101]; 
    int a[33], b[33]; 
    int n, vol, i, j, k,m; 
    int x, y, z; 
    int dp[1011][30]; 
    scanf("%d", &t); 
    while(t--) 
    { 
 		scanf("%d%d%d", &n, &vol, &m); 
        for(i = 1 ; i <= n ; i++) scanf("%d", &v[i]); 
        for(i = 1 ; i <= n ; i++) scanf("%d", &w[i]); 
 		memset(dp , 0, sizeof(dp)); 
        memset(a , 0, sizeof(a)); 
        memset(b , 0, sizeof(b)); 
        for(i = 1 ; i <= n ; i++) 
        for(j = vol ; j >= w[i] ; j--) 
        { 
            for (k = 1; k <= m; k++) 
            { 
                a[k] = dp[j-w[i]][k] + v[i]; 
                b[k] = dp[j][k]; 
            } 
            x = 1;y = 1;z = 1; 
            while( z <= m && ( x <= m || y <= m ) )//合并重新生成前k个最优解 
            { 
                if(a[x] > b[y]) dp[j][z] = a[x] , x++; 
                else 			dp[j][z] = b[y] , y++; 
                if(dp[j][z]!= dp[j][z-1]) z++; //K种解 
            } 
         }
 		printf("%d\n", dp[vol][m]); 
    } 
    return 0; 
} 


经典命题:https://vijos.org/p/1412

AC代码:

#include<stdio.h>
#include<cstring>
#include<iostream>
using namespace std;
int dp[5001][51];
void merge(int to,int from, int value, int size){
    int tmp[105];
    int i = 0, j = 0, k = 0;
    while(i < size && j < size)
    {
        if(dp[to][i] > dp[from][j]+value)
            tmp[k++] =dp[to][i++];
        else
            tmp[k++] =dp[from][j++]+value;
    }
    while(i < size) tmp[k++] = dp[to][i++];
    while(j < size) tmp[k++] = dp[from][j++]+value;
    for(i=0; i<size; i++)   dp[to][i] = tmp[i];
}
int main(){
    int k,v,n;
    int i,j;
    int a,b;
    cin>>k>>v>>n;
    memset(dp,-0x3f3f3f3f,sizeof(dp));
    dp[0][0]=0;
    for(i=0;i<n;i++)
    {
        cin>>a>>b;
        for(j=v; j>=a; j--)
            merge(j,j-a,b,k);
    }
    int ans=0;
    for(i=0;i<k&&dp[v][i]>=0;i++)
        ans+=dp[v][i];
    cout<<ans<<endl;
    return 0;
}

背包中必要的物品

传送门:http://bailian.openjudge.cn/practice/4120/

#include <stdio.h>
#include <string.h>
#include<iostream>
#include<algorithm>
using namespace std;

const int N = 10000+7;
int a[255],ans[255];
int f[N]={0};
int n;
int calc(int x,int y)
{
    if(x<0) return 0;
    else return f[x]-calc(x-y,y);
}
int main()
{
    int t,x,y;
    f[0]=1;
    cin>>n>>x;
    for(int i=0;i<n;i++) cin>>a[i];
    
    for(int i=0;i<n;i++)
        for(int j=x;j>=a[i];j--)
            f[j]+=f[j-a[i]];
    
    int top=0;
    for(int i=0;i<n;i++)   if(!(f[x]-calc(x-a[i],a[i]))) ans[top++]=a[i];
    cout<<top<<endl;
    for(int i=0;i<top;i++) cout<<ans[i]<<" ";cout<<endl;
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值