『基础DP专题:LIS,LCS和背包九讲(不包括泛化物品)及实现』

版权声明:随你转载,你开心就好(记得评论和注明出处哦) https://blog.csdn.net/Prasnip_/article/details/81056480

<前言>


<更新提示>

<第一次更新>重点背包


<正文>

序列dp问题

LIS问题(最长上升子序列)

求长度为n的序列A中最长上升子序列的长度。

分析

状态:f[i]代表以a[i]结尾的最长上升子序列长度。
方程: f [ i ] = m a x ( f [ i ] , f [ j ] + 1 ) ( f [ j ] < f [ i ] )
边界:f[i]=1(i=1~n)。
优化:暴力转移。
代码实现:

#include<bits/stdc++.h>
using namespace std;
int n,a[1000080]={},f[100080]={};
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i],f[i]=1;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<i;j++)
        {
            if(a[j]<a[i])f[i]=max(f[i],f[j]+1);
        }
    }
    cout<<f[n]<<endl;
    return 0;
}

LCS问题(最长公共子序列)

题目

求两个长度为n的序列A,B的最长公共子序列长度。

分析

状态:f[i][j]表示序列字串A[1~i],B[1~j]的最长公共字串。
方程:

f [ i ] [ j ] = { m a x ( f [ i 1 ) [ j ] , f [ i ] [ j 1 ] ) ( a [ i ] b [ j ] ) f [ i 1 ] [ j 1 ] + 1 ( a [ i ] = b [ j ] )

边界:f[i][0]=f[j][0]=0。
优化:暴力转移
代码实现:

#include<bits/stdc++.h>
using namespace std;
int n,a[10080]={},f[1080][1080]={},b[1080]={};
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
    }
    for(int i=1;i<=n;i++)
    {
        cin>>b[i];
    }
    for(int i=1;i<=n;i++)f[i][0]=0;
    for(int i=1;i<=n;i++)
    {
        for(int j=1;j<=n;j++)
        {
            if(a[i]==b[j])f[i][j]=f[i-1][j-1]+1;
            else f[i][j]=max(f[i-1][j],f[i][j-1]); 
        }
    }
    cout<<f[n][n]<<endl;
    return 0;
}

背包问题

01背包

题目

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

分析

这是最基础的背包问题,其特征为每一样物品只能取一件或不取。我们用子问题定义状态,其状态如下:
状态:f[i][j]代表已经对i件物品进行决策,使用背包容量为j时的最大价值。
我们每一次作得决策为放第i件物品或不放第i件物品,所以我们可以得出状态转移方程如下:
方程: f [ i ] [ j ] = m a x ( f [ i 1 ] [ j ] , f [ i 1 ] [ j c o s t [ i ] ] + v a l u e [ i ] )
当状态为二维时,其代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,v,cost[100080]={},value[100080]={},f[1080][1080]={};
int main()
{
    cin>>n>>v;
    for(int i=1;i<=n;i++)cin>>cost[i]>>value[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=cost[i];j<=v;j++)
        {
            f[i][j]=max(f[i-1][j],f[i-1][j-cost[i]]+value[i]);
        }
    }
    cout<<f[n][v]<<endl;
    return 0;
}

优化:空间复杂度的优化。
其优化方式如下:注意到我们每一次决策时,状态f[i][j]都由f[i-1]这一维转移而来,考虑推掉数组的第一维。我们继续定义f[i]代表背包容量为i时的最大价值,考虑到其由上一次的决策转移而来,而新的状态转移方程 f [ j ] = m a x ( f [ j ] , f [ j c o s t [ i ] ] + v a l u e [ i ] ) 需要调用 f [ j c o s t [ i ] ] ,为了避免在调用时 f [ j c o s t [ i ] ] 已经被本个物品的决策更新,我们在枚举容量j时采用倒叙枚举。感性的理解,如果我们调用已经决策过的 f [ j c o s t [ i ] ] ,相当于背包中装了多个相同的物品,是不可取的。
当状态为一维时,其代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,v,cost[100080]={},value[100080]={},f[100080]={};
int main()
{
    cin>>n>>v;
    for(int i=1;i<=n;i++)cin>>cost[i]>>value[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=v;j>=cost[i];j--)
        {
            f[j]=max(f[j],f[j-cost[i]]+value[i]);
        }
    }
    cout<<f[v]<<endl;
    return 0;
}

