动态规划01-背包问题

01背包问题

N N N件物品和一个容量是 V V V 的背包。每件物品只能使用一次。
i i i 件物品的体积是 V i V_i Vi,价值是 W i W_i Wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式:
第一行两个整数 N , V N,V NV,用空格隔开,分别表示物品数量和背包容积。
接下来有 N N N行,每行两个整数 V i , W i V_i,W_i ViWi,用空格隔开,分别表示第 i i i 件物品的体积和价值。
输出格式:
输出一个整数,表示最大价值。
数据范围:
0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < V i , W i ≤ 1000 0<V_i,W_i≤1000 0<Vi,Wi1000
输入样例:
4 5
1 2
2 4
3 4
4 5
输出样例:
8

解决思路

由于已经有很好的文章解答该问题,本文就不班门弄斧了,对于该问题的解决思路可以参照于hello algo 中的解答,本文主要给出代码实现以及注释
14.4 0-1 背包问题 - Hello 算法

代码实现

#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
//使用数组v,n分别存储物品大小和价值
int v[N], w[N];
//使用dp[i][j]数组存储前i个物品中不超过容量j的最大价值
int dp[N][N];

int main()
{
    cin >> n >> m;
    //读入数据
    for(int i = 1; i <= n; i++)
        {
            cin >> v[i] >> w[i];
        }
    //使用两层循环进行状态转移
    //遍历每个物品i,对每个物品进行抉择
    for(int i = 1; i <= n; i++)
        {
            //遍历不同的背包容量,表示当前状态下的背包容量为j
            for(int j = 1; j <= m; j++)
                {
                    //当要选择的物品的体积不超过当前的背包容量,才可以选择放入
                    if(j >= v[i])
                    {
                        //从放入当前物品,背包容量减小,价值增加
                        //和不放入当前物品,背包容量不变的状态中选择一个价值大的
                        dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - v[i]] + w[i]);
                    }else
                    {
                        dp[i][j] = dp[i - 1][j];
                    }
                }
        }
    cout << dp[n][m] << endl;
    return 0;
}

关于倒叙遍历问题可以看我的另一篇blog,里面有具体解释。
01背包问题空间优化中的内层循环问题

#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int dp[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        {
            cin >> v[i] >> w[i];
        }
    //由于在进行状态转移时,每次用到的都是上一次的数据
    //因此可以使用滚动数组的形式进行数据的存储,但是由于每次都来自于上方和左上方的数据
    //因此在进行从左向右遍历时第i - 1层的数据会被第 i 层的数据覆盖
    //需要使用倒序遍历的方式避免数据的覆盖
    for(int i = 1; i <= n; i++)
        {
            for(int j = m; j >= 1; j--)
                {
                    if(j >= v[i])
                    {
                        dp[j] = max(dp[j],dp[j - v[i]] + w[i]);
                    }
                }
        }
    cout << dp[m] << endl;
    return 0;
}
#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int dp[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--)
                {
                    //其实并没有什么优化,仅仅减少了一个判断条件
                    //由于我们不可以放入比当前背包容量还要大的物品
                    // 因此我们可以将这个if 判断放到for循环中进行处理
                    //使代码更加优雅
                    dp[j] = max(dp[j],dp[j - v[i]] + w[i]);
                }
        }
    cout << dp[m] << endl;
    return 0;
}

完全背包问题

N N N件物品和一个容量是 V V V 的背包。每件物品可以使用无限次。
i i i 件物品的体积是 V i V_i Vi,价值是 W i W_i Wi
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式:
第一行两个整数 N , V N,V NV,用空格隔开,分别表示物品数量和背包容积。
接下来有 N N N行,每行两个整数 V i , W i V_i,W_i ViWi,用空格隔开,分别表示第 i i i 件物品的体积和价值。
输出格式:
输出一个整数,表示最大价值。
数据范围:
0 < N , V ≤ 1000 0<N,V≤1000 0<N,V1000
0 < V i , W i ≤ 1000 0<V_i,W_i≤1000 0<Vi,Wi1000
输入样例:
4 5
1 2
2 4
3 4
4 5
输出样例:
10

