DP背包问题学习笔记及系列练习题

  01 背包:

    01背包:在M件物品中取出若干件物品放到背包中,每件物品对应的体积v1,v2,v3,....对应的价值为w1,w2,w3,,,,,每件物品最多拿一件。

    和很多DP题一样,对于每一个物品,都只有拿或者不拿这两种状态,不拿或者拿不动,dp[i][j]=dp[i-1][j],容量不变,而如果拿的话,为dp[i][j]=dp[i-1][j-w[i]]+v[i];所以总的来说:

dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i])

    在二维的写法中,dp[ i ] [ j ]表示拿前 i 件物品去塞容积为 j 的背包可以得到的最大价值,所以最后dp[ n ][ v ]就是所求问题的答案。b站某大佬做的动画演示填表过程

    要点全在动画演示里,要说这么解释,我觉得没什么必要,拿个例子,不要偷懒,拿笔把表填一下,绝对会豁然开朗!

    上acwing 板子裸题

    

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 1e3+10;
int dp[maxn][maxn];
int  w[maxn],v[maxn];
int n,s;
void ac ( )
{
     for(int i=1 ;i<= n;i++)
     {
         for(int j= 1 ;j<= s;j++    )
         {
             if(w[i]>j)  //放不下
             {
                 dp[i][j]=dp[i-1][j];
             }
             else
             {
                 dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
             }
         }
     }
}
int main()
{

     cin>>n>>s;
     for(int i =1 ; i <= n; i ++)
     {
         cin>>w[i]>>v[i];
     }
     ac();
     cout<<dp[n][s]<<endl;
}

    但是二维有时候会MLE,所以考虑用一个滚动数组来优化。观察DP式,我们发现,每一行只与上一行发生关系,之前的没必要存,浪费。所以我们用一个一维数组来记录上一次的状态,下一次直接进行比较并更新。为dp[ j ] = max(dp[j],dp[j-w[i]]+v[i]);

    但是要注意的是,一维的第二层for遍历和二维的顺序是不同的,二维:

        for(int j= 1 ;j<= s;j++    )
  一维:    for(int j = m;j>=w[i];j--)
    根据手推的结果(可以用这个:容量为10的背包,第一件物品所占空间为6,价值300.第二件:5,200 第三件:4,100)或者根据对二维的理解,如果j是正着来的话,会出现一个物品多次加的情况(01背包每个物品至多选一次),所以要倒着来。多次加,就成了完全背包问题。
  所谓完全背包,不同于01背包的是它的物品可以无限选多个,承接上面的,只需要改一下J的循环顺序就可以了)
  下面是ACWING的完全背包板子题:
   
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 1e5;
int dp[maxn];
int w[maxn],d[maxn];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i =1 ;i<=n;i++)
        cin>>w[i]>>d[i];
    for(int i =1 ;i<=n;i++)
    {
        for(int j = w[i];j<=m;j++)
            dp[j]=max(dp[j],dp[j-w[i]]+d[i]);
    }
    cout<<dp[m]<<endl;
}

  hdu1114

  完全背包恰好装满求最小值:

  给出n种硬币,以及该种硬币的价值和重量。求在已知重量的条件下求出这些硬币的一个组合,使得它们的价值之和最小。能装满,求最小值,否则impossible

  直接完全背包板子,但是由于是求最小值,所以式子变为dp[j]=min(dp[j],dp[j-d[i]]+w[i]);  但是由于是求得最小值,那么需要把dp初始化为无穷大,但是dp[0]=0;不这样的话,我们求的结果全是无穷大INF,不信的话可以手推一下。每次求都是min(inf,inf+价值)很明显不行。所以dp[0]=0。  

  结果是inf,说明装不满,否则,即为装满的最小值

  

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int inf = 9999999;
const int maxn = 1e5;
int dp[maxn];
int w[maxn],d[maxn];
int main()
{
     int t;
     scanf("%d",&t);
     while(t--)
     {
             
         int k1,k2;
         scanf("%d%d",&k1,&k2);
         int m=k2-k1;
         int n;
         scanf("%d",&n);
         for(int i=1;i<=n;i++ )
             cin>>w[i]>>d[i];
         for(int i =1 ;i <=  m; i++)    
              dp[i]=inf;
         dp[0]=0;
         for(int i = 1; i <= n; i++)
         {
             for(int j = d[i];j<=m;j++)
                 {
                     dp[j]=min(dp[j],dp[j-d[i]]+w[i]);
                }
        }
        if(dp[m]==inf)
            cout<<"This is impossible."<<endl;
        else
            printf("The minimum amount of money in the piggy-bank is %d.
",dp[m]);
     }
}
   01背包HDU2546(贪心+DP)
   中文题意,不再解释了。对于余额m,小于5时输出m。大于5时,需要用到贪心了。我们需要用余下的5元买最贵的菜,m=m-5,然后用减去5的m去买来获取最大的花钱数
最后输出m+5-dp[m]-d[n]  ,记得对d数组从小到大排一下序,然后套用01背包基本模板就好了。
  其实刚开始我在思考,01背包需要物品的重量与价值,但是这个题只有价格也就是所谓的重量啊,缺东西啊,但又仔细一想,这个价格同时是重量又是价值啊!
  
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
const int maxn  = 1005;
int dp[maxn];
int d[maxn];
using namespace std;
int main()
{
    int n;
    while(cin>>n)
    {
        memset(dp,0,sizeof(dp));  //记得初始化 
        if(n==0)
            break;
        for(int i= 1; i<=n;i++)
            cin>>d[i];
        int m;
        cin>>m;
        if(m<5)
            cout<<m<<endl;
        else
        {
            sort(d+1,d+1+n);
            m=m-5;
            for(int i = 1;i<n;i++)
                for(int j=m;j>=d[i];j--)
                    dp[j]=max(dp[j],dp[j-d[i]]+d[i]);
                    
            cout<<m+5-dp[m]-d[n]<<endl;
        }
    }
}
多重背包:要求的东西和01,完全背包一样,只是不同的是,每个物品有一定的数量限制,可多取但是在物品数量范围内。
只是在01背包基础上加了一个for(k),此为物品数量
ACWING 多重背包板子题
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn= 200;
int dp[maxn];
int v[maxn],w[maxn],s[maxn];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i= 1;i<=n;i++)
        cin>>v[i]>>w[i]>>s[i];
    for(int i=1;i<=n;i++)
        for(int j=m;j>=v[i];j--)
            for(int k=0;k<=s[i]&&k*v[i]<=j;k++)  //注意k*v[i]<=j
                dp[j]=max(dp[j],dp[j-k*v[i]]+k*w[i]);
        cout<<dp[m]<<endl;
}

   多重背包的二进制优化:

   HDU2844

  题意是:给定一些硬币价值和数量,问它能组合1到m元中的几种情况。dp后统计dp[i]==i的数目,即为答案

  如果按上题模板来写的话,会因为每种硬币c<=1000而超时。所以考虑优化。这里借鉴了博客 https://www.cnblogs.com/wsblm/p/10752252.html的代码。

  何为二进制优化,比如说,面值为1 的硬币20枚,那么完全背包的话需要20次转移。但是我们可以把它拆掉:面值为1的一枚,为2的一枚,依次是4,8。最后多的5,定为一枚。所以我们只需要5次转移就可以了。

  模板:

  

        for(int i = 0; i< n ;i++)
        {
            int k=1;
            int p=s[i];
            while(k<p)
            {
                x[tot]=v[i]*k;  //V数组为价值
                tot++;
                p-=k;
                k*=2;
            }
            x[tot++]=p*v[i];  //新数组记录新面值
        }
  接下来呢,我们就可以把他们看成01背包来做了,不得不说发明这些算法的人真牛*.
  
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
const int maxn = 1e5+10;
int dp[maxn];
int v[maxn],s[maxn],x[maxn];
int main()
{
    int n,m;
    while(scanf("%d%d",&n,&m))
    {
        if(n==0&&m==0)
            break;
    //    memset(dp,0,sizeof(dp));
        for(int i=0;i<=m;i++)
            dp[i]=0;
        for(int i=0;i<n;i++)    
            cin>>v[i];
        for(int i=0;i<n;i++)
            cin>>s[i];
        int tot=0;
        for(int i = 0; i< n ;i++)
        {
            int k=1;
            int p=s[i];
            while(k<p)
            {
                x[tot]=v[i]*k;
                tot++;
                p-=k;
                k*=2;
            }
            x[tot++]=p*v[i];
        }
        for(int i= 0;i<tot;i++)
            for(int j = m;j>=x[i];j--)
                dp[j]=max(dp[j],dp[j-x[i]]+x[i]);
        int ans=0;
        for(int i=1;i<=m;i++)
        {
            if(dp[i]==i)
                ans++;
        }
        cout<<ans<<endl;
    }
}

   01背包HDU1171

  这个题意其实本来我不太懂,以为相同的value不能放一块呢,但是看样例二又不对.....总的来说,就是给N种不同的设施,每一行输入设施的价值和数量。把所有设施分两组,但是