边界及初始化问题
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1..V]均设为-∞,这样就可以保证最终得到的f[N]是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0..V]全部设为0。
为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
这个初始化的讲解可以适用于每一类的背包问题,之后就不再赘述了。

完全背包

题目

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

分析

我们考虑利用01背包的思路,那么仍然可以类似的定义状态。
状态:f[i][j]代表已经对i件物品进行决策,使用背包容量为j时的最大价值。
但是,不一样的是,完全背包中每一样物品都可以取无限次,所以我们的状态转移方程有所改变。
方程: f [ i ] [ j ] = m a x ( f [ i 1 ] [ j ] , f [ i 1 ] [ j k c o s t [ i ] ] + k v a l u e [ i ] ) ( k > 0 , k c o s t [ i ] < j )
对于这个方程,我们所需要求解的状态数是不变的,但相比01背包问题,由于需要枚举k,时间复杂度将幅增加,趋近于

O ( V i = 1 n c [ i ] v )

的时间复杂度。对于题目数据量较大时,这个时间复杂度是非常高的。我们在求解时必须对时间进行优化。由于这种方法不常用且不够优秀,这里不再给出代码。我们直接讨论如何优化。
优化:时间复杂度和空间复杂度的优化。
其优化方式如下:在01背包的一维状态求解中,我们提到,如果我们调用已经决策过的 f [ j c o s t [ i ] ] ,并使用01背包的状态转移方程进行转移,就相当于物品可以取若干件。而这恰好适用于完全背包,所以,我们可以直接套用一维的01背包,并将j的枚举顺序由逆序改为顺序即可。
当然,如果一下无法理解为什么改一改顺序就能有如此大的差别,我们用画图来理解:
这里写图片描述
假设i=1,即在对物品1进行规划,我们在枚举容量j时,在某个点t处做了一次决策,并选择了从之前的状态 f [ j c o s t [ i ] ] + v a l u e [ i ] 转移而来,那么,我们可以认为,在f[t]中得出的答案取了物品1。
这里写图片描述

那么,不妨设i=1时又有一个时间点t1,它在决策时也是从之前的状态 f [ j c o s t [ i ] ] + v a l u e [ i ] 转移而来,最奇妙的事发生了,当t1-cost[i]恰为t时,f[t1]从时间点t转移而来,即等价于 f [ t 1 ] = f [ t ] + v a l u e [ i ] ,这时,由于这次决策我们选择由从前的状态转移而来,相当于取了物品1,但从前的状态f[t]中也取了物品1,也就是说,物品1取了两次。
同理,还有更多的时间点t2,t3,…,tk,物品1也可以换为物品2,3,…,p。这样,就相当于在决策时允许取一个物品多次,就是在做完全背包了。

代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,v,cost[100080]={},value[100080]={},f[100080]={};
int main()
{
    cin>>n>>v;
    for(int i=1;i<=n;i++)cin>>cost[i]>>value[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=cost[i];j<=v;j++)
        {
            f[j]=max(f[j],f[j-cost[i]]+value[i]);
        }
    }
    cout<<f[v]<<endl;
    return 0;
}

多重背包

题目

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

分析

这个问题和完全背包非常相似,特点为物品可以取给定的次数,状态我们仍然这样定义。
状态:f[i][j]代表已经对i件物品进行决策,使用背包容量为j时的最大价值。
其暴力算法的状态转移方程也与完全背包极其相似。
方程: f [ i ] [ j ] = m a x ( f [ i 1 ] [ j ] , f [ i 1 ] [ j k c o s t [ i ] ] + k v a l u e [ i ] ) ( 0 < k n [ i ] )
当然,暴力转移的时间复杂度也是如此的高。

O ( V i = 1 N n [ i ] )