解决思路

对于该问题有两种思考方式,但是无论是哪一个最终的答案都是相同的

  • acwing上的思路

三重循环
将dp[i][j]分为k个集合,分别为选择第i个物品的数量从0到k,k为可以放入当前背包的最大个数,因此需要三重循环进行遍历
image.png
二重循环
简单的等式转换
通过上面两个式子,我们不难看出dp[i][j]中从dp[i-1][j-v]+w开始的一系列式子均可以通过将dp[i][j-v]加上w得到,从而将dp[i][j]的状态转移方程与k脱离关系,使得循环转换为二重循环
image.png
一维数组
类比于之前的01背包问题的优化,可以将其优化为一维数组

  • hello algo上的思路

相比于acwing上的层层推进,hello算法上的思路更加的一步到位。从真实的选取中理解该变化,我们将情况分为2个集合,对于第i个物品分别为不选取和选取,对于不选情况自然是背包容量不会减少同时对于第i个物品的选择已经结束,需要处理前i-1的物品的选择,也就是dp[i][j] = dp[i-1][j],而对于选择的情况,由于物品的数量是无尽的,因此仍可以从i个物品中选择,dp[i][j] = dp[i][j-v]+w。
image.png
通过对比不难看到上述两种问题的状态转移方程仅有一处不同
在此同时给出上述两种方案的原地址
acwing算法基础课-动态规划
14.5 完全背包问题 - Hello 算法

代码实现

#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int dp[N][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 = 1; j <= m; j++)
        {
            for(int k = 0; k * v[i] <= j; k++)
            {
                dp[i][j] = max(dp[i][j],dp[i-1][j - k*v[i]] + k*w[i]);
            }
        }
    }

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

#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int dp[N][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 = 1; j <= m; j++)
        {
            //不放入的情况
            dp[i][j] = dp[i - 1][j];
            //放入的情况
            if(j >= v[i])
            {
                //由于每个物品的选择次数都成为了无数次
                // 因此做完第i个物品的决策之后依旧可以选择该物品
                dp[i][j] = max(dp[i][j],dp[i][j - v[i]] + w[i]);
            }
        }
    }

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

#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
int v[N], w[N];
int dp[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++)
        {
            //如果不采用正序遍历,得到的结果反而会是上一层的数据,而不是本层的数据
            dp[j] = max(dp[j],dp[j - v[i]] + w[i]);
        }
    }

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

多重背包问题

N N N种物品和一个容量是 V V V 的背包。
i i i 种物品最多有 S i S_i Si 件,每件体积是 V i V_i Vi,价值是 W i W_i Wi
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数, N N N V V V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N N N 行,每行三个整数 V i V_i Vi, W i W_i Wi, S i S_i Si,用空格隔开,分别表示第 i i i 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0 < N ≤ 1000 0<N≤1000 0<N1000
0 < V ≤ 2000 0<V≤2000 0<V2000
0 < V i , W i , S i ≤ 2000 0<V_i,W_i,S_i≤2000 0<Vi,Wi,Si2000
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例
10

解决思路

对于该问题我们有多种解决方法

  • 三种循环遍历

按照之前的完全背包问题,使用一样的解决方法,仅仅增加一个约束条件,保证当前的 k k k在给出的 s s s的范围之内。

  • 利用二进制优化

由于上面的解决方法使用了三重循环,因此我们可以处理的数据大小仅仅在 1 0 2 10^2 102之内,对于 1 0 3 10^3 103的数据量会导致 TLE。
对于该问题我们采用将其转换为 0 0 0 1 1 1背包问题进行解决,但是如果像上面直接铺开,将一个数量为 10 10 10的背包分为 [ 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 , 1 ] [1,1,1,1,1,1,1,1,1,1] [1,1,1,1,1,1,1,1,1,1] 0 0 0 1 1 1背包,并不会对该问题有任何优化。因此我们可以采用二进制的方式进行压缩也就是将一个数量为 10 10 10的背包分为 [ 1 , 2 , 4 , 3 ] [1,2,4,3] [1,2,4,3],关于为什么不是 [ 1 , 2 , 4 , 8 ] [1,2,4,8] [1,2,4,8]是由于如果这样最终 4 − 8 4-8 48之间的数就无法表示,并且会超出 10 10 10的范围。

  • 使用单调队列优化

