背包小结

本文深入解析了01背包、完全背包及多重背包问题的基本概念、状态转移方程与算法实现细节,通过具体实例帮助读者理解并掌握各类背包问题的解决方法。

先来说一下理论部分:

一.01背包问题

题目

有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。

基本思路

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。

用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:

f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下:“将前i件物品放入容量为v的背包中”这个子问题,

若只考虑第i件物品的策略(放或不放),那么就可以转化为一个只牵扯前i-1件物品的问题。如果不放第i件物品,那么问题就转化为“前i-1件物品放入容量为v的

背包中”,价值为f[i-1][v];如果放第i件物品,那么问题就转化为“前i-1件物品放入剩下的容量为v-c[i]的背包中”,此时能获得的最大价值就是f[i-1][v-

c[i]]再加上通过放入第i件物品获得的价值w[i]。


我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种

问法的实现方法是在初始化的时候有所不同。

如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。

如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。

为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的

nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都

不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。

这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。


二.完全背包

题目

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

基本思路

这个问题非常类似于01背包问题,所不同的是每种物品有无限件。也就是从每种物品的角度考虑,与它相关的策略已并非取或不取两种,而是有取0件、取1件、取2件……等很多种。如果仍然按照解01背包时的思路,令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值。仍然可以按照每种物品不同的策略写出状态转移方程,像这样:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}

既然01背包问题是最基本的背包问题,那么我们可以考虑把完全背包问题转化为01背包问题来解。最简单的想法是,考虑到第i种物品最多选V/c[i]件,于是可以把第i种物品转化为V/c[i]件费用及价值均不变的物品,然后求解这个01背包问题。这样完全没有改进基本思路的时间复杂度,但这毕竟给了我们将完全背包问题转化为01背包问题的思路:将一种物品拆成多件物品。

更高效的转化方法是:把第i种物品拆成费用为c[i]*2^k、价值为w[i]*2^k的若干件物品,其中k满足c[i]*2^k<=V。这是二进制的思想,因为不管最优策略选几件第i种物品,总可以表示成若干个2^k件物品的和。这样把每种物品拆成O(logV/c[i])件物品,是一个很大的改进。

但我们有更优的O(VN)的算法。

O(VN)的算法

这个算法使用一维数组,先看伪代码:

for i=1..N

    for v=0..V

        f[v]=max{f[v],f[v-cost]+weight}

P01的伪代码

for i=1..N

    for v=V..0

        f[v]=max{f[v],f[v-c[i]]+w[i]};

你会发现,这个伪代码与P01的伪代码只有v的循环次序不同而已。为什么这样一改就可行呢?首先想想为什么P01中要按照v=V..0的逆序来循环。这是因为要保证

第i次循环中的状态f[i][v]是由状态f[i-1][v-c[i]]递推而来。换句话说,这正是为了保证每件物品只选一次,保证在考虑“选入第i件物品”这件策略时,依据的是一个绝

无已经选入第i件物品的子结果f[i-1][v-c[i]]。而现在完全背包的特点恰是每种物品可选无限件,所以在考虑“加选一件第i种物品”这种策略时,却正需要一个可能已选

入第i种物品的子结果f[i][v-c[i]],所以就可以并且必须采用v=0..V的顺序循环。这就是这个简单的程序为何成立的道理。

值得一提的是,上面的伪代码中两层for循环的次序可以颠倒。这个结论有可能会带来算法时间常数上的优化。

三.多重背包

题目

有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最

大。

基本算法

这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取n[i]件。令f[i][v]表示前i种物品恰

放入一个容量为v的背包的最大权值,则有状态转移方程:

f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}

复杂度是O(V*Σn[i])。

转化为01背包问题

另一种好想好写的基本方法是转化为01背包求解:把第i种物品换成n[i]件01背包中的物品,则得到了物品数为Σn[i]的01背包问题,直接求解,复杂度仍然是O(V*Σn[i])。

但是我们期望将它转化为01背包问题之后能够像完全背包一样降低复杂度。仍然考虑二进制的思想,我们考虑把第i种物品换成若干件物品,使得原问题中第i种物品可取的每种

策略——取0..n[i]件——均能等价于取若干件代换以后的物品。另外,取超过n[i]件的策略必不能出现。

方法是:将第i种物品分成若干件物品,其中每件物品有一个系数,这件物品的费用和价值均是原来的费用和价值乘以这个系数。使这些系数分别为1,2,4,...,2^(k-1),n[i]-2^k+1,

