被迫营业的一天——背包问题

前言:背包问题是一类很经典的动态规划问题,知识涵盖可以占常见动态规划类型里的10%。

 

由于前辈们总结的太好,现在基本上见不到单纯的模板背包问题了,命题人多会结合一些其他知识点进行综合考察,当然背包问题本身就涉及了许多不同的算法。

 

然后就吉林省赛而言,我认为背包问题的难度覆盖在银牌区间,也就是说,赛场上你能确定这一道背包题并且顺风做出来就是可以拿下省二的。

 

经典背包问题讲的是给定一个有限承重为W的背包,再给任意个物品。每个物品有其价值m和重量w。计算在背包不超重前提下所能得到最大价值和。

 

经典背包问题:

01背包问题:每件物品只有一个,故只有选和不选两种情况。

入门思想:暴力枚举——开辟二维数组,初始化,行标 i 作为物品编号,列标 j 作为背包当前重量,数组值存放当前所能得到最大价值。

 

从数组左上角起始点,开始依次尝试放入每一件物品,按照遍历顺序,每一个位置得到的就都会是编号至 i 重量为 j 时所能得到的最大价值。

 

如此,在我们遍历完成整个二维数组之后,最后的[n][W]位置必然为全局最优解。

 

看完过程后,想必大家都发现了实际作用的其实只有对角线附近的那层阶梯式,每一行价值其实都是在上一行价值的基础上增添过来的我们对列标的使用其实是一维的,那不妨就用数组下标来替代掉,反复处理一个一维数组就好了。

 

而实际上代码就应该是线性的dp,开二维数组是对空间的严重浪费,只是为了帮助大家理解。但这个任务我要布置给学弟学妹们自行研究(一定要研究,搞明白后的顺便去看看滚动数组,完事来和我说理解)。

for (int i = 0; i < n; i++)
    for (int j = 0; j <= W; j++)
        if (j < w[i])
            dp[i + 1][j] = dp[i][j];
        else
            dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
printf("%d", dp[n][W]);

   打表

#include<iostream>
using namespace std;
int dp[505][505],v[505], w[505];
int main()
{
    int n, W;
    cin >> n >> W;
    for (int i = 0; i < n; i++)
        cin >> w[i] >> v[i];
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j <= W; j++)
            if (j < w[i]) //放不下时不改变重量,价值传递给编号第i+1件物品
                dp[i + 1][j] = dp[i][j];
            else
                dp[i + 1][j] = max (dp[i][j], dp[i][j - w[i]] + v[i]); //放的下时就记录最大值
        cout<<"当前判断至第"<<i+1<<"个物品:重量为 "<<w[i]<<" 价值为 "<<v[i]<<endl;
        for (int k1 = 0; k1 <= W; k1++)
            cout<<k1<<"\t";
        cout<<"(此行为背包当前重量)"<<endl;
        for (int k1 = 0; k1 <= n; k1++)
        {
            for (int k2 = 0; k2 <= W; k2++)
                cout << dp[k1][k2] << "\t";
            cout << endl;
        }
        cout << endl << endl;
    }
    cout<<dp[n][W];
    return 0;
}

  

完全背包问题:每件物品有无限个。

入门思想:对于完全背包问题,我知道大家很容易想要去用贪心做,因为好像优先 价值与重量比最大 的策略呼之欲出。这里强调一下千万不要!贪心解决背包问题不可取,可以说是必漏情况。赛场上造成的结果就会是大家WA一发之后就不再看了。

 

举一个经典的例题:你有面值2元、5元、7元的三种纸币,每种纸币你有无限张。要购买一件27元的商品,如何用最少的纸币数付清?

我们第一反应策略就是一直用最大面值的纸币:结果是7 7 7 2 2 2总共用了6张,而最优解实际为7 5 5 5 5只用了5张。所以,贪心需谨慎~~

 