但是多重背包的优化就不像完全背包那么容易了,所以,暴力转移的方法有时使用。其代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int N,v,cost[100080]={},value[100080]={},f[100080]={},n[100080]={};
int main()
{
    cin>>N>>v;
    for(int i=1;i<=n;i++)cin>>cost[i]>>value[i]>>n[i];
    for(int i=1;i<=N;i++)
    {
        for(int j=cost[i];j<=v;j++)
        {
            for(int k=1;k<=n[i]&&j>=k*cost[i];k++)
            {
                f[j]=max(f[j],f[j-cost[i]*k]+value[i]*k);
            }
        }
    }
    cout<<f[v]<<endl;
    return 0;
}

优化:时间复杂度的优化。
其优化方式如下:我们将n[i]看作一个二进制数,对其的每一位进行分解,即每一次在n[i]中取出2temp,temp=1,2,3,…,直至n[i]小于2temp,剩下的n[i]作为最后取出的数。这样我们就得到了一个n[i]的二进制分解序列。
以12为例:
12-20=11(取出1)
11-21=9(取出2)
9-22=5(取出4)
由于5<23,直接取出5
所以12的二进制序列为1,2,4,5。二进制序列有什么用呢?请看:
1=1
2=2
3=1+2
4=4
5=5
6=5+1
7=5+2
8=5+1+2
9=5+4
10=5+4+1
11=5+4+2
12=5+4+2+1
得知由数k分解而成的二进制序列a1,a2,…,an,可以由加法组成1~k的任一数。这里,我们可以从二进制的角度考虑这一性质,所以我们称之为二进制优化。对于题中的每一个n[i],我们对其进行二进制分解,并将二进制序列中的每一个项的个数的物品i储存为新物品,价值和费用以个数翻倍。由于我们知道二进制序列可以由加法组成1~k的任一数。我们也就能以二进制序列重新储存的新物品组成1~n[i]个物品i的情况。也就是说,经过二进制优化的转换后,我们对新物品做01背包就行了,这大大的减轻了时间复杂度。仅为:

O ( V i = 1 N l o g 2 n [ i ] )

代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,num[100080]={},value[100080]={},spend[100080]={},v; 
int value_[1000080]={},spend_[1000080]={},f[1000080]={};
int main()
{
    cin>>n>>v;
    for(int i=1;i<=n;i++)cin>>num[i]>>spend[i]>>value[i];
    int m=0;
    for(int i=1;i<=n;i++)
    {
        int temp=0;
        while(num[i]-(1<<temp)>=0)
        {
            num[i]-=(1<<temp);
            value_[++m]=value[i]*(1<<temp);
            spend_[m]=spend[i]*(1<<temp);
            temp++;
        }
        if(num[i])value_[++m]=value[i]*num[i],spend_[m]=spend[i]*num[i];
    }
    for(int i=1;i<=m;i++)
    {
        for(int j=v;j>=spend_[i];j--)
        {
            f[j]=max(f[j],f[j-spend_[i]]+value_[i]);
        }
    }
    cout<<f[v]<<endl;
    return 0;
}

混合背包

题目

如果在背包问题中,有的物品只可以取一次(01背包),有的物品可以取无限次(完全背包),有的物品可以取的次数有一个上限(多重背包)。应该怎么求解呢?

分析

显然,对于这类问题,我们不必重新定义并解决。这个问题完全是对以上三个背包问题的灵活运用。在解决时,我们只需判断物品的个数的类型,调用以上三个问题的代码就可以完美解决了。
由于以上已经给出代码,本题代码不再给出,其伪代码思路如下:

for(int i=1;i<=N;i++)
    ifi件物品属于01背包
        ZeroOnePack(c[i],w[i])
    else ifi件物品属于完全背包
        CompletePack(c[i],w[i])
    else ifi件物品属于多重背包
        MultiplePack(c[i],w[i],n[i])

二维费用的背包

题目

二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为value[i]。

分析