且k是满足n[i]-2^k+1>0的最大整数。例如,如果n[i]为13,就将这种物品分成系数分别为1,2,4,6的四件物品。

分成的这几件物品的系数和为n[i],表明不可能取多于n[i]件的第i种物品。另外这种方法也能保证对于0..n[i]间的每一个整数,均可以用若干个系数的和表示,这个证明可以分

0..2^k-1和2^k..n[i]两段来分别讨论得出,并不难,希望你自己思考尝试一下。

这样就将第i种物品分成了O(log n[i])种物品,将原问题转化为了复杂度为<math>O(V*Σlogn[i])的01背包问题,是很大的改进。


具体例子:

整数划分(三)

时间限制: 1000 ms  |  内存限制: 65535 KB
难度: 5
描述

整数划分是一个经典的问题。请写一个程序,完成以下要求。

 

输入
每组输入是两个整数n和k。(1 <= n <= 50, 1 <= k <= n)
输出
对于输入的 n,k;
第一行: 将n划分成若干正整数之和的划分数。
第二行: 将n划分成k个正整数之和的划分数。
第三行: 将n划分成最大数不超过k的划分数。
第四行: 将n划分成若干个 奇正整数之和的划分数。
第五行: 将n划分成若干不同整数之和的划分数。
第六行: 打印一个空行
样例输入
5 2
样例输出
7
2
3
3
3
提示
样例输出提示:
1.将5划分成若干正整数之和的划分为: 5, 4+1, 3+2, 3+1+1, 2+2+1, 2+1+1+1, 1+1+1+1+1
2.将5划分成2个正整数之和的划分为: 3+2, 4+1
3.将5划分成最大数不超过2的划分为: 1+1+1+1+1, 1+1+1+2, 1+2+2
4.将5划分成若干 奇正整数之和的划分为: 5, 1+1+3, 1+1+1+1+1
5.将5划分成若干不同整数之和的划分为: 5, 1+4, 2+3

//1.将n划分成若干正整数之和的划分数
//2.将n划分成k个正整数之和的划分数
//3.将n划分成最大数不超过k的划分数
//4.将n划分成若干个奇正整数之和的划分数
//5.将n划分成若干不同正整数之和的划分数
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int n,k;
int f1()
{//01背包
    int dp[400]={0};
    int i,j;
    dp[0]=1;
    for(i=1; i<=n; i++)
        for(j=i; j<=n ; j++)
        {
            dp[j]=dp[j]+dp[j-i];
        }
    return dp[n];
}
int f2(int n,int k)
{//递归
    if(k==1||n==k)
        return 1;
    if(n<k)
        return 0;
    return f2(n-1,k-1)+f2(n-k,k);
}
int f3()
{
    int dp[400]={0};
    int i,j;
    dp[0]=1;
    for(i=1; i<=k; i++)
        for(j=i; j<=n ; j++)
        {
            dp[j]=dp[j]+dp[j-i];
        }
    return dp[n];
}
int f4()
{
    int dp[400]={0};
    int i,j;
    dp[0]=1;
    for(i=1; i<=n; i+=2)
        for(j=i; j<=n ; j++)
        {
            dp[j]=dp[j]+dp[j-i];
        }
    return dp[n];
}
int f5()
{//完全背包
    int dp[400]={0};
    int i,j;
    dp[0]=1;
    for(i=1; i<=n; i++)
        for(j=n; j>=i ; j--)
        {
            dp[j]=dp[j]+dp[j-i];
        }
    return dp[n];
}
int main()
{
    while(~scanf("%d %d",&n,&k))
    {

        printf("%d\n",f1());
        printf("%d\n",f2(n,k));
        printf("%d\n",f3());
        printf("%d\n",f4());
        printf("%d\n",f5());
    }
    return 0;
}
这里有一个整数划分的总结,可以看一下。

整数划分


苹果

时间限制: 3000 ms  |  内存限制: 65535 KB
难度: 3
描述

ctest有n个苹果,要将它放入容量为v的背包。给出第i个苹果的大小和价钱,求出能放入背包的苹果的总价钱最大值。