这两组的value要尽量接近。
 看上去给了数量,以为是一个多重背包,但是其实可转化为01背包来算的。我们把这个设施分开,就是比如 2 3 我们可以分成三个价值为2的设施来算。这样的话,这么多个设施,
dp一下背包容量为sum/2的就可以了,然后输出sum-dp[sum/2]和dp[sum/2] 。由于整形sum,sum/2的话一定小于等于sum-sum/2的(比如7/2=3,7-3>3)所以sum-dp[sum/2]一定比dp[sum/2]大

有几个细节:dp和v数组的初始化    

范围的选择:对dp数组,要开50*100*50/2

    

#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int maxn = 150009;
int dp[maxn];
int v[maxn];
void init()
{
    memset(dp,0,sizeof(dp));
    memset(v,0,sizeof(v));
}
int main()
{
    int n;
    while(~scanf("%d",&n)&&n>0)
    {
        int a,b;
        int sum=0;
        int tot=0;
        init();
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d",&a,&b);
            sum+=a*b;
            while(b--)
            {
                v[tot++]=a;
            }        
        }
        int all=sum;
        for(int i=0;i<tot;i++)
            for(int j = sum/2 ;j >=v[i];j--)
                dp[j]=max(dp[j],dp[j-v[i]]+v[i]);

            cout<<sum-dp[sum/2]<<" "<<dp[sum/2]<<endl;
    }
    return 0;
}
  