暂未实现

代码实现

#include <iostream>
using namespace std;

const int N = 110;
int n, m;
int v[N], w[N], s[N];
int dp[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 = 1; j <= m; j++)
        {
            for(int k = 0; k <= s[i] && k*v[i] <= j; k++)
            {
                dp[i][j] = max(dp[i][j],dp[i-1][j - k*v[i]] + k*w[i]);
            }
        }
    }
    cout << dp[n][m] << endl; 
    return 0;
}
#include <iostream>
using namespace std;

//需要开一个N*logS的数组大小
const int N = 2010, M = 25000;
int n, m;
int v[M], w[M];
int dp[N];

int main()
{
    cin >> n >> m;
    int cnt = 0;
    for(int i = 0; i < n; i++)
    {
        //将每一组的s数量按照从1,2,4,8的方式分为不同的01背包问题
        //如果一件物品规定的使用次数为 10 次,我们将其扁平化为三件物品:1*重量&1*价值,2*重量&2*价值,4*重量&4*价值,3*重量&3*价值
        int a, b, s;
        cin >> a >> b >> s;
        int k = 1;
        //按照每次乘2的规律进行分配
        while(k <= s)
        {
            cnt++;
            v[cnt] = k * a;
            w[cnt] = k * b;
            s -= k;
            k *= 2;
        }
        //将剩余的s单独分为一个01背包
        if(s > 0)
        {
            cnt++;
            v[cnt] = s * a;
            w[cnt] = s * b;
        }
    }
    //01背包问题模板
    n = cnt;
    for(int i = 1; i <= n; i++)
    {
        for(int j = m; j >= v[i]; j--)
        {
            dp[j] = max(dp[j],dp[j - v[i]] + w[i]);
        }
    }
    cout << dp[m] << endl; 
    return 0;
}

分组背包问题

N N N组物品和一个容量是 V V V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是 V i j Vij Vij,价值是 W i j Wij Wij,其中 i i i是组号, j j j是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N , V N,V N,V用空格隔开,分别表示物品组数和背包容量。
接下来有 N N N组数据:

  • 每组数据第一行有一个整数 S i S_i Si,表示第 i i i个物品组的物品数量;
  • 每组数据接下来有 S i S_i Si行,每行有两个整数 V i j , W i j Vij,Wij Vij,Wij用空格隔开,分别表示第 i i i个物品组的第 j j j个物品的体积和价值

输出格式
输出一个整数,表示最大价值。
数据范围
0 < N , V ≤ 100 0<N,V≤100 0<N,V100
0 < S i ≤ 100 0<S_i≤100 0<Si100
0 < V i j , W i j ≤ 100 0<Vij,Wij≤100 0<Vij,Wij100

解决思路

对于分组背包问题我们可以采用将集合分组讨论的方法,对于一个 d p [ i ] [ j ] dp[i][j] dp[i][j]的集合,我们可以将其分为 N N N组,其中选择每组中第 k k k个数,因此其状态转移方程可以写为 d p [ i ] [ j ] = d p [ i − 1 ] [ j − v [ i ] [ k ] ] + w [ i ] [ k ] dp[i][j] = dp[i - 1][j - v[i][k]] + w[i][k] dp[i][j]=dp[i1][jv[i][k]]+w[i][k]
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例
8

代码实现

#include <iostream>
using namespace std;

const int N = 110;
int v[N][N], w[N][N], s[N];
int dp[N];
int n, m;

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
    {
        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 >= 1; j--)
        {
            for(int k = 0; k < s[i]; k++)
            {
                if(j >= v[i][k])
                {
                    dp[j] = max(dp[j], dp[j - v[i][k]] + w[i][k]);
                }
            }
        }
    }
    cout << dp[m] << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值