输入
有多组测试数据,每组测试数据第一行为2个正整数,分别代表苹果的个数n和背包的容量v,n、v同时为0时结束测试,此时不输出。接下来的n行,每行2个正整数,用空格隔开,分别代表苹果的大小c和价钱w。所有输入数字的范围大于等于0,小于等于1000。
输出
对每组测试数据输出一个整数,代表能放入背包的苹果的总价值。
样例输入
3 3
1 1
2 1
3 1
0 0
样例输出
2
典型的01背包
#include<stdio.h>
#include<algorithm>
using namespace std;
int main()
{
    int n,v;
    while(scanf("%d %d",&n,&v),n!=0||v!=0)
    {
        int i,j;
        int a[1005],b[1005];
        int dp[1005]={0};
        for(i=0;i<n;i++)
            scanf("%d %d",&a[i],&b[i]);
        for(i=0;i<n;i++)
            for(j=v;j>=a[i];j--)
            dp[j]=max(dp[j],dp[j-a[i]]+b[i]);
        printf("%d\n",dp[v]);
    }
    return 0;
}

2126: tmk买礼物

Time Limit: 1 Sec   Memory Limit: 128 MB
Submit: 339   Solved: 109

Submit Status Web Board

Description

明天是校赛的日子,为了庆祝这么喜庆的日子,TMK打算买些礼物给女票LSH庆祝一下。
  TMK进入了雪梨超市,然后刚踏入的一瞬间,店主就对TMK说:“恭喜你成为了本店第2147483647位顾客,本店在搞一个活动,对本店第2147483647位顾客进行赠送活动。你先看看你有多少钱?”
  TMK一摸口袋,发现只有n个硬币,每个硬币的价值为a[i]。
  然后店主继续说:“现在你用你的钱凑一些数,如果你的钱能凑成[0,x]里面所有的数,那么你将会免费获得该店价值x元的代金券,假设你有四个硬币面值分别为1,2,4,100,你就可以凑成[0,7]里面所有的数,我们将会送你7元的代金券。现在就用你的硬币来试试吧。Enjoy yourself!”
  在TMK努力凑钱的时候,店主想知道他要送多少代金券给TMK。

Input

第一行一个整数T,表示数据组数。

对于每组数据,首先读入一个整数n(n<=100000),然后接下来的一行有n个整数,表示a[i] (0<a[i]<=1e9)

Output

对于每个数据,输出一个整数x,表示店主要送x元的代金券给TMK

Sample Input

131 2 3

Sample Output

6

HINT




看似是要用背包写,但是数据太大,用背包可能会超,聪明的队友直接把规律找出来了,那就看代码自己悟
最后输出的数据是long long int型 主要是这段代码:            for(i=1; i<t; i++)
           {
                if(s>=a[i]-1)
                    s+=a[i];
                else
                    break;
            }
#include<stdio.h>
#include<algorithm>
using namespace std;
int a[105000];
int main()
{
    int n;
    scanf("%d",&n);
    while(n--)
    {
        int i,j,t;
        long long int s=0;
        scanf("%d",&t);
        for(i=0;i<t;i++)
          scanf("%d",&a[i]);
        sort(a,a+t);
        if(a[0]>1)
            printf("0\n");
        else
        {
            s=a[0];
            for(i=1; i<t; i++)
            {
                if(s>=a[i]-1)
                    s+=a[i];
                else
                    break;
            }
            printf("%lld\n",s);
        }
    }
    return 0;
}
Cash Machine
Time Limit: 1000MS Memory Limit: 10000K
Total Submissions: 35060 Accepted: 12716

Description

A Bank plans to install a machine for cash withdrawal. The machine is able to deliver appropriate @ bills for a requested cash amount. The machine uses exactly N distinct bill denominations, say Dk, k=1,N, and for each denomination Dk the machine has a supply of nk bills. For example, 

N=3, n1=10, D1=100, n2=4, D2=50, n3=5, D3=10 

means the machine has a supply of 10 bills of @100 each, 4 bills of @50 each, and 5 bills of @10 each. 

Call cash the requested amount of cash the machine should deliver and write a program that computes the maximum amount of cash less than or equal to cash that can be effectively delivered according to the available bill supply of the machine. 

Notes: 
@ is the symbol of the currency delivered by the machine. For instance, @ may stand for dollar, euro, pound etc. 

Input

The program input is from standard input. Each data set in the input stands for a particular transaction and has the format: 

cash N n1 D1 n2 D2 ... nN DN 

where 0 <= cash <= 100000 is the amount of cash requested, 0 <=N <= 10 is the number of bill denominations and 0 <= nk <= 1000 is the number of available bills for the Dk denomination, 1 <= Dk <= 1000, k=1,N. White spaces can occur freely between the numbers in the input. The input data are correct. 