费用增加了一维,我们将状态也由原来的二维增加一维即可。
状态:f[i][j][k]代表已经对i件物品进行决策,使用背包容量分别为j和k时的最大值。
状态转移方程仍然以此类推,直接转移即可。
方程: f [ i ] [ j ] [ k ] = m a x ( f [ i 1 ] [ j ] [ k ] , f [ i 1 ] [ j a [ i ] ] [ k b [ i ] ] + v a l u e [ i ] )
代码的实现也非常简单,和01背包基本相同。
其代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,u,v,a[100080]={},b[100080]={},value[100080]={},f[180][180][180]={};
int main()
{
    cin>>n>>u>>v;
    for(int i=1;i<=n;i++)cin>>a[i]>>b[i]>>value[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=a[i];j<=u;j++)
        {
            for(int k=b[i];k<=v;k++)
            {
                f[i][j][k]=max(f[i-1][j][k],f[i-1][j-a[i]][k-b[i]]+value[i]);
            }
        }
    }
    cout<<f[n][u][v]<<endl;
    return 0;
}

当然,二维费用的背包同样可以像01背包一样优化,将三维的状态压为二维。同理,也将j,k的循环逆序进行即可。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,u,v,a[100080]={},b[100080]={},value[100080]={},f[1080][1080]={};
int main()
{
    cin>>n>>u>>v;
    for(int i=1;i<=n;i++)cin>>a[i]>>b[i]>>value[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=u;j>=a[i];j--)
        {
            for(int k=v;k>=b[i];k--)
            {
                f[j][k]=max(f[j][k],f[j-a[i]][k-b[i]]+value[i]);
            }
        }
    }
    cout<<f[u][v]<<endl;
    return 0;
}

这样,我们可以同理对更高维费用的背包问题进行拓展,费用几维,我们将状态扩展几维,循环枚举增加几重就可以解决。

分组的背包

题目

有N件物品和一个容量为V的背包。第i件物品的费用是cost[i],价值是value[i]。这些物品被划分为若干组,每组中的物品互相冲突,最多选一件。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

分析

背包的物品有了分组的限制,以前的状态显然不行了。我们必须重新重新设计并定义状态。有了组的限制,我们考虑将状态加一维。就像01背包的二维写法一样,我们这样定义状态。
状态:f[k][j]代表已经对前k组物品进行决策,使用背包容量为k的最大值。
那么,状态转移方程也与01背包的二维写法类似。
方程: f [ k ] [ j ] = m a x ( f [ k ] [ j ] , f [ k 1 ] [ j c o s t [ i ] ] + v a l u e [ i ] ) ( i g r o u p K )
得到了方程,只要三重循环暴力转移状态得出答案即可。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int value[180][1080],cost[180][1080]={},num[1080]={};
int n,v,f[1080][1080]={},k=-1; 
int main()
{
    cin>>v>>n;
    for(int i=1;i<=n;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        cost[c][++num[c]]=a,value[c][num[c]]=b;
        k=max(k,c);
    }
    for(int i=1;i<=k;i++)
    {
        for(int j=v;j>=0;j--)
        {
            f[i][j]=f[i-1][j];
            for(int l=1;l<=num[i];l++)
            {
                if(j>=cost[i][l])f[i][j]=max(f[i][j],f[i-1][j-cost[i][l]]+value[i][l]);

            }
        }
    }
    cout<<f[k][v]<<endl;
    return 0;
}

当然,我们也可以像01背包一样将第一维优化掉,其代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int value[180][1080],cost[180][1080]={},num[1080]={};
int n,v,f[1080]={},k=-1; 
int main()
{
    cin>>v>>n;
    for(int i=1;i<=n;i++)
    {
        int a,b,c;
        cin>>a>>b>>c;
        cost[c][++num[c]]=a,value[c][num[c]]=b;
        k=max(k,c);
    }
    for(int i=1;i<=k;i++)
    {
        for(int j=v;j>=0;j--)
        {
            for(int l=1;l<=num[i];l++)
            {
                if(j>=cost[i][l])f[j]=max(f[j],f[j-cost[i][l]]+value[i][l]);
            }
        }
    }
    cout<<f[v]<<endl;
    return 0;
}

有依赖的背包

题目

这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。为了简化起见,我们先设没有某个物品既依赖于别的物品,又被别的物品所依赖;另外,没有某件物品同时依赖多件物品。