然而我们考虑这样一种背包问题:在选择一件物品装入背包时,可以只装入物品的一部分,而不一定要全部装入背包。这是延伸出的另一类背包问题——部分背包,这时就是使用贪心算法求解了,问题也就变成了考贪心算法的基本题,今天不讲。

 

好,回归正题,回顾我们之前01背包的思想,发现面对完全背包依旧可行。所以我们继续暴力枚举,遍历出每个位置所能得到最大价值,最后的[n][W]位置必然为全局最优解。和01背包一模一样的原因,代码同样是该优化成一维的,交给大家了。

for (int i =0; i < n ; i ++)
	for (int j =0; j <= W ; j ++)
		if(j < w [ i ])
			dp [ i +1][ j ]= dp [ i ][ j ];
		else
			dp [ i +1][ j ]= max ( dp [ i ][ j ] , dp [ i +1][ j - w [ i ]]+ v [ i ]);
printf ( "%d" , dp [ n ][ W ]);

  打表

#include<iostream>
using namespace std;
int dp[505][505], v[505], w[505];
int main()
{
    int n, W;
    cin >> n >> W;
    for (int i = 0; i < n; i++)
        cin >> w[i] >> v[i];
    for (int i = 0; i < n; i++)
    {
        for (int j = 0; j <= W ; j ++)
            if (j < w [ i ])
                dp [ i + 1][ j ] = dp [ i ][ j ];
            else
                dp [ i + 1][ j ] = max ( dp [ i ][ j ], dp [ i + 1][ j - w [ i ]] + v [ i ]);
        cout << "当前判断至第" << i + 1 << "个物品:重量为 " << w[i] << " 价值为 " << v[i] << endl;
        for (int k1 = 0; k1 <= W; k1++)
            cout << k1 << "\t";
        cout << "(此行为背包当前重量)" << endl;
        for (int k1 = 0; k1 <= n; k1++)
        {
            for (int k2 = 0; k2 <= W; k2++)
                cout << dp[k1][k2] << "\t";
            cout << endl;
        }
        cout << endl << endl;
    }
    cout << dp[n][W];
    return 0;
}

  

多重背包问题:每种物品有有限不同个。

入门思想:最朴素的思想——既然每种物品有限个,那我们就能把每种物品都摆开,诶嘿,你看它就又变成了标准的01背包问题!所以多重背包的实现就是在01背包的基础上再加一内层循环,但是这样我们的代码就已经有了三层循环,如果某种物品虽然有限但是个数很多,我们交一发就会容易得到一个时间超限

 

for(int i = 0; i < n; i++)
{
    int v, w, m;
    cin >> w >> v >> m;
    for(int j = W; j >= 0; j--)
        for(int k = 1; k <= m && k * w <= j; k++)
            dp[j] = max(dp[j], dp[j - k * w] + k * v);
}
printf("%d", dp[W]);

  如何优化?——摆开物品的时候别摆太开,从而将一堆物品作为一个整体,进行整体判断选与不选。而为了避免出现之前完全背包提到的问题,具体如何摆是个十分巧妙的活。前辈们总结出来的方法是:摆成log2n份。

问题其实已经抽象成——给定一个数s,问最少可以把s分成多少个数,使得这些数能组成1~s 的所有数。

这个点我觉得说来话长,难以简单的给大家说明白,怕造成误解,所以大家之后有兴趣自己去看吧。然后还有另一种终极的优化方法,用到了单调队列这里就提一下,超纲了。等大家学到了再说,今天不讲。(但是我要一定要吹一波单调栈和单调队列,涉及线性数据结构的题,这俩学好了基本可以乱杀)

#include<iostream>
using namespace std;
int dp[50005];
int main()
{
    int n, W;
    cin >> n >> W;
    for (int i = 0; i < n; i++)
    {
        int v, w, m;
        cin >> w >> v >> m;
        for (int k = 1; m > 0; k <<= 1)
        {
            int mul = min (k, m);
            for (int j = W; j >= w * mul; j--)
                dp[j] = max (dp[j], dp[j - w * mul] + v * mul);
            m -= mul;
        }
    }
    cout << dp[W];
    return 0;
}

 

