草庐IT

动态规划之背包问题

Dream.Luffy 2023-04-11 原文

前言:

动态规划的本质,是对问题状态的定义和状态转移方程

动态规划具备三个特点:

        1.将原来的问题分解成几个相似的子问题;

        2.所有的子问题都只需要解决一次

        3.每个状态存储子问题的解

一般从三个角度考虑动态规划:

        1.状态表示:

        2.状态计算 - > 集合的划分

        3.状态初始化

集合的划分依据,需要满足两个条件

        1. 以最后一个改变的元素为依据。

        2. 集合中应包含所有的方案。

而动态规划问题一般可以分为线性DP,背包问题,区间DP,计数类DP,数位统计DP,状态压缩DP,树形DP,背包问题是大头,也是我们这章的重点。

全文共12499

目录:

一 .四个基础背包问题 

        ①01背包        

        ②完全背包

        ③多重背包

        ④分组背包

二 .背包问题的扩展

        ①二维背包

        ②混合背包

        ③背包求方案数

        ④背包存路径

        ⑤背包问题求方案数的至多、至少、恰好问题的总结

正文:

.背包问题都以01背包为基础

我们以01背包问题为引子:

有 N 件物品和一个容量是 V 的背包。问最多能装入背包的总价值是多大?

状态表示所有只从前i个物品中选, 总体积不超过j的方案集合

集合划分

初始化:第0行和第0列都为0,表示没有装物品时的价值都为0:F(0,j) = F(i,0) = 0

对于集合的两种划分方式:

第一种方案:如果不选择第i个物品,价值就与前一维相同: f[i][ j] = f[i - 1][j] 

第二种方案:不选择第i个物品: f[i][j] = f[i - 1][j - v[i]]  + w[i] 

从而得到状态转移方程,即max(f[i][j], f[i - 1][j - v[i]]  + w[i] )

问题一:为什么方案二在这里第二维表示是j - v[i] ? 

答:因为要选择的第i个物品体积为v[i] ,而总体积为j,所以要空出v[i]的体积来填充

二维c++代码如下:时间复杂度 :O(n * m )

注:具体题目可以在洛谷、力扣等平台找

#include <iostream>

using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N];
int f[N][N];

int main() {
    cin >> n >> m;
    for(int i = 1; i <= n; i++) cin >> v[i] >> w[i]; //注意i要大于0,因为后面要用到下标i - 1;
    for(int i = 1; i <= n; i++) {
        for(int j = 0; j <= m; j++) {
            if(j < v[i])f[i][j] = f[i-1][j];
            else f[i][j] = max(f[i - 1][j], f[i-1][j-v[i]]+w[i]);
            //最后即比较加上该物品时的价值与不加该物品时的价值哪个大, 
            //不加该物品时f[i][j]是指不包含该物品时其它相加的价值总和
            //而加上此物品后,得到的是 f[i][j - v[i]] 的价值最大值加上当前物品价值w[i]
        }
    }

    cout << f[n][m] << endl;
 return 0;    
}

为什么答案是f[n][m] ? 

答:第一维是因为要枚举n个物品, 而第二维,即我们的背包体积,f[n][m] 是从f[1][0]一路推过来的。 

一维优化: 时间复杂度 :O(n * m )

​
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;

int n, m;//n是物品数量,m是背包最大容量 
int v[N], w[N];//存储物品体积和价值 
int f[N];//存储最优解答 
int main()
{
    cin >> n >> m;
    //输入对应的 体积和价值 
    for(int i = 1;i <= n;i ++) cin >> v[i] >>w[i];

    for(int i =1;i <= n;i ++)
        for(int j = m;j >= v[i];j --)//如果正序枚举,j循环中的小体积的更新更新会 影响后面的更新 
            f[j] = max(f[j], f[j - v[i]] + w[i]);//因为f[j - v[i]] + w[i]这里用的 f[j - w[i]
                                                //应该是上一层i的,即i - 1而不能在这层之前被更新,否则会影响到后面更新       

    cout << f[m] << endl;
    return 0;
}


​

这里存在两个问题:

一. 为什么可以这样优化 

        我们可以注意到二维数组的更新方式为

        f[i][j] = f[i - 1][j]  // 不含i的所有选法的最大价值
        if j >= v[i]    //判断含i的选法是否成立
        f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])

        可以看出,f[i][] 只与f[i - 1]相关,所以根本没必要保留f[i - 2]即后面的状态值。       

        将空间从O(n*m)优化为 O(m), n, m分别为物品个数和背包体积。