Output

For each set of data the program prints the result to the standard output on a separate line as shown in the examples below. 

Sample Input

735 3  4 125  6 5  3 350
633 4  500 30  6 100  1 5  0 1
735 0
0 3  10 100  10 50  10 10

Sample Output

735
630
0
0


多重背包转01背包,同时用到了二进制优化
#include<stdio.h>
#include<algorithm>
using namespace std;
int main()
{
    int n,m;
    while(~scanf("%d %d",&n,&m))
    {
        int a[1001];
        int t=0,p,q,i,j,tt;
        for(i=0;i<m;i++)
        {
            int s1,v1;
            scanf("%d %d",&s1,&v1);
            for(j=1;s1>0;j=j*2)
            {
                if(s1>=j)
                    s1=s1-j,a[t]=j*v1,t++;
                else
                    a[t]=s1*v1,s1=0,t++;
            }
        }
        int b[100010]={0};
        int ma=0;
        for(int i=0;i<t;i++)
        {
            for(int j=n;j>=a[i];j--)
                b[j]=max(b[j],b[j-a[i]]+a[i]),ma=max(ma,b[j]);
        }
        printf("%d\n",ma);
    }
    return 0;
}

Dividing

                                                                       Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)
                                                                       Total Submission(s): 25726    Accepted Submission(s): 7358


Problem Description
Marsha and Bill own a collection of marbles. They want to split the collection among themselves so that both receive an equal share of the marbles. This would be easy if all the marbles had the same value, because then they could just split the collection in half. But unfortunately, some of the marbles are larger, or more beautiful than others. So, Marsha and Bill start by assigning a value, a natural number between one and six, to each marble. Now they want to divide the marbles so that each of them gets the same total value. 
Unfortunately, they realize that it might be impossible to divide the marbles in this way (even if the total value of all marbles is even). For example, if there are one marble of value 1, one of value 3 and two of value 4, then they cannot be split into sets of equal value. So, they ask you to write a program that checks whether there is a fair partition of the marbles.
 

Input
Each line in the input describes one collection of marbles to be divided. The lines consist of six non-negative integers n1, n2, ..., n6, where ni is the number of marbles of value i. So, the example from above would be described by the input-line ``1 0 1 2 0 0''. The maximum total number of marbles will be 20000. 

The last line of the input file will be ``0 0 0 0 0 0''; do not process this line.
 

Output
For each colletcion, output ``Collection #k:'', where k is the number of the test case, and then either ``Can be divided.'' or ``Can't be divided.''. 

Output a blank line after each test case.
 

Sample Input
      
1 0 1 2 0 0 1 0 0 0 1 1 0 0 0 0 0 0
 

Sample Output
      
Collection #1: Can't be divided. Collection #2: Can be divided.
 


 
思路:典型的多重背包,防止超时要进行一些必要的剪枝和二进制优化



我本来想先转化为01背包的,当然也用到了二进制优化,但是总是时间超限。
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
int dp[100000];
int main()
{
    int s[6],t=0;
    while(~scanf("%d %d %d %d %d %d",&s[0],&s[1],&s[2],&s[3],&s[4],&s[5]))
    {
        int ss=0,i,j,k,a[1005],cnt;
        for(i=0;i<6;i++)
            ss+=s[i];
        if(ss==0)
        break;
        ss=0;
        int m,tt=0,tlag=0;
        printf("Collection #%d:\n",++t);
        for(i=0;i<6;i++)
            ss+=s[i]*(i+1);
        if(ss%2!=0)
            printf("Can't be divided.\n\n");
        else
        {
            ss/=2;
            memset(dp,0,sizeof(dp));
            dp[0]=1;
            for(i=0;i<6;i++)
            {
                for(j=1;j<=s[i];j*=2)
                {
                    cnt=(i+1)*j;
                    for(k=ss;k>=cnt;k--)
                        if(dp[k-cnt]!=0)
                           dp[k]=1;
                    s[i]=s[i]-j;
                }
                cnt=s[i]*(i+1);
                if(cnt!=0)
                {
                    for(k=ss;k>=cnt;k--)
                    {
                        if(dp[k-cnt]!=0)
                            dp[k]=1;
                    }
                }
            }
            if(dp[ss]==1)
               printf("Can be divided.\n\n");
            else
               printf("Can't be divided.\n\n");
        }
    }
    return 0;
}









评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值