由01背包问题谈动态规划 Charm Bracelet POJ - 3624

168 阅读1分钟

01背包问题是一道极其经典的线性规划入门问题,这道题也是Poj上的一道经典题目。

简单回顾

      动态规划, 又称dp(Dynamic programming), 是一类极其常见而重要的算法. 其来源最早来源于递推和搜索, 所以动态规划又称为记忆化搜索, 在性质上又和贪心算法极为类似, 大概具备以下几种特性

      1.无后效性, 当前状态一旦确定, 今后不会再影响到当前状态

      2.满足最优子结构, 抽象而成的子问题, 也就是子状态, 只要沿着某种算法可以取得当前最优解, 那么依次递推下来最终结果也一定就是最优解

      拿到一个动态规划问题的关键事情有两个

      1.寻找合适的子问题, 也就是确定状态, 这一点上和搜索是一样的, 要先把问题抽象为子状态(一般dp中初始状态和目标状态都不难确定)

      2.寻找状态之间的转移关系, 这个往往要通过具体题意来确定, 也就是写出正确的状态转移方程. 这也是dp的核心问题所在, 我的经验只谈就是仔细理解题意, 多多练习

      而动态规划问题往往还要注意以下问题

      1.边界问题, 避免边界问题也有几种策略:

                 a.从1开始, 避免i-1导致的越界; 

                 b. 把状态转移方程的系数依次+1eg: dp[i][j] = max(dp[i-1][j], dp[i][j-1])    可以改写为dp[i+1][j+1] = max(dp[i][j+1],                              dp[i+1][j]);

                 c.再要不就是单独考虑边界情况; 还需要注意很多时候边界问题可以直接合入正常的递推方程中(例如本题)

      2.复杂度问题, dp一般都是n^2复杂度, 往往问题会出在空间上, 很多时候需要写滚动数组来避免内存溢出

仔细考虑这道题, 对于一种物品我们有拿和不拿两种操作, 但如果想拿的话要满足一定能装下这个情况, 否则就只能不拿

              dp[i][j] = max{dp[i-1][j], dp[i-1][j-w[i]] + v[i] | j >= w[i] }

              dp[i][j] = dp[i-1][j]  (j < w[i])

这个是我的初始版本,想的比较简单,因为体重要求是3500*13000的大小,如果开这么大一个数组的话大概是180MB,差不多一个红警的大小了,一定会爆

​
//01背包问题
#include <iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int main()
{
    int n, m;
    scanf("%d%d",&n, &m);
    int w[n+5], d[n+5];
    for(int i = 1; i <= n; i++)
        scanf("%d%d",&w[i], &d[i]);
    //从前i种物品选择j重量的物品的价值最大值
    int dp[n+5][m+5];
    //因为数组开的比较大所以省去考虑边界情况
    memset(dp, 0, sizeof(dp));
    for(int i = 1; i <= n; i++){
        for(int j= 1; j <= m; j++){
            //状态转移方程
            if(j >= w[i]){
                if(dp[i-1][j] > dp[i-1][j-w[i]]+d[i])
                    dp[i][j] = dp[i-1][j];
                /*else   这是我前期考虑错的一个问题,如果w[i]大于j且还选第i种的话直接就是0了
                    dp[i][j] = dp[i-1][j-w[i]]+d[i];*/
            }
            else
                dp[i][j] = dp[i-1][j];
        }
    }
    printf("%d\n",dp[n][m]);
    return 0;
}

​

然后这是后期采用滚动数组进行优化~由于每个dp至于上一行的垂直对应元素和左面某个元素(j-w[i])有关,所以可以采用从右到左的滚动数组

//01背包问题
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int main()
{
    int n, m;
    scanf("%d%d",&n, &m);
    int w[n+5], d[n+5];
    for(int i = 1; i <= n; i++)
        scanf("%d%d",&w[i], &d[i]);
    //从前i种物品选择j重量的物品的价值最大值
    int dp[m+5];
    //因为数组开的比较大所以省去考虑边界情况
    memset(dp, 0, sizeof(dp));
    for(int i = 1; i <= n; i++){
        //采用滚动数组从右到左来求
        for(int j= m; j >= 0; j--){
            //状态转移方程
            if(w[i] <= j)
                dp[j] = max(dp[j], dp[j-w[i]]+d[i]);
        }
    }
    printf("%d\n",dp[m]);
    return 0;
}

滚动数组也有两种写法, 我最终使用了下面这种写法, 因为比较容易理解

//01背包问题
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include <string>
#include <vector>
#include <queue>
using namespace std;
#define ms(x, n) memset(x,n,sizeof(x));
typedef  long long LL;
const LL maxn = 3410;
const LL maxm = 12900;

int n, m, w[maxn], v[maxn];
int dp[2][maxm]; //从前i种物品选择j重量的物品的价值最大值
int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i++)
        cin >> w[i] >> v[i];
    ms(dp, 0);
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            if(j >= w[i])
                dp[1][j] = max(dp[0][j], dp[0][j-w[i]]+v[i]);
            else dp[1][j] = dp[0][j];
        }
        for(int k = 0; k <= m; k++)
            dp[0][k] = dp[1][k];
    }

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

\