二. 为什么第二行循环需要倒叙枚举 (与之相对的是完全背包的正序枚举)

        当我们更新索引值较大的dp值时,需要用到索引值较小的上一层dp值dp[j - v[i]];
也就是说,在更新索引值较大的dp值之前,索引值较小的上一层dp值必须还在,还没被更新;
所以只能索引从大到小更新。 

就是说 在状态转移方程f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i])中f[i - 1][j - v[i]] 这里使用的是i - 1层的dp,而如果我们正向枚举时 我们的 f[ j - v[i] ]会在 f[j] 之前更新,也就是在更新到f[j - v[i]]这一层时,我们使用的将是被更新过的 f[i][j - v[i]] ,这与原方程f[i - 1][j - v[i]] 不同,所以我们需要倒叙枚举,使得我们将使用的f[j - v[i]]  是未被更新过的

二.有了以上的基础,我们就可以展开对 完全背包,多重背包, 分组背包, 这三个基础背包模型的学习

   完全背包有 N种物品和一个容量是 V 的背包,每种物品都有无限件可用。

   状态表示: 所有只从前i个物品中选, 总体积不超过j的方案集合

   集合的划分方式:第i个物品选几个

 状态转移方程由集合的划分方式可得max(f[i ][j], f[i - 1][j - k * v[i]]  + k * w[i] )

二维代码如下:时间复杂度 :O(n * m )

#include<iostream>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N],w[N];
int main()
{
    int n,m;
    cin>>n>>m;
    for(int i = 1 ; i <= n ;i ++)
    {
        cin>>v[i]>>w[i];
    }

    for(int i = 1 ; i<=n ;i++)
    for(int j = 0 ; j<=m ;j++)
    {
        for(int k = 0 ; k*v[i]<=j ; k++)
            f[i][j] = max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);
    }

    cout<<f[n][m]<<endl;
}

这里多了一层循环,用于枚举决策,而我们一般的循环次序就如此:物品个数 -> 体积 -> 决策

这里,我们列举一下更新次序的内部关系,可以发现优化方式:

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .....)
f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w , .....)

由上两式,可得出如下递推关系: 
                        f[i][j]=max(f[i,j-v]+w , f[i-1][j])

得到了以上的关系,我们可以发现,第三重循环k可以舍去

于是得到优化的一维代码:时间复杂度 :O(n * m )

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int n, m;
int v[N], w[N];
int f[N];
int main()
{
    cin >> n >> m;
    for(int i = 1;i <= n;i ++) cin >> v[i] >> w[i];

    for(int i = 1;i <= n;i ++)
        for(int j = v[i];j <= m;j ++)//因为完全背包 使用当前行的状态更新, 
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m] << endl;
}

这里,我们是正序枚举的,因为从前面得到的递推公式中我们可以发现     f[i][j] = max(f[i][j],f[i][j - v]);  这里使用的都是第i层dp,故不用倒叙枚举,而在背包中,只有完全背包是正序枚举的。

多重背包:有 N种物品和一个容量是 V 的背包,每种物品都有s[i]件 (01背包 和完全背包 可以看作多重背包的特殊情况)

状态表示所有只从前i个物品中选, 总体积不超过j的方案集合

集合划分方式与完全背包一样

状态转移方程由集合的划分方式可得max(f[i ][j], f[i - 1][j - k * v[i]]  + k * w[i] )

优化前代码如下: 时间复杂度:O(n∗v∗s))

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110; //数据量小可以朴素做法 
int n, m;
int v[N], w[N], s[N];//s[n]存储物品个数 
int f[N][N];
int main()
{
    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 = 0;j <= m;j ++)
            for(int k = 0;k <= s[i] && k * v[i] <= j;k ++)
                f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] +w[i] *k);//当k=0时就包括了f[i - 1][j],相当于对f[i][j]赋值

    cout << f[n][m] << endl;
    return 0;
}

但是要注意到这不能像完全背包一样优化,我们一样可以观察这两个状态转移方程

f[i , j ] = max( f[i-1,j] , f[i-1,j-v]+w ,  f[i-1,j-2*v]+2*w , f[i-1,j-3*v]+3*w , .... , f[i-1,j-s[i]*v]+s[i]*w ) 
f[i , j-v]= max(            f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w ,    .....,  f[i-1,j-s[i]*v]+s[i]*w  ,f[i-1,j - (s[i]  + 1)*v]+ s[i] *w );

