背包九讲学习ing

正在学习dp,持续更新背包九讲,记录每次学习的内容,希望对于你也有帮助。
若有错误,欢迎指正!!!
如果对于我所讲解的问题有不懂的或者模糊的,欢迎在评论区评论,我定会在博客中更新讲解。
一、01背包
这是最基础的一类背包问题:
有n件物品,容量为m的背包,每一件物品都有自己的容量c【i】和价值w【i】,问你能装下最大的价值之和?
对于初学者来说,dp算法还是较难理解的,什么是状态转移方程,他又怎么能够确保一定最优解呢?
我就01背包来说说我的看法(先不考虑空间优化):
对于这道题目来说,
状态转移方程就是:dp【i】【j】=max(dp【i-1】【j】,dp【i-1】【j-c【i】】+w【i】)。
对于这个方程来说,
首先,dp数组表示背包容量为j时,考虑过前i个物品后,我的背包里能装下的最大价值。
其次,max里的两个参数分别含义是:1.我不拿第i件物品,那么我的此时能装下的最大价值之和就是前i-1个物品中最大价值之和。2.若是我拿了第i件物品,那么我此时能装下的最大价值之和就是前i-1个物品装在j-c【i】的空间下的最大价值之和加上第i件物品的价值w【i】。
拿不拿这个决策取决于max里两个参数的大小。
最终,既然你是要输出最大的价值之和,那么当然要等到遍历完所有物品和容量才行,即输出dp【n】【m】。

那么这个状态转移方程应该怎么用呢?
为了解决第二个问题(保证当前状态下的最优解),我们必须要遍历完所有的物品和尽量使用完所有的容量。就需要遍历。

    for(int i=1;i<=n;i++)//遍历所有的物品
        for(int j=1;j<=v;j++)//对于每一个物品所有容量情况下的最大价值之和
        {
            if(j-c[i]<0){dp[i][j]=dp[i-1][j];continue;}//防止数组越界
            dp[i][j]=maxi(dp[i-1][j],dp[i-1][j-c[i]]+w[i]);
        }

由代码我们可以看到要加上一个判断条件,就是防止数组越界。因为如果我当前的容量小于物品所需容量,那么是肯定拿不了的,所以我们可以直接让他等于前i-1个物品在当前容量上的最大价值之和。
附上完整代码:

#include <iostream>
#include <cstring>
using namespace std;
int v,n;//0<=n<=10000;0<v<10000
int c[10005],w[10005];
int dp[10005][10005];
int maxi(int a,int b)
{
    return a>b?a:b;
}
int main()
{
    while(cin>>n>>v)
    {
        if(v==0&&n==0)break;
        memset(c,0,sizeof(c));
        memset(w,0,sizeof(w));
        memset(dp,0,sizeof(dp));
        for(int i=1;i<=n;i++)
        {
            cin>>c[i]>>w[i];
        }
        for(int i=1;i<=n;i++)
            for(int j=0;j<=v;j++)
            {
                if(j-c[i]<0){dp[i][j]=dp[i-1][j];continue;}
                dp[i][j]=maxi(dp[i-1][j],dp[i-1][j-c[i]]+w[i]);
            }
        /*可供于测试dp的整个过程
        for(int i=1;i<=n;i++)
            {for(int j=0;j<=v;j++)
                cout<<dp[i][j]<<" ";
                cout<<endl;
            }
         */
        cout <<dp[n][v]<< endl;
    }
    return 0;
}

接下来说一下关于空间优化的问题。
我们知道上面代码的时间复杂度和空间复杂度都是O(n*m)。由于肯定要遍历所有的i和j,所以若是不改变算法的话时间复杂度是肯定不会降低的,我们所能优化的也就只有空间复杂度了。
那么为什么能优化空间复杂度,并且怎么优化呢?
我们不难发现i在这里只是起着遍历所有物品的作用,不像j还会向前j-c【i】延伸,也就是一旦这个i便利过了我只有在i+1的时候才会用到其他时间都不会用到,所以i的用处并不大,可以优化掉,这样空间复杂度就降到了O(m)了。
那么应该怎么优化呢?(将dp【i】【j】变成dp【j】)
我们知道如果j小于c【i】则dp【i】【j】=dp【i-1】【j】,所以对于j小于c【i】我们不需要考虑,dp【j】不管就不会变化,达到同样的效果。而为了使用dp【i-1】【j-c【i】】我们可以从后向前遍历j(由m到c【i】),这样前i-1件物品的dp【j】就不会发生变化,相当于以前的dp【i-1】【j-c【i】】。
优化后的状态转移方程:dp【j】=max(dp【j】,dp【j-c【i】】+w【i】);
注:还可以稍稍减少空间占用的地方就是c【i】和w【i】了,因为我们不需要储存c和w这两个数组,我们每一次只用到了c【i】和w【i】,用完就不用了所以可以每次输入时就进行dp,就可以省两个数组的空间了。
附上优化空间后的代码:

#include <iostream>
#include <cstring>
using namespace std;
int n,m,c,w,dp[12885];

int main()
{
    while(cin>>n>>m)
    {
    memset(dp,0,sizeof(dp));//初始化为0
    for(int i=1;i<=n;i++)
    {
        cin>>c>>w;
        for(int j=m;j>=c;j--)
        {
            if(dp[j]<(dp[j-c]+w))
            dp[j]=dp[j-c]+w;
        }
    }
    /*测试过程
    for(int i=0;i<=m;i++)cout<<dp[i]<<" ";
    cout<<endl;
    */
    cout<<dp[m]<<endl;
    }
    return 0;
}

二、完全背包
这个是在01背包的基础上变形的一类背包问题:
同样是有一个容量为m的背包,每一种物品的费用是c【i】,价值为w【i】,不同的是每一种物品不再只是一件,而是有无穷多件,问能装下大的最大价值?
既然都已经说了是在01背包的基础上变形的,那么他和01背包的解法肯定也具有一定的相似性。与01背包唯一不同的就是同一件物品可以选用多次。
先说一下这道题目的状态转移方程:dp【j】=max(dp【j】,dp【j-c【i】】+w【i】);
你若仔细观察,你会发现他和01背包的状态转移方程差不多。那么大家心里就会有疑问了,为什么不同的问题会有相同的状态转移方程?(好奇心马上就蹦了出来)
答案就是虽然两者状态转移方程相同,但对于他的处理方式却不同。01背包的遍历方式是i(0-n),j(m-c【i】);而完全背包的j则是从c【i】到m。正是这小小一点的不同,就处理了两个不同的问题。
下面我来解释一下为什么换一下遍历顺序结果就会不一样:
首先,在01背包上我们将j由m到c【i】(暂称逆循环)的循环目的是为了利用i-1的dp【j-c【i】】,所以不能改变之前j之前的数组。
那么完全背包为什么又要正着循环呢,因为每一个物品我们都能够选择无限次,而且我们不再需要看他的上一件物品的dp【j-c【i】】。为了更好的理解,我先来模拟一下过程解释一下为什么能正循环。
对于每一个物品i,我们都要对其从c【i】循环到m,此时的dp【j】依旧是在与是否那这一件物品之间进行比较,若拿则是dp【j-c【i】】+w【i】,不拿则不变。所以说如果这个物品很好,我选择拿了,那么j从c【i】到m我就可能会拿多次这件物品,也就是对于这个物品i来说,在所有的容量j中都已经达到了最优的选择,那么我在下一件物品i+1时就不会再次考虑第i件物品了。之后继续在下一次循环中找到每一种容量下我是否要拿第i+1件物品了。
总而言之,就是每一次循环完j后,对于第i件物品我都已经做出了最优的选择。
刚才解释了为什么不需要考虑前i-1件物品了,接着说为什么要正循环?
对于第i件物品,我先假设他就是最好的物品,我全部装他会收获最大价值,那么你就要在dp【c【i】*{1~(m/c【i】)}】------ //注解:(m/c【i】)是我这个背包能装下第i件物品的数量//-------- 的时候都要选择这件物品,而你每一次判断都是在dp【j】与dp【j-c【i】】之间进行比较,所以说我需要将前面我已经选择了这件物品之后的dp数组更新再判断,所以需要正循环j。
//开始写了更多的注解,结果发现语文不好描述不清,想要更好的理解,就拿张纸,在纸上模拟一次j的循环,自然会理解。
注意:这道题和01背包一样可以省略掉c【】,w【】数组的空间,虽然并无大碍,但能省即省吧!
附上完全背包代码:

#include <iostream>
#include <cstring>
using namespace std;
int n,m,c,w;
int dp[10005];
int main()
{
    while(cin>>n>>m)
    {
        memset(dp,0,sizeof(dp));//初始化为0
        for(int i=0;i<n;i++)
        {
            cin>>c>>w;
            for(int j=c;j<=m;j++)
            {
                if(dp[j]<dp[j-c]+w)
                    dp[j]=dp[j-c]+w;
            }
        }
        cout<<dp[m]<<endl;
    }
    return 0;
}

对于前两道题目的感触:不要死记硬背,要多去理解,理解一下计算机的处理方式,多创新多钻研!!!
语文功底不好,不敢多语,若有讲解不清,可评论留言。

三、多重背包
问题:
与前两道题类似,给你一个容量为m的背包,有n种物品,每一个的占用容量为c【i】,价值为w【i】,但是这种物品一共有num【i】件。与前两道题不同的就在于他的数量可能不止一个,但也不是无穷个。
当我第一次看到这个题目时,我觉得难度突然上了一个档次,觉得对于每一种物品都要考虑他的个数,不能按照统一思想去解决,就觉得很难。但当你静下心仔细想想之后发现,其实很简单,甚至都不用编写新函数。
是不是有点惊讶?明明比前两道题目复杂怎么还说简单了,其实这就考验了你对于前两种背包的理解了。
01背包:只对于一件物品进行选择。完全背包:对相同的物品选择x件。(x在0~+00)
多重背包:对相同的物品选择x件。(x在0~num);是不是发现了其中的相似处。
下面说一下解法:
我们可以将多重背包分解成为01背包和完全背包;可以分解成01背包是毋庸置疑的(因为可以想象成更多件相同的物品罢了),那么怎样才能分解为完全背包呢?想一想如果这件物品的总重量之和大于背包容量m,那么对于背包来说是不是和无穷件物品一样,此时就可以把它当作完全背包来处理。你这样写出代码后会发现出现TLE的问题,是因为你转化为01背包的时间复杂度为O(n∑(num【i】)),所以你要优化,我们可以将num件物品分别分成1、2、4、8、…2的k次幂件物品作为单独的一件物品处理。这样你的代码时间复杂度就降到了O(n∑log(num【i】))。
总结一下思想:
这道题主要是考验对于前两种背包的理解和熟练应用。还有就是对于01背包问题的优化处理,把num分解成不同2k次幂件物品,大大降低了时间复杂度。
注:这道题使用的依旧是一维的dp数组,依旧不需要设置c【i】,w【i】,num【i】三个数组,可以减少一点空间占用。
附上最终代码:

#include <iostream>
#include <cstring>
using namespace std;
int c,w,num,n,m,dp[10005];
int maxi(int a,int b)
{
    return a>b?a:b;
}
void onepack(int c,int w)//01背包
{
    for(int i=m;i>=c;i--)
        dp[i]=maxi(dp[i],dp[i-c]+w);
}
void compack(int c,int w)//完全背包
{
    for(int i=c;i<=m;i++)
        dp[i]=maxi(dp[i],dp[i-c]+w);
}
int main()
{
    while(cin>>n>>m)
    {
        memset(dp,0,sizeof(dp));//初始化为0
        for(int i=0;i<n;i++)
        {
            cin>>c>>w>>num;
            if(num*c>m)//若num件物品总占容量超过m,背包装不下则相当于无穷件i件物品,等同于完全背包
                compack(c,w);
            else//反之,若小于m,将num件物品分成很多个不同容量的物品则相当于01背包
            {
                int k=1;
                while(k<num)
                {
                    onepack(k*c,k*w);
                    num-=k;
                    k*=2;//k=1、2、4、8...
                }
                onepack(num*c,num*w);//剩下的所有再次当作一个01背包处理
            }
        }
        cout<<dp[m]<<endl;
    }
    return 0;
}

四、混合三种背包
问题:
同样是n种物品,容量为m的背包,每一个有自己的占容量c【i】和价值w【i】,但是每一件可能只能拿一件也可能无限件,也可能有上限。
相信看过前三种背包问题的人都有感触,这道题目和多重背包很像,就是由三种背包混合在一起。其实就是一个判断,具体的不多讲,详细的全部在前三个背包讲解里面。
直接附上代码吧:

#include <iostream>
#include <cstring>
using namespace std;
int c,w,num,n,m,dp[10005];
int maxi(int a,int b)
{
    return a>b?a:b;
}
void onepack(int c,int w)//01背包
{
    for(int i=m;i>=c;i--)
        dp[i]=maxi(dp[i],dp[i-c]+w);
}
void compack(int c,int w)//完全背包
{
    for(int i=c;i<=m;i++)
        dp[i]=maxi(dp[i],dp[i-c]+w);
}
void mulpack(int c,int w,int num)
{
    if(num*c>m)//若num件物品总占容量超过m,背包装不下则相当于无穷件i件物品,等同于完全背包
                compack(c,w);
    else//反之,若小于m,将num件物品分成很多个不同容量的物品则相当于01背包
    {
        int k=1;
        while(k<num)
        {
            onepack(k*c,k*w);
            num-=k;
            k*=2;//k=1、2、4、8...
        }
        onepack(num*c,num*w);//剩下的所有再次当作一个01背包处理
    }
}
int main()
{
    while(cin>>n>>m)
    {
        memset(dp,0,sizeof(dp));//初始化为0
        for(int i=0;i<n;i++)
        {
            cin>>c>>w>>num;//由于没有具体题目,所以令num=-1为无穷件
            if(num==1)
                onepack(c,w);
            else if(num==-1)
                compack(c,w);
            else
                mulpack(c,w,num);
        }
        cout<<dp[m]<<endl;
    }
    return 0;
}

五、二维背包问题
问题:依旧是n种物品,不同的是你有一个容量为m的背包和p的钱,每一件物品有自己的所占容量c【i】和价格d【i】以及自己的价值w【i】。
看到这里,大家都会发现背包问题都是一级一级的往上加的。
接下来说说我对于这道题目的理解:
这道题目,其实就是相当于多了一个因素要考虑,就是他的价格。因为容量和价格之间没有相互影响的因素,所以我们可以把它分开考虑。这里的分开考虑只是想说价格与容量之间没有联系,只需在将dp数组上加一维。状态转移方程是:dp【j】【k】=max(dp【j】【k】,dp【j-c【i】】【k-d【i】】+w【i】)
理解之后发现处理方式就是和01背包和完全背包一样。
只不过需要多一层循环k,这里当然也是为了遍历完所有的j和k。
最后附上代码:

/*
代码没有实际测试数据,所以最终是否能ac尚不可知,但思想肯定就是这样的。
*/
#include <iostream>
#include <cstring>
using namespace std;
int c,d,w,num,n,m,p,dp[10005][10005];
int maxi(int a,int b)
{
    return a>b?a:b;
}
void onepack(int c,int d,int w)//01背包
{
    for(int i=m;i>=c;i--)
        for(int j=p;j>=d;j--)
        dp[i][j]=maxi(dp[i][j],dp[i-c][j-d]+w);
}
void compack(int c,int d,int w)//完全背包
{
    for(int i=c;i<=m;i++)
        for(int j=d;j<m;j++)
        dp[i][j]=maxi(dp[i][j],dp[i-c][j-d]+w);
}
void mulpack(int c,int d,int w,int num)
{
    if(num*c>m||num*d>p)//若num件物品总占容量超过m,或者nun件物品钱数大于你带的钱,背包装不下则相当于无穷件i件物品,等同于完全背包
                compack(c,d,w);
    else//反之,若小于m,将num件物品分成很多个不同容量的物品则相当于01背包
    {
        int k=1;
        while(k<num)
        {
            onepack(k*c,k*d,k*w);
            num-=k;
            k*=2;//k=1、2、4、8...
        }
        onepack(num*c,num*d,num*w);//剩下的所有再次当作一个01背包处理
    }
}
int main()
{
    while(cin>>n>>m>>p)
    {
        memset(dp,0,sizeof(dp));//初始化为0
        for(int i=0;i<n;i++)
        {
            cin>>c>>d>>w>>num;//由于没有具体题目,所以令num=-1为无穷件
            if(num==1)
                onepack(c,d,w);
            else if(num==-1)
                compack(c,d,w);
            else
                mulpack(c,d,w,num);
        }
        cout<<dp[m][p]<<endl;
    }
    return 0;
}

其实在更高维的问题中也是一样的逻辑,只要各种因素之间没有关系,你就可以通过增加维数来解决问题。
若有错误,欢迎指正!
----持续更新,喜爱可关注哦------

猜你喜欢

转载自blog.csdn.net/wjl_zyl_1314/article/details/82664877