分析

按照这个题目的特征,如果采用暴力转移,显然会超时。因为主件和附件的组合情况是
指数级别的,我们必须思考其他解决办法。
状态:f[j]代表背包容量为j时的最大值。
这题我们需要直接考虑解决策略,因为其没有明确的状态转移方程,需要不断迭代更新。我们考虑如下操作,每一次,我们对每一个主件与其附件的集合做一次01背包,得到一个数组temp[j]代表必须取主件i,背包容量为j时的最大值,当然,我们需要从f数组来更新每一次的temp数组。对附件完成01背包后,我们得到temp数组,在用temp数组取重新更新f数组,查看是否有更优解。
可以总结如下:我们每一次强制取主件i,利用f数组更新temp数组,再对主件和附件的集合做01背包。如果得到了更优解,重新更新f数组。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int v,n,spend[100080]={},value[100080]={},mainly[100080]={};
int f[100080]={},temp[100080]={};
int main()
{
    cin>>v>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>spend[i]>>value[i]>>mainly[i];
    }
    for(int i=1;i<=n;i++)
    {
        if(!mainly[i])
        {
            for(int j=1;j<=spend[i];j++)temp[j]=0;
            for(int j=spend[i];j<=v;j++)temp[j]=f[j-spend[i]]+value[i];
            //强制加入主件i 
            for(int j=1;j<=n;j++)
            {
                if(mainly[j]==i)
                {
                    for(int k=v;k>=spend[i]+spend[j];k--)
                    {
                        temp[k]=max(temp[k],temp[k-spend[j]]+value[j]);
                    }
                }
            }
            for(int j=spend[i];j<=v;j++)
            {
                f[j]=max(f[j],temp[j]);
            }
        }
    }
    cout<<f[v]<<endl;
    return 0;
}

背包问题的其他问法

输出方案

对于输出方案,我们可以从完成01背包后得到的状态f[n][v]倒推,如果满足 f [ n ] [ v ] = f [ n 1 ] [ v c o s t [ n ] ] + v a l u e [ n ] 那么就说明物品n在状态转移时取了,将物品n加入最后的方案中即可。同理,可以继续判断进行,得到方案。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,v,cost[100080]={},value[100080]={},f[1080][1080]={},ans[1080]={},t=0;
int main()
{
    cin>>v>>n;
    for(int i=1;i<=n;i++)cin>>cost[i]>>value[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=v;j++)
        {
            if(j<cost[i])f[i][j]=f[i-1][j];
            else f[i][j]=max(f[i-1][j],f[i-1][j-cost[i]]+value[i]);
        }
    }
    cout<<f[n][v]<<endl;
    while(n>0)
    {
        if(f[n][v]==f[n-1][v-cost[n]]+value[n])
        {
            ans[++t]=n;
            v-=cost[n];
        }
        n--;
    }
    for(int i=t;i<=1;i++)cout<<ans[i]<<" ";
    cout<<endl;
} 
求字典序最小的方案

易知,如果直接按以上的方法输出,并不一定能保证解的字典序最小。如何保证字典序最小呢,显然,我们从小到大枚举物品是否选择,如果出现了最优解中某个物品可选可不选的情况,显然要选,因为我们从小到大枚举,他的字典序必然最大。而在输出方案时我们只能从n向下枚举,那么,我们就需要在输入时反向输入,输出时在输出n-i+1在反回来以解决这个问题。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,v,cost[100080]={},value[100080]={},f[1080][1080]={};
int main()
{
    cin>>v>>n;
    for(int i=n;i>=1;i--)cin>>cost[i]>>value[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=v;j++)
        {
            if(j<cost[i])f[i][j]=f[i-1][j];
            else f[i][j]=max(f[i-1][j],f[i-1][j-cost[i]]+value[i]);
        }
    }
    cout<<f[n][v]<<endl;
    int N=n,i=n;
    while(i>0)
    {
        if(f[i][v]==f[i-1][v-cost[i]]+value[i])
        {
            cout<<N-i+1<<" ";
            v-=cost[i];
        }
        n--;
    }
} 
求方案总数