2019沈阳网络赛(C)Dawn-K's water完全背包

    

       一开始题意并没有理解细。给你n种类型的矿泉水,然后每种的价格+重量

    求在大于等于质量m下花钱的最小值。既然要求最小的dp值,那肯定是将模板改为dp=min()了。但是求完这个还不算完。因为题意要求,钱花的少,还要买得更多。

    这个怎么解决呢,看dp[s]=x  这个式子表明花x元买了s价值的东西。既然是要质量大于等于m,我们首先定当前质量为m,价值为dp[m],接下来在i > m遍历,如果出现了dp[i]<=dp[m],说明我们可以花更少的钱买更多的质量(i>=m),而且质量一定满足大于等于m,更新最大质量与钱,最后输出它们。

    一直WA一直WA,这题要注意范围,一个是dp,最大1e4。然后是初始化用到的最大值,我用1e8WA了,我也不清楚,然后学到了0x3f3f3f3f。以后设它为最大值就好了。当然1e9也可以过!

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn = 1e3+10;
const int inf = 0x3f3f3f3f; 
int v[maxn],w[maxn];
int dp[10000+10];
int main()
{
    int n ,m ;
    while(cin>>n>>m)
    {
        for(int i = 1;i<=n;i++)
            cin>>v[i]>>w[i];
        for(int i=1;i<=10010;i++)
            dp[i]=inf;
        dp[0]=0;
        for(int i= 1;i<=n;i++)
            for(int j=w[i];j<=10000;j++)
                dp[j]=min(dp[j],dp[j-w[i]]+v[i]);
        int ax=dp[m];
        int k = m;
        for(int i=m+1;i<=10000;i++)
        {
            if(dp[i]<=ax)
            {
                ax=dp[i];
                k=i;
            }
        }
        cout<<ax<<" "<<k<<endl;
    }
}

     HDU2639

     01背包第k大价值

     题意如此,求第K大价值

    借用某大佬的一段话:https://blog.csdn.net/Lulipeng_cpp/article/details/7584981 

    实际上,一个正确的状态转移方程的求解过程遍历了所有可用的策略,也就覆盖了问题的所有方案。只不过由于是求最优解,所以其 它在任何一个策略上达不到最优的方案都被忽略了。如果把每个状态表示成一个大小为K的数组,并在这个数组中有序的保存该状态可取到的前K个最优值。那么, 对于任两个状态的max运算等价于两个由大到小的有序队列的合并。另外还要注意题目对于“第K优解”的定义,将策略不同但权值相同的两个方案是看作同一个解还是不同的解。如果是前者,则维护有序队列时要保证队列里的数没有重复的。

    从大到小的dp[j][l],1<=l<=k,所以用两个一维数组来记录当前物品拿还是不拿的状态。再合并这两个数组,取前k个就可以了。因为他们也是从大到小排的嘛......

    记得去重~~以及d1[k+1]=-1,d2[k+1]=-1;

    

#include<iostream>
#include<cstdio>
#include<map>
#include<cstring>
#include<vector>
#include<algorithm>
using namespace std;
int dp[1010][40];
int d1[1010],d2[1010];
int va[1010],wi[1010];
int main()
{
    int t;
    cin>>t;
    while(t--)
    {
        int n,v,k;
        cin>>n>>v>>k;
        memset(dp,0,sizeof(dp));
        for(int i = 0 ; i<n ; i++)
            cin>>va[i];
        for(int i=0;i<n;i++)
            cin>>wi[i];
        for(int i = 0; i < n ; i++ )
        {
            for(int j = v ; j >= wi[i]; j --)
            {
                int l ; 
                for(l =1 ; l <= k ; l++)
                {
                    d1[l]=dp[j][l];
                    d2[l]=dp[j-wi[i]][l]+va[i];
                }
                int z,x,y;
                x=y=z=1;
                d1[l]=-1;
                d2[l]=-1;
                while(z<=k&&(d1[x]!=-1||d2[y]!=-1))
                {
                    if(d1[x]>d2[y])
                    {
                        dp[j][z]=d1[x];
                        x++;
                    }
                    else
                    {
                        dp[j][z]=d2[y];
                        y++;
                    }
                    if(dp[j][z-1]!=dp[j][z])
                        z++;
                }
            }
        }
        cout<<dp[v][k]<<endl;
    }
}

   

原文地址:https://www.cnblogs.com/liyexin/p/11885900.html