混合背包问题:物品有多种,每种什么样看题。

当前三种经典背包问题我们都可手撕,那么即便他们混合起来我们也不怕了!

之前提到:多重背包实际上就是01背包的变形,因此我们可以把多重背包和01背包一起处理。若是完全背包直接按照完全背包的模板去做。

那么我们应该做怎样一个处理顺序呢?很容易得到——一定是遇到完全背包就先遍历完把值放在那,反正后续处理出现更大价值的也会给覆盖掉,然后再做一遍01背包,就大功告成啦~

#include<iostream>
using namespace std;
int n, W, s;
int dp[10005], w[1005], v[1005];
int main()
{
    cin >> n >> W;
    for (int i = 1; i <= n; i++)
    {
        int  wx, vx, f;
        cin >> wx >> vx >> f;
        if (!f) //完全背包
        {
            for (int j = wx; j <= W; j++)
                dp[j] = max (dp[j], dp[j - wx] + vx);
        }
        else
        {
            if (f == -1)
                f = 1;
            for (int k = 1; k <= f; k <<= 1) //记录01背包
                w[++s] = wx * k, v[s] = vx * k, f -= k;
            if (f)
                w[++s] = f * wx, v[s] = f * vx;
        }
    }
    for (int i = 1; i <= s; i++) //处理01背包
        for (int j = W; j >= w[i]; j--)
            dp[j] = max (dp[j], dp[j - w[i]] + v[i]);
    cout << dp[W];
    return 0;
}

  

变种背包问题:

二维费用背包问题:经典背包问题上又加一维限制

比如本来我们的背包只有重量的限制,现在又增加了一维体积的限制。入门思想:开辟二维数组,行标和列表分别记录二维的限制条件,在经典背包基础上再加一层遍历循环。

#include<iostream>
using namespace std;
int dp[505][505];
int main()
{
    int n, W, L;
    cin >> n >> W >> L;
    for (int i = 0; i < n; i++)
    {
        int v, w, l;
        cin >> v >> w >> l;
        for (int j = W; j >= w; j--)
            for (int k = L; k >= l; k--)
                dp[j][k] = max (dp[j][k], dp[j - w][k - l] + v);
    }
    cout << dp[W][L];
    return 0;
}

  

分组背包问题:物品多了组别,每组物品只能选一个

 

有依赖背包问题:物品能不能选,多了限制条件

 

背包方案数问题

求背包方案问题

 

 

 测试数据

4 5
1 2
2 4
3 4
4 5

8

  01背包

4 5
1 2
2 4
3 4
1 5

25

  完全背包

4 5
1 2 3
2 4 1
3 4 3
4 5 2

10

  多重背包

4 5
1 2 -1
2 4 1
3 4 0
4 5 2

8

  混合背包    -1对应01、0对应完全

4 5 6
3 1 2
4 2 4
5 3 4
6 4 5

8

  二维费用背包

 

 模板:

for(int i = 0; i < n; i++)
        for(int j = W; j >= w[i]; j--)
            dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
printf("%d", dp[W]);

  01背包

for (int i = 0; i < n; i++)
{
    cin >> w >> v;
    for (int j = w; j <= W ; j++)
        dp [ j ] = max ( dp[ j ], dp[ j - w] + v);
}
printf ("%d", dp[W]);

  完全背包

for (int i = 0; i < n; i++)
{
    int v, w, m;
    cin >> w >> v >> m;
    for (int k = 1; m > 0; k <<= 1)
    {
        int mul = min(k, m);
        for (int j = W; j >= w * mul; j--)
            dp[j] = max(dp[j], dp[j - w * mul] + v * mul);
        m -= mul;
    }
}
printf("%d", dp[W]);

  多重背包

posted @ 2021-06-03 14:01  anyiya  阅读(45)  评论(0编辑  收藏  举报