0/1背包入门题解

背包问题的题目集来自CSDN上的夜深人静讲算法栏目。

HDU2602

最最基础的背包问题。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 5;


int main()
{
    int t;
    cin >> t;
    while (t--)
    {
        int dp[maxn] = {0};
        int n, v;
        cin >> n >> v;
        int val[maxn], vol[maxn];
        for (int i = 1; i <= n; i++) cin >> val[i];
        for (int i = 1; i <= n; i++) cin >> vol[i];

        for (int i = 1; i <= n; i++)
            for (int j = v; j >= vol[i]; j--)
                dp[j] = max(dp[j], dp[j-vol[i]]+val[i]);
        
        cout << dp[v] << endl;
    }
}

设计状态

\(dp[i][j]\)表示前\(i\)个物品恰好放入容量为\(j\)的背包中的最大价值。

转移方程

二维转移方程来表示可以为\(dp[i][j] = max(dp[i-1][j], dp[i-1][j-vol[i]]+val[i])\)

滚动数组优化

我们发现每次得到\(dp[i][j]\)只需要用到“上一层”的数据,所以进行降维来优化空间:
\(dp[i]\)表示体积为i的背包中最多能容纳的价值。那么可以得到转移方程:

\[dp[i] = max(dp[i], dp[i-vol[i]+val[i]) \]

现在看核心式子:

for (int i = 1; i <= n; i++)
    for (int j = v; j >= vol[i]; j--)
        dp[j] = max(dp[j], dp[j-vol[i]]+val[i]);

在这里我们的第一层枚举了\(i\),即当前进行到的物品的位置。
但是当dp跑到\(dp[j]\)时,\(dp[j-vol[i]]\)已经产生,假如\(j\)足够大,则\(dp[j-vol[i]-vol[i]]\)必定也已经产生
所以照这样跑下去,就是一个物品可以无限次选择的情况。但是在一个物品只能选择一次的情况下。我们希望在跑到\(dp[i]\)\(dp[i-vol[i]]\)不要出现。
因此我们需要把第二层循环中从大到小进行状态转移,这是降维的核心优化。

HDU1203

只是将上一种背包问题中的求最大值变成了求最小值
添加的内容就是一个初始化,还有一个把转移方程中的\(max\)换成\(min\)的过程。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e4 + 5;

double dp[maxn];
int vol[maxn];
double val[maxn];
int main()
{
    int n, m;
    while (cin >> n >> m && (n || m))
    {
        for (int i = 0; i <= n; i++) dp[i] = 1;

        for (int i = 1; i <= m; i++) cin >> vol[i] >> val[i];
        for (int i = 1; i <= m; i++) val[i] = 1-val[i];

        for (int i = 1; i <= m; i++)
            for (int j = n; j >= vol[i]; j--)
                dp[j] = min(dp[j], dp[j-vol[i]]*val[i]);
        
        printf("%.1lf%%\n", 100-(dp[n]*1.0*100));
    }
}

值得注意的是这里的转移方程中的*可以通过log优化成加法,不过意义并不是很大。
同时,这里的数据大小也体现出了降维优化的必要性,如果变成二维数组肯定是塞不下的。

HDU1171

背包板子。
将存在性问题化成最大值问题的关键是将一个事物的体积看作权值。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e3+5;

int dp[maxn*5*50];
int v[maxn], m[maxn];

int main()
{
    int n;
    while (~scanf("%d", &n))
    {
        if (n < 0) break;
        memset(dp, 0, sizeof(dp));
        int sum = 0;
        for (int i = 1; i <= n; i++) {cin >> v[i] >> m[i]; sum += v[i] * m[i];}
        
        for (int i = 1; i <= n; i++)
            for (int k = 1; k <= m[i]; k++)
                for (int j = sum/2; j >= v[i]; j--)
                    dp[j] = max(dp[j], dp[j-v[i]]+v[i]);

        cout << sum - dp[sum/2] << " " << dp[sum/2] << endl;
    }
}

P2925

01背包存在性问题板子。

HDU1864

比较麻烦的一道01背包板子题。包括但不限于输入输出处理,浮点化整形,化01背包。
值得学习的一题。

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1000 * 30 * 100 + 5;
int dp[maxn];
int cnt;       //合法发票数
int val[maxn]; //每张合法发票的额度
int main()
{
    double v;
    int n;
    while (scanf("%lf%d", &v, &n) == 2 && n)
    {
        cnt = 0;
        int max_val = (int)(v * 100);
        for (int i = 1; i <= n; i++)
        {
            int num;
            char type; //商品类型
            double va = 0, vb = 0, vc = 0;
            scanf("%d", &num);
            bool flag = true; //合法发票
            while (num--)
            {
                scanf(" %c:%lf", &type, &v);
                if (type == 'A') va += v;
                else if (type == 'B') vb += v;
                else if (type == 'C') vc += v;
                else flag = false; //含有其他种类商品
            }
            if (flag && va <= 600 && vb <= 600 && vc <= 600 && va + vb + vc <= 1000)
                val[++cnt] = (int)((va + vb + vc) * 100);
        }
        memset(dp, 0, sizeof(dp));
        for (int i = 1; i <= cnt; i++)
            for (int j = max_val; j >= val[i]; j--)
                dp[j] = max(dp[j], dp[j - val[i]] + val[i]); //经过优化的一维数组类型的背包方程
        printf("%.2lf\n", (dp[max_val]) / 100.0);
    }
    return 0;
}

HDU2126

考虑状态:最大值,方案数。建立二维背包:\(dp[i][j]\)分别表示容量为\(i\)方案数为\(j\)有多少种。
状态转移方程:\(dp[j][k] += dp[j-val[i][k-1]\),意思很简单,保证前一个i已经被处理过,所以容量为j-val[i]的背包肯定已经得到,这时把所有的可能性累加起来就好了。
还有一个需要注意的点是初始化:\(dp[i][0]\)表示没有任何一种方案,也即没有任何一个物品,所以有1种。

#include <bits/stdc++.h>
using namespace std;

void solve()
{
    int n, m;
    cin >> n >> m;
    int dp[505][35] = {0};
    int val[35];
    for (int i = 1; i <= n; i++) cin >> val[i];
    sort(val+1, val+1+n);
    int ans = 0, sum = 0;
    for (int i = 1; i <= n; i++)
    {
        sum += val[i];
        if (sum > m && !ans) {ans = i-1; break;}
    }
    if (val[1] > m) {printf("Sorry, you can't buy anything.\n"); return;}
    else if(sum <= m) {printf("You have 1 selection(s) to buy with %d kind(s) of souvenirs.\n", n); return;}
    
    for (int i = 0; i <= m; i++) dp[i][0] = 1;

    for (int i = 1; i <= n; i++)
        for (int j = m; j >= val[i]; j--)
            for (int k = ans; k >= 1; k--)
                dp[j][k] += dp[j-val[i]][k-1];

    printf("You have %d selection(s) to buy with %d kind(s) of souvenirs.\n", dp[m][ans], ans);
}

int main()
{
    int t;
    cin >> t;
    for (int i = 1; i <= t; i++)
    {
        solve();
    }
} 

HDU2955

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 1e2 + 5;
const int maxsum = 1e6 + 5;

int vol[maxn];
double val[maxn];
double dp[maxsum];

void solve()
{
    double pi;
    int n;

    memset(dp, 0, sizeof(dp));
    memset(vol, 0, sizeof(vol));
    memset(val, 0, sizeof(val));

    scanf("%lf %d", &pi, &n);
    int sum = 0;
    for (int i = 1; i <= n; i++)
    {
        scanf("%d %lf", &vol[i], &val[i]);
        sum += vol[i];
    }
    dp[0] = 1.0;    
    for (int i = 1; i <= n; i++)
        for (int j = sum; j >= vol[i]; j--)
            dp[j] = max(dp[j], dp[j-vol[i]]*(1.0-val[i]));

    for (int i = sum; i >= 0; i--)
    {
        //cout << dp[i] << endl;
        if (dp[i] >= (1.0-pi)) {printf("%d\n", i); break;}
    }
}

int main()
{
    int t;cin>>t;
    while(t--){
        solve();
    }
}

普普通通的概率dp,但是因为我忘关流同步导致wa了n多发,maxsum的范围是个小坑点。

PKU2184

很绕的一道题。

#include <iostream>
#include <algorithm>
#include <stdio.h>
#define PII pair<int, int>
using namespace std;

const int INF = 1e6;
const int MAXM = 1e5 + 5;
const int MAXN = 111;

PII a[MAXN];
int dp[MAXM];
int n;

int main()
{
    while (scanf("%d", &n) != EOF)
    {
        int sum = 0;
        for (int i = 0; i < n; i++)
        {
            scanf("%d%d", &a[i].first, &a[i].second);
            if (a[i].first > 0) sum += a[i].first;
        }
        sort(a, a + n);
        for (int i = 0; i <= sum; i++) dp[i] = -INF;
        dp[0] = 0;
        for (int i = n - 1; i >= 0; i--)
        {
            int f = a[i].first, s = a[i].second;
            if (f < 0 && s < 0) continue;
            if (f >= 0)
            {
                for (int j = sum - f; j >= 0; j--)
                    dp[j + f] = max(dp[j + f], dp[j] + s);
            } else {
                for (int j = -f; j <= sum; j++)
                    dp[j + f] = max(dp[j + f], dp[j] + s);
            }
        }
        int ans = 0;
        for (int i = 0; i <= sum; i++)
            if (dp[i] >= 0)
                ans = max(ans, i + dp[i]);
        printf("%d\n", ans);
    }

    return 0;
}

这道题让我逐渐意识到背包问题的本质,其实就是在当有两个变量时,要求限制其中的某一个变量,来求某一个线性组合的最值。zheda
这道题目中要求对s,f有限定条件:\(sum_s>=0,sum_f>=0,\)来求\(s+f\)的最大值。关键在于求\(sum_s\)\(sum_f\)的过程。

这道题一个很重要的地方就是负数的处理,需要先进行排序。
附上另一种更常规的处理方法。

#include <stdio.h>
#include <string.h>
#include <iostream>
#include <algorithm>
using namespace std;
typedef pair<int, int> P;
const int INF = 0x3f3f3f3f;
const int maxn = 1e2 + 5;
const int maxval = 2e6 + 5;

int a[maxn], b[maxn];
int dp[maxval+5];
int main()
{
    int n;
    scanf("%d", &n);
    for (int i = 1; i <= n; i++)
        scanf("%d%d", &a[i], &b[i]);
    memset(dp, -INF, sizeof(dp));
    dp[100000] = 0;
    for (int i = 1; i <= n; i++)
    {
        if (a[i] > 0)
        {
            for (int j = maxval; j >= a[i]; j--)
                dp[j] = max(dp[j], dp[j - a[i]] + b[i]);
        }
        else
        {
            for (int j = 0; j <= maxval + a[i]; j++)
                dp[j] = max(dp[j], dp[j - a[i]] + b[i]);
        }
    }
    int ans = 0;
    for (int i = 100000; i <= maxval; i++)
    {
        if (dp[i] > 0)
            ans = max(ans, dp[i] + i - 100000);
    }
    cout << ans << endl;
    return 0;
}

这种做法比上一种更加便于理解:把负数化成正数,然后将分界点100*1000作为0点:dp[100000]=0。
若a[i]<0,则j-a[i]其实是j+(-a[i]),为了让dp[j+(-a[i])]==0,所以需要从左往右枚举。

P1507

#include <bits/stdc++.h>
using namespace std;
const int maxn = 4e2 + 5;

int read()
{
    char ch=getchar();
    int x=0,f=1;
    while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
    return x*f;
}

int main()
{
    int maxv = read(), maxm = read();
    int n = read();
    int v[maxn], m[maxn], cal[maxn];
    int dp[maxn][maxn] = {0};
    for (int i = 1; i <= n; i++)
    {
        v[i] = read(), m[i] = read(), cal[i] = read();
    } 

    for (int i = 1; i <= n; i++)
        for (int j = maxv; j >= v[i]; j--)
            for (int k = maxm; k >= m[i]; k--)
                dp[j][k] = max(dp[j][k], dp[j-v[i]][k-m[i]] + cal[i]);
    
    cout << dp[maxv][maxm] << endl;
}   

普普通通的二维背包问题,有几个变元就有几个**“除了i枚举个数循环”**以外的循环。
`

#HDU4502

坑点:相同的区间价值要更新较大的那一个。写起来有点像最短路的松弛操作,不过本质上还是dp问题,和一般的背包不同的是,这里的forj循环不需要考虑方向的问题,因为不用在意同一段区间被选择多次。
题目要求工资最大值,所以dp[i]表示的就是工资最大值,然后对于某一天i,对它之前的每一天j进行枚举。

```c++
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e2 + 5;

int read()
{
    char ch=getchar();
    int x=0,f=1;
    while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
    return x*f;
}

int mp[maxn][maxn];
int dp[maxn];

void solve()
{
    int m = read(), n = read();
    memset(dp, 0, sizeof(dp));
    memset(mp, 0, sizeof(mp));
    for (int i = 1; i <= n; i++)
    {
        int t1 = read(), t2 = read();
        mp[t1][t2] = max(mp[t1][t2], read());
    }

    for (int i = 1; i <= m; i++)
        for (int j = i; j >= 1; j--)//for (int j = 1; j <= i; j++)
            dp[i] = max(dp[i], dp[j-1] + mp[j][i]);
    
    cout << dp[m] << endl;
}

int main()
{
    int t = read();
    while (t--)
    {
        solve();
    }
}

HDU3448

痛苦的一题,三维数组开不出来,翻了题解才知道用dfs艹过去,结果dfs又写错了好几遍,最后搬其他题解的代码作为dfs解背包问题的板子来用了。

#include <bits/stdc++.h>
using namespace std;
int n, k, w, ans, a[50];
void dfs(int t, int num, int sum)
{
    ans = max(ans, sum);
    if (num == k + 1) return;
    if (sum + a[num] <= w && t + 1 <= n) dfs(t + 1, num + 1, sum + a[num]);
    dfs(t, num + 1, sum);
}
int main()
{
    int i, sum;
    while (scanf("%d%d", &n, &w) != EOF)
    {
        sum = 0;
        scanf("%d", &k);
        for (i = 1; i <= k; i++) scanf("%d", &a[i]);
        sort(a + 1, a + k + 1, greater<int>()); 
        for (i = 1; i <= n; i++) sum += a[i];
        if (sum <= w)
        {
            printf("%d\n", sum);
            continue;
        }
        ans = 0;
        dfs(0, 1, 0);
        printf("%d\n", ans);
    }
    return 0;
}

HDU2660

刚被3448TLE到抑郁,找个水题做一做又因为memset的原因卡了十来分钟。
要改改数组初始化的习惯了。

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 20 + 5;

ll read()
{
    char ch=getchar();
    ll x=0,f=1;
    while(ch<'0'||ch>'9'){if(ch=='-') f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9') x=x*10+(ch^48),ch=getchar();
    return x*f;
}

int dp[1000][20];
struct Pac
{
    int val, vol;
}pac[maxn];

void solve()
{
    ll n = read(), k = read();
    for (int i = 1; i <= n; i++) pac[i].val = read(), pac[i].vol = read();
    ll w = read();
    memset(dp, 0, sizeof(dp));
    for (int i = 1; i <= n; i++)
        for (int j = w; j >= pac[i].vol; j--)
            for (int m = 1; m <= k; m++)
                dp[j][m] = max(dp[j][m], dp[j-pac[i].vol][m-1] + pac[i].val);

    cout << dp[w][k] << endl;
}

int main()
{
    int t;cin>>t;
    while(t--){
        solve();
    }
}

PKU1157

典型的dp,从小到大找就能找出dp式子\(dp[i][j] = max(dp[i-1][j-1]+a[i][j], dp[i][j-1])\)
然后顺着推就行了,感觉跟背包关系不大。

posted @ 2021-03-24 15:27  Clo91eaf  阅读(64)  评论(0)    收藏  举报