对于一个给定了背包容量、物品费用、物品间相互关系(分组、依赖等)的背包问题,除了再给定每个物品的价值后求可得到的最大价值外,还可以得到装满背包或将背包装至某一指定容量的方案总数。
对于这类改变问法的问题,一般只需将状态转移方程中的max改成sum即可。例如若每件物品均是完全背包中的物品,转移方程即为
f[j]=sum{f[j],f[j-cost[i]]}
初始条件f[0]=1。
事实上,这样做可行的原因在于状态转移方程已经考察了所有可能的背包组成方案。

using namespace std;
int n,v,cost[100080]={},value[100080]={},f[1080]={};
int main()
{
    cin>>v>>n;
    for(int i=1;i<=n;i++)cin>>cost[i]>>value[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=v;j>=cost[i];j--)
        {
            f[j]=f[j]+f[j-cost[i]];
        }
    }
    cout<<f[v]<<endl;
} 
求最优方案总数

我们在完成背包问题的同时,需要重新设置一个状态g[j]为该f[j]状态最优方案的方案总数 。我们得知方案总数同样由f[j]的每一次决策转移而来,那么,g[j]的状态转移方程可以推导如下:

w h e n f [ j ] f r o m f [ j ] g [ j ] = g [ j ] w h e n f [ j ] f r o m f [ j c o s t [ i ] ] + v a l u e [ i ] g [ j ] = g [ j c o s t [ i ] ] w h e n f [ j ] = f [ j c o s t [ i ] ] + v a l u e [ i ] g [ j ] = g [ j ] + g [ j c o s t [ i ] ]

利用这个状态转移方程,在完成背包问题的同时求解最优方案总数即可。代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,v,cost[100080]={},value[100080]={},f[1080]={},g[1080]={};
int main()
{
    cin>>v>>n;
    g[0]=1;
    for(int i=1;i<=n;i++)cin>>cost[i]>>value[i];
    for(int i=1;i<=n;i++)
    {
        for(int j=v;j>=cost[i];j--)
        {
            if(f[j]>f[j-cost[i]]+value[i])continue;//情况1    
            if(f[j]<f[j-cost[i]]+value[i]){g[j]=g[j-cost[i]],f[j]=f[j-cost[i]]+value[i];continue;}//情况2 
            if(f[j]==f[j-cost[i]]+value[i]){g[j]+=g[j-cost[i]];continue;}//情况3 
        }
    }
    cout<<g[v]<<endl;
}
第k优解

对于求解背包问题的第k优解问题,我们可以将状态加一维,f[j][k]代表背包容量为j的第k优解。然后我们在状态转移时依次将前k个解依次从f[j][1..k],f[j-c[i]][1..k]+w[i]转移,共同合并为f[j][1..k]即可,实现难度较小。
代码实现如下:

#include<bits/stdc++.h>
using namespace std; 
int n,v,k,spend[100080]={},value[100080]={},f[10080][100]={},temp[100]={},ans=0;
int main()
{
    memset(f,-10,sizeof(f));
    f[0][1]=0;
    cin>>k>>v>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>spend[i]>>value[i];
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=v;j>=spend[i];j--)
        {
            int P1=1,P2=1;
            for(int p=1;p<=k;p++)
            {
                temp[p]=f[j][p];
                if(temp[P1]>f[j-spend[i]][P2]+value[i])
                {
                    f[j][p]=temp[P1++];
                }
                else f[j][p]=f[j-spend[i]][P2++]+value[i];
            }
        }
    }
    cout<<f[v][k]<<endl;
    return 0;
}

小结

泛化物品对于普及组来说较难,这里不再讨论。其他的背包问题基本已经总结在此,背包的变化实际上也不多,理解都不是很难,主要在于需要在考试时建立相应的模型,判断问题是否属于背包问题的范畴,更变,能灵活运用知识点,牢记解决策略,知识才能派上用场。


<后记>
参考博客:https://blog.csdn.net/Ronaldo7_ZYB/article/details/81069906
及背包九讲


<废话>

猜你喜欢

转载自blog.csdn.net/Prasnip_/article/details/81056480