我们可以发现后面多了一项,这是为什么? 因为s[i]是相同的 前面往后挪了一项,后面也要挪一项。

那么这里可以像完全背包那样优化吗

答案是不能的,因为我们无法在知道 (f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w ,    .....,  f[i-1,j-s[i]*v]+s[i]*w  ,f[i-1,j - (s[i]  + 1)*v]+ s[i] *w ); 的最大值的情况下,

得到                                                    (f[i-1,j-v]   ,  f[i-1,j-2*v] + w , f[i-1,j-3*v]+2*w ,    .....,  f[i-1,j-s[i]*v]+s[i]*w) 的最大值。

因为当 f[i-1,j - (s[i]  + 1)*v]+ s[i] *w 与前面一个值相等时,我们就无法得到最大值, 所以我们只能放弃原有的优化方式, 而采用全新的,二进制优化方式。

优化后代码: 时间复杂度:(n * v * log s)

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 12010, M = 2010;

int n, m;
int v[N], w[N];
int f[M];
int main()
{
    cin >> n >> m;
    int cnt = 0;
    for(int i = 1;i <= n;i ++)//拆分打包 
    {
        int a, b, s; //体积,价值,数量
        cin >> a >> b >> s;
        int k = 1;
        while( k <= s)
        {
            cnt ++;
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;//以二进制优化 
         } 
         if(s > 0)//如果有多 
         {
            cnt ++;
            v[cnt] = a * s;
            w[cnt] = b * s;
         }
    }
    for(int i = 1;i <= cnt;i ++)//以一重背包的形式枚举 
    {
        for(int j = m;j >= v[i];j --)
        {
            f[j] = max(f[j],f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
    return 0;
 } 

什么? 你说二进制优化凭什么正确

这里,我们将每一个体积及价值拆分成 二进制形式,然后将每一个打包成的体积,进行一次01背包即可,

在这里,我们可以假设物品一体积为7 ,那么我们可以将其拆分为1, 2, 4;

而我们知道01背包是枚举每个物品是否需要取,也就是对这些被拆分的体积进行组合,可以得到1 ,2 ,3 , 4, 5, 6, 7每一个体积, 所以可以进行二进制枚举。

当然也有更高效的单调队列优化方式,平时不常用到,这里就不一一例举。

分组背包 :

有 N 组物品和一个容量是 V 的背包。

每组物品有若干个,同一组内的物品最多只能选一个。

状态表示所有只从前i组物品中选, 总体积不超过j的方案集合

分组背包的本质:每个组中的决策都是互斥的

 状态转移方程由集合的划分方式可得max(f[j], f[j - v[i][k]] + w[i][k]);

废话不多说,直接上代码(如何不知道一维是怎么优化来的,再回顾上面的内容,后面不再赘述)

#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
int f[N], v[N][N], w[N][N], s[N];
int main()
{
    int n , m;
    cin >> n >> m;
    for(int i = 1;i <= n;i ++)//有n个小组 
    {
        cin >> s[i];
        for(int j = 0;j < s[i]; j ++)
            cin >>v[i][j] >> w[i][j];
     } 

     for(int i = 1;i <= n;i ++)
        for(int j = m;j >= 0;j --)
            for(int k = 0;k < s[i];k ++)//枚举所有选择 
                if(v[i][k] <= j)
                    f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
    cout << f[m] << endl;     //f[j]为不选,后者为选 
}

好了,结束了,跑路咯....啥?嫌少?那就把你狠狠地小扩展一下!

第一个扩展二维费用的背包问题

有 NN 件物品和一个容量是 V的背包,背包能承受的最大重量是 M。

每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。

这里,有两个限制条件,也即有两个体积,只需多加一层循环,做一遍01背包即可

我是代码:

#include<iostream>
using namespace std;

const int N = 110;

int n, V,M; //V和M为两维费用
int f[N][N];

int main()
{
    cin >> n >> V >> M;
    
    for(int i = 0;i < n;i ++)
    {
        int v, m, w;
        cin >> v>> m >> w;
        for(int j = V; j >= v;j --) 
            for(int k = M;k >= m;k --)
                f[j][k] = max(f[j][k] , f[j - v][k - m] + w);
    }
    cout << f[V][M] << endl;
    
    return 0;
}

不过瘾?

第二个扩展: 不如整个混合背包 

有 N 种物品和一个容量是 V 的背包。

物品一共有三类:

  • 第一类物品只能用1次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 si 次(多重背包);

每种体积是 vi,价值是 wi。

输入:

  • 第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

    接下来有 N 行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 ii 种物品的体积、价值和数量。

  • si=−1 表示第 i 种物品只能用1次;
  • si=0表示第 i 种物品可以用无限次;
  • si>0 表示第 i 种物品可以使用 si 次;

芜湖,这咋搞??

还记得我们在前面提到的吗?

01背包和完全背包,是多重背包的特殊形式

于是我们就有了以下两种想法: 

1.把01背包并入多重背包计算, 完全背包单独计算

2.因为体积限定,以有完全背包实际上并不能取无限个物品i,故我们可以将完全背包某个物品的上限记为 总体积m / 物品体积v[i] , 那么我们可以同时将01背包和完全背包纳入多重背包中计算。

以下对多重背包的计算会采用二进制优化,以回顾前面内容

方法一隆重登场: 这里直接将二进制融入了循环

//解法一,将01背包看为多重背包
#include<iostream>
using namespace std;

const int N = 1010;

int n, m;
int v[N], w[N], s[N];
int f[N];

int main()
{
    cin >>n >> m;
    for(int i = 1;i <= n;i ++) cin >> v[i] >> w[i] >> s[i];

    for(int i = 1;i <= n;i ++)
    {
        if(!s[i]) //完全背包
        {
            for(int j = v[i];j <= m;j ++)
            {
                f[j] = max(f[j], f[j - v[i]] + w[i]);
            }
        }
        else
        {
            //将多重背包利用二进制枚举优化
            //将01背包融入多重背包中
            if(s[i] == -1) s[i] = 1;
            for(int k = 1;k <= s[i];k *= 2)//二进制优化枚举
            {
                for(int j = m;j >= k * v[i]; j --)
                {
                    f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
                }
                s[i] -= k;
            }
            if(s[i])//若s[i]还没减完
            {
                for(int j = m;j >= s[i] * v[i];j --)
                {
                    f[j] = max(f[j], f[j - s[i] * v[i]] + s[i] * w[i]);
                }
            }
         }
    }
    cout << f[m]  << endl;
}

方法二隆重登场:

//解法二,将完全背包也看为多重背包问题
#include<iostream>
using namespace std;
const int N = 100010; //范围要开大,因为要用二进制优化
int n, m, v[N], w[N], f[N];
int main()
{
    cin >>n >> m;
    int cnt  = 1;
    for(int i = 1;i <= n;i ++)
    {
        int a, b ,s;
        cin >>a >> b >> s;
        int k = 1;
        if(s < 0) s = 1;
        else if(s == 0) s = m/a;//把01背包和多重背包先转化成多重背包,若为完全背包,则在最优情况下,只能取总体积/该物品体积向下取整
        while(k <= s)
        {
            v[cnt] = a * k;
            w[cnt] = b * k;
            s -= k;
            k *= 2;
            cnt ++;
        }
        if(s > 0)
        {
            v[cnt] = s * a;
            w[cnt] = s * b;
            cnt ++;
        }
    } //将多重背包进行二进制优化,变成01背包 
    for(int i = 1;i <= cnt;i ++)
    {
        for(int j = m;j >= v[i];j --)
        {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m] << endl;
}

方法二的第一种二进制表达方式:
 

#include<iostream>
using namespace std;
const int N = 1010;

int n, m;
int v[N], w[N], s[N], f[N];

int main()
{
    cin >>n >> m;
    for(int i = 1;i <= n;i ++ ) cin >> v[i] >> w[i] >> s[i];

    for(int i = 1;i <= n;i ++)
    {
        if(!s[i]) 
            s[i] = m / v[i]; //完全背包
        else if(s[i] == -1)
            s[i] = 1;
        //二进制优化
        for(int k = 1;k <= s[i];k *= 2)
        {
            for(int j = m;j >= k * v[i];j --)
            {
                f[j] = max(f[j], f[j - k * v[i]] + k * w[i]);
            }
            s[i] -= k;
        }

        if(s[i] > 0)
        {
            for(int j = m; j >= s[i] * v[i];j --)
            {
                f[j] = max(f[j], f[j - s[i] * v[i]] + s[i] * w[i]);
            }
        }
    }
    cout << f[m];
    return 0;
}

再来再来!

第三个扩展数字组合

给定 N 个正整数 A1,A2,…,AN,从中选出若干个数,使它们的和为 M,求有多少种选择方案。

第一行包含两个整数 N 和 M。

第二行包含 NN 个整数,表示A1,A2,…,AN。

求方案数是什么玩意???

这里,我们可以发现两个和之前不同的地方

①限制条件由原来的体积不超过M,变为 体积(若干个数)和恰好为M。

②答案由给定体积求最大价值,变为求方案数。
写题不能靠瞎想,我们画个图康康

也就是说,选i和不选i 都是能得到当前总和j 的方法咯 ,既然两条路都行,那就把它们都加起来吧!

于是,我们不就得到了状态转移方程了吗

f[ i , j ] = f[ i - 1 , j ] + f[ i - 1 , j - v[ i ] ]

优化为一维:f[j] += f[j - v[i]];

等等,这个恰好有啥用? 正是由这个条件,我们初始化时可以发现  f[0][0] = 1  (记住这个恰好哦,后面还有至多,至少,恰好的总结)

为啥? 因为当个数为0,体积为0时也是一种选法 soga!

直接上代码!

#include <iostream>

using namespace std;

const int N = 110, M = 10010;

int n, m;
int v[N];
int f[M];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; ++ i) cin >> v[i];

    f[0] = 1;
    for (int i = 1; i <= n; ++ i)
    {
        for (int j = m; j >= v[i]; -- j)
        {
            f[j] += f[j - v[i]];
        }
    }
    cout << f[m] << endl;
    return 0;

啊这,为啥f[0] = 1啊?  因为当物品数为0时,体积大于0都不是合法方案如f[0][1],f[0][2]...f[0][j] , 只有f[0][0]是合法方案都没有体积你想塞啥进去(¬︿̫̿¬☆)

第四个扩展: 潜水员问题,求最小值(至少的问题)

 算法分析:

这里就不画图了哈。。懒癌附身

状态表示:f[i,j,k] 所有从前i个物品中选,且氧气含量至少j, 氮气含量至少k的所有选法的气缸重量总和的最小值

状态计算

所有不包含物品i的所有选法:f[i - 1,j,k]

所有包含物品i的所有选法:f[i - 1,j - v1,k - v2]

这里需要注意的是,所需的氧气或者氮气的数量如果是负数,它与数量为0是等价的。

为什么呢?例如f[5][10] 至少需要5氧气,10氮气, 求能拿到价值的最小值,而现在只有一个物品,氧气是6,氮气是7价值w,该物品是可以被携带的,因为它说至少需要5氧气,那么氧气6还是可以用到的,只是多1个氧气占着没用而已,因此,若用了这个物品,则变成了求 f[0][1] + w 表示氧气已经不再需要了。

也就是说, f[5][10]是可以由f[-1][3]更新过来的, 所以在 至少 这个前提下,体积是可以小于0的

由此我们可以得到状态转移方程:

for(int j = V;j >= 0;j --)
    for(int k = M;k >= 0;k --) //注意这里是大于等于0
        f[j][k] = min(f[j][k], f[max(0, j - v)][max(0, k - m)] + w); // 体积小于0与0等价,但下标不能小于0

给出代码:

#include<cstring>
#include<iostream>
#include<algorithm>

using namespace std;

const int N = 22, M = 80;

int n, m, K;
int f[N][M];
int main()
{
    cin >>n >> m >> K;
    
    memset(f, 0x3f, sizeof f); //因为要求的是最小值,初始化为正无穷是为了在更新时不使用它
    f[0][0] = 0;
    
    while(K --)
    {
        int v1, v2, w;
        cin >> v1 >> v2 >> w;
        for(int i = n;i >= 0;i --) //j可以小于v,因为求得是至少
            for(int j = m;j >= 0; j --)
                f[i][j] = min(f[i][j], f[max(0, i - v1)][max(0, j - v2)] + w);
                
    }
    cout << f[n][m];
    return 0;
}

第五个扩展: 背包问题记录方案路径

算法分析:

 题目要求输出字典序最小的解,假设存在最优解包含第一个物品,为了确保字典序最小那么我们必然选第一个。

于是问题就转化成从2~N这些物品中寻找最优解。因为我们要得到的是字典序最小的答案,所以状态定义也与之前有所不同。

在前面,我们的f[i,j] 记录的都是前i个物品总容量为j的最优解,那么现在我们将f[i,j] 定义为从第i 个元素到最后一个元素总容量为j的最优解。于是我们可以得到新的状态转移方程

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

由此,f[1][m] 是最大价值,由状态转移方程,我们可以发现 有三种情况

如果f[i, m] == f[i + 1, m - v[i]] + w[i] ] 说明选取了第i个物品可以得到最优解

如果f[i, m] == f[i+ 1, m], 说明不选取第i个物品才能得到最优解 

如果 f[i, m] == f[i + 1, m - v[i]] + w[i] ] == f[i+ 1, m], 说明两种情况选与不选都可以得到最优解,但是由于我们要得到字典序最小的答案,故我们也需要选取这个物品

需要注意的一个不同点是,这里第一次循环需要逆序枚举,由我们的状态转移方程可以得到

//求具体方案数需要逆向枚举,此时f[1][m]为最优解,再由1~n枚举,从后往前得到具体路径。
#include<iostream>
using namespace std;

const int N = 1010;

int n, m;
int w[N],v[N];
int f[N][N];

int main()
{
    cin >>n >> m;
    for(int i = 1;i <= n;i ++) cin >> v[i] >> w[i];
    for(int i = n;i >= 1;i --)
        for(int j = 0;j <= m;j ++)
        {
            f[i][j] = f[i + 1][j];
            if(j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
        }
        
    int j = m;
    for(int i = 1;i <= n;i ++)
        if(j >= v[i] && f[i][j] == f[i + 1][j - v[i]] + w[i])
        {
            cout << i << " ";
            j -= v[i];
        }

    return 0;
}

如果题目没有要求字典序最小时,我们就不用改变原有的状态转移方程,只需在寻找答案时逆序枚举即可。

第六个扩展: 背包问题求方案数

 这里直接上代码了

//f[i][j]表示从前i个物品中选择,体积就是j的价值,g[i][j]表示表示从前i个物品种选择
//体积就是j的最大价值对应方案数
#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;

const int N=1010,mod=1e9+7;

int f[N],g[N];

int main()
{
    int n,m;
    cin>>n>>m;
    memset(f,-0x3f,sizeof f);
    f[0]=0,g[0]=1;//显然选体积为0价值为0,而什么都不选的选法为1
    int mt=0;//用于保存最大值
    for(int i=1;i<=n;i++)
    {
        int v,w;
        cin>>v>>w;
        for(int j=m;j>=v;j--)
        {
            int maxv=max(f[j],f[j-v]+w);
            int s=0;//s这个临时变量存储的是能递推过来的最大价值的方案数
            if(maxv==f[j]) s+=g[j];//当maxv==f[j]时,说明最大价值可由上一层递推过来
            //那么s就需要加上上一层的方案数
            if(maxv==f[j-v]+w)  s=(s+g[j-v])%mod;//当maxv==f[j-v]+w时,说明最大价值可由本层递推过来
            //那么s就需要加上本层的方案数
            f[j]=maxv,g[j]=s;//最终体积为j的对应的最大价值的方案数便为s
            mt=max(mt,maxv);
        }
    }

    int res=0;
    for(int i=1;i<=m;i++)//mt就是最大价值
        if(f[i]==mt)  res=(res+g[i])%mod;
    cout<< max(res, 1) <<endl;
    return 0;
}


第七个扩展: 背包问题方案数总结

求方案数初始化总结
二维情况
1、体积至多j,f[0][i] = 1, 0 <= i <= m,其余是0 //因为体积至多是j, 假设体积是5,那么体积为0 也满足了条件,所以每种方案都有一种
2、体积恰好j,f[0][0] = 1, 其余是0 //同理,体积为j时,物品数为0不合法
3、体积至少j,f[0][0] = 1,其余是0 //同上,假设体积至少为5,不合法

一维情况
1、体积至多j,f[i] = 1, 0 <= i <= m,
2、体积恰好j,f[0] = 1, 其余是0
3、体积至少j,f[0] = 1,其余是0

求最大值最小值初始化总结
二维情况
1、体积至多j,f[i,k] = 0,0 <= i <= n, 0 <= k <= m(只会求价值的最大值)
2、体积恰好j,
当求价值的最小值:f[0][0] = 0, 其余是INF
当求价值的最大值:f[0][0] = 0, 其余是-INF
3、体积至少j,f[0][0] = 0,其余是INF(只会求价值的最小值)

一维情况
1、体积至多j,f[i] = 0, 0 <= i <= m(只会求价值的最大值)
2、体积恰好j,
当求价值的最小值:f[0] = 0, 其余是INF
当求价值的最大值:f[0] = 0, 其余是-INF
3、体积至少j,f[0] = 0,其余是INF(只会求价值的最小值)

//但是在至少的情况下,对体积的枚举不同,可以允许v > j, 因为体积为-2时,也满足了至少为0这一条件

这里给出两个求至少情况的代码作参考:

求方案数时:

#include <iostream>

using namespace std;

const int N = 110;

int n, m;
int f[N][N];

int main()
{
    cin >> n >> m;
    f[0][0] = 1;
    for(int i = 1;i <= n;i ++)
    {
        int v;
        cin >> v;
        for(int j = 0;j <= m;j ++)//即使物品体积比j大,j - v < 0,也能选,等价于f[i - 1][0]
        {
            f[i][j] = f[i - 1][j] + f[i - 1][max(0,j - v)];
        }
    }
    cout << f[n][m] << endl;

    return 0;
}


求价值时:

#include <iostream>
#include <cstring>

using namespace std;

const int N = 110, INF = 0x3f3f3f3f;

int n, m;
int f[N][N];

int main()
{
    cin >> n >> m;

    memset(f, INF, sizeof f);
    f[0][0] = 0;

    for(int i = 1;i <= n;i ++)
    {
        int v, w;
        cin >> v >> w;
        for(int j = 0;j <= m;j ++)
        {
            f[i][j] = min(f[i - 1][j], f[i][max(0, j - v)] + w);//即使物品体积比j大,j - v < 0,也能选,等价于f[i - 1][0]
        }
    }

    cout << f[n][m] << endl;

    return 0;
}

心中无女人,coding自然神

下班下班,有空再更。

有关动态规划之背包问题的更多相关文章

  1. ruby - 在 64 位 Snow Leopard 上使用 rvm、postgres 9.0、ruby 1.9.2-p136 安装 pg gem 时出现问题 - 2

    我想为Heroku构建一个Rails3应用程序。他们使用Postgres作为他们的数据库,所以我通过MacPorts安装了postgres9.0。现在我需要一个postgresgem并且共识是出于性能原因你想要pggem。但是我对我得到的错误感到非常困惑当我尝试在rvm下通过geminstall安装pg时。我已经非常明确地指定了所有postgres目录的位置可以找到但仍然无法完成安装:$envARCHFLAGS='-archx86_64'geminstallpg--\--with-pg-config=/opt/local/var/db/postgresql90/defaultdb/po

  2. ruby - 通过 rvm 升级 ruby​​gems 的问题 - 2

    尝试通过RVM将RubyGems升级到版本1.8.10并出现此错误:$rvmrubygemslatestRemovingoldRubygemsfiles...Installingrubygems-1.8.10forruby-1.9.2-p180...ERROR:Errorrunning'GEM_PATH="/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/ruby-1.9.2-p180@global:/Users/foo/.rvm/gems/ruby-1.9.2-p180:/Users/foo/.rvm/gems/rub

  3. ruby - 通过 RVM (OSX Mountain Lion) 安装 Ruby 2.0.0-p247 时遇到问题 - 2

    我的最终目标是安装当前版本的RubyonRails。我在OSXMountainLion上运行。到目前为止,这是我的过程:已安装的RVM$\curl-Lhttps://get.rvm.io|bash-sstable检查已知(我假设已批准)安装$rvmlistknown我看到当前的稳定版本可用[ruby-]2.0.0[-p247]输入命令安装$rvminstall2.0.0-p247注意:我也试过这些安装命令$rvminstallruby-2.0.0-p247$rvminstallruby=2.0.0-p247我很快就无处可去了。结果:$rvminstall2.0.0-p247Search

  4. ruby - Fast-stemmer 安装问题 - 2

    由于fast-stemmer的问题,我很难安装我想要的任何ruby​​gem。我把我得到的错误放在下面。Buildingnativeextensions.Thiscouldtakeawhile...ERROR:Errorinstallingfast-stemmer:ERROR:Failedtobuildgemnativeextension./System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/rubyextconf.rbcreatingMakefilemake"DESTDIR="cleanmake"DESTDIR=

  5. ruby - 安装 Ruby 时遇到问题(无法下载资源 "readline--patch") - 2

    当我尝试安装Ruby时遇到此错误。我试过查看this和this但无济于事➜~brewinstallrubyWarning:YouareusingOSX10.12.Wedonotprovidesupportforthispre-releaseversion.Youmayencounterbuildfailuresorotherbreakages.Pleasecreatepull-requestsinsteadoffilingissues.==>Installingdependenciesforruby:readline,libyaml,makedepend==>Installingrub

  6. java - 从 JRuby 调用 Java 类的问题 - 2

    我正在尝试使用boilerpipe来自JRuby。我看过guide从JRuby调用Java,并成功地将它与另一个Java包一起使用,但无法弄清楚为什么同样的东西不能用于boilerpipe。我正在尝试基本上从JRuby中执行与此Java等效的操作:URLurl=newURL("http://www.example.com/some-location/index.html");Stringtext=ArticleExtractor.INSTANCE.getText(url);在JRuby中试过这个:require'java'url=java.net.URL.new("http://www

  7. ruby-on-rails - 简单的 Ruby on Rails 问题——如何将评论附加到用户和文章? - 2

    我意识到这可能是一个非常基本的问题,但我现在已经花了几天时间回过头来解决这个问题,但出于某种原因,Google就是没有帮助我。(我认为部分问题在于我是一个初学者,我不知道该问什么......)我也看过O'Reilly的RubyCookbook和RailsAPI,但我仍然停留在这个问题上.我找到了一些关于多态关系的信息,但它似乎不是我需要的(尽管如果我错了请告诉我)。我正在尝试调整MichaelHartl'stutorial创建一个包含用户、文章和评论的博客应用程序(不使用脚手架)。我希望评论既属于用户又属于文章。我的主要问题是:我不知道如何将当前文章的ID放入评论Controller。

  8. 【高数】用拉格朗日中值定理解决极限问题 - 2

    首先回顾一下拉格朗日定理的内容:函数f(x)是在闭区间[a,b]上连续、开区间(a,b)上可导的函数,那么至少存在一个,使得:通过这个表达式我们可以知道,f(x)是函数的主体,a和b可以看作是主体函数f(x)中所取的两个值。那么可以有,  也就意味着我们可以用来替换 这种替换可以用在求某些多项式差的极限中。方法: 外层函数f(x)是一致的,并且h(x)和g(x)是等价无穷小。此时,利用拉格朗日定理,将原式替换为 ,再进行求解,往往会省去复合函数求极限的很多麻烦。使用要注意:1.要先找到主体函数f(x),即外层函数必须相同。2.f(x)找到后,复合部分是等价无穷小。3.要满足作差的形式。如果是加

  9. SPI接收数据异常问题总结 - 2

    SPI接收数据左移一位问题目录SPI接收数据左移一位问题一、问题描述二、问题分析三、探究原理四、经验总结最近在工作在学习调试SPI的过程中遇到一个问题——接收数据整体向左移了一位(1bit)。SPI数据收发是数据交换,因此接收数据时从第二个字节开始才是有效数据,也就是数据整体向右移一个字节(1byte)。请教前辈之后也没有得到解决,通过在网上查阅前人经验终于解决问题,所以写一个避坑经验总结。实际背景:MCU与一款芯片使用spi通信,MCU作为主机,芯片作为从机。这款芯片采用的是它规定的六线SPI,多了两根线:RDY和INT,这样从机就可以主动请求主机给主机发送数据了。一、问题描述根据从机芯片手

  10. git使用常见问题(提交代码,合并冲突) - 2

    文章目录git常用命令(简介,详细参数往下看)Git提交代码步骤gitpullgitstatusgitaddgitcommitgitpushgit代码冲突合并问题方法一:放弃本地代码方法二:合并代码常用命令以及详细参数gitadd将文件添加到仓库:gitdiff比较文件异同gitlog查看历史记录gitreset代码回滚版本库相关操作远程仓库相关操作分支相关操作创建分支查看分支:gitbranch合并分支:gitmerge删除分支:gitbranch-ddev查看分支合并图:gitlog–graph–pretty=oneline–abbrev-commit撤消某次提交git用户名密码相关配置g

随机推荐