背包问题小结

题目来源于 https://www.acwing.com/problem/
acwing题库的2~12题。

大概有这些类别:
在这里插入图片描述
个人觉得比较难的还是输出方案。

01背包问题: 每个物品只能选一次
F[i][j] = max(F[i - 1][j - v[i]] + w[i],F[i - 1][j])
代表第i个物品选与不选的状态
第一维状态可以省点变成
F[j] = max(F[j],F[j - v[i]] + w[i])。但是j要逆序枚举。这样的话相当于是算F[j]的时候,F[j]和F[j - v[i]]都没有算过,等于是F[i - 1][j]和F[i - 1][j - v[i].

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int maxn = 1e3;
int v[maxn],w[maxn],s[maxn];
int dp[maxn];

int main()
{
    int n,m;scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d%d",&v[i],&w[i]);
    }
    
    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]);
        }
    }
    
    printf("%d\n",dp[m]);
    return 0;
}

完全背包问题: 每个物品可以选无限次。
F[i][j] = max(F[i - 1][j],F[i - 1][j - k * v[i]] + k * w[i]]。
k的话要多一维循环。

但在滚动数组下,只要把 j 正序枚举,就可以省去k。
简化掉一维:F[j] = max(F[j],F[j - v[i]])。
原理是相当于子状态都是遍历过的状态,那么已经经过了选择,那么每次选择的子状态都是经过选择该物品的,该物品相当于可以无限取。

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int maxn = 1e3;
int v[maxn],w[maxn],s[maxn];
int dp[maxn];

int main()
{
    int n,m;scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d%d",&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]);
        }
    }
    
    printf("%d\n",dp[m]);
    return 0;
}

多重背包问题: 物品数量有限,且每个物品数量不同
一般来说有3种方法:暴力,二进制优化,单调队列优化。
比较常用而且也是比较方便的就是二进制优化。

暴力的转移:F[j] = F[j - k* v[i]] + w[i] . k ≤ s[i]

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int maxn = 1e3;
int v[maxn],w[maxn],s[maxn];
int dp[maxn];

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

二进制优化:
每个物品有s[i]个,我们知道二进制不同次幂相加可以组成任何数。那么将s[i]拆成二进制次幂的物品,组成的时候不就相当于可以组成0 ~ s[i]的任何物品数了吗。

经过了这样的拆解,实际上可以变成01背包。这种形式个人认为最为简便,也最适合模板化。

类似于:s[i] = 5,那么将5拆成2 3, s[i] = 6, 6拆成2, 4。。。

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int maxn = 1e3 + 7;
int n,m;
int w[maxn],v[maxn],s[maxn];
int a[30005],b[30005];
int dp[2005];

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d%d%d",&w[i],&v[i],&s[i]);
    }
    int cnt = 0;
    for(int i = 1;i <= n;i++)
    {
        for(int j = 1;j <= s[i];j <<= 1)
        {
            a[++cnt] = j * w[i];
            b[cnt] = j * v[i];
            s[i] -= j;
        }
        if(s[i])
        {
            a[++cnt] = s[i] * w[i];
            b[cnt] = s[i] * v[i];
        }
    }
    
    for(int i = 1;i <= cnt;i++)
    {
        for(int j = m;j >= a[i];j--)
        {
            dp[j] = max(dp[j],dp[j - a[i]] + b[i]);
        }
    }
    
    printf("%d\n",dp[m]);
    return 0;
}

模板化:
数量过大就完全背包,数量一定就二进制拆分后直接01背包。

void multiplebag(int w,int v,int s)
{
    if(w * s >= m)
    {
        completebag(w, v);
        return;
    }
    
    for(int k = 1;k <= s;k <<= 1)
    {
        Zeroonebag(w * k, v * k);
        s -= k;
    }
    Zeroonebag(w * s, v * s);
}

单调队列优化:

每个物品有s[i]个。这个决策过程是单纯针对选几个这个物品的。

对于F[i][now]来说。now可以拆成 j + k * w[i]。那么F[i][now]这个状态最多可以选k个这个物品(假设k ≤ s[i])。
事实上,选k个这个物品对应的子状态是F[i][j],选k - 1个这个物品对应的子状态是F[i][j + w[i]],选k - 2个这个物品对应的子状态是F[i][j + w[i] * 2]

事实上,在选择这个物品的阶段下,这些子状态都已经计算出来了。能否利用这些子状态O(1)算出F[now]呢?

这就利用到了单调队列的算法。

(一般来说w代表价值(wealth),v代表体积(volume,但有时候我个人会把w当做(weight),v当做价值(value))

假设从F[i - 1][j + x * w[i]]转移过来
那么相当于选择了(k - x)个物品
F[i][now] = F[i - 1][j + x * w[i]] + (k - x) * v[i].
令 y = j + x * w[i]。0 ≤ x ≤ k
则等价于F[i][now] = max{ F[i - 1][y] + (now - y) / w[i] * v[i] }。
now / w[i] * v[i]部分都是相同的
于是变成了维护max{ F[i - 1][y] - y / w[i] * v[i] } j ≤ y ≤ now
用递减单调维护这个值,每次队首取出的下标就可以O(1)算出F[i][now]。

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

int w[1005],v[1005],s[1005];
int f[20005],g[20005],q[20005];

int main()
{
    int n,m;scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)scanf("%d%d%d",&w[i],&v[i],&s[i]);
    
    for(int i = 1;i <= n;i++)
    {
        memcpy(g,f,sizeof(f));
        for(int j = 0;j < w[i];j++)
        {
            int l = 0,r = -1;
            for(int k = j;k <= m;k += w[i])
            {
                f[k] = g[k];
                
                while(l <= r && k - s[i] * w[i] > q[l])l++;
                
                if(l <= r)f[k] = max(f[k],g[q[l]] + (k - q[l]) / w[i] * v[i]);
                
                while(l <= r && g[q[r]] - (q[r] - j) / w[i] * v[i] <= g[k] - (k - j) / w[i] * v[i])
                {
                    r--;
                }
                
                q[++r] = k;
            }
        }
    }
    
    printf("%d\n",f[m]);
    return 0;
}

混合背包问题: 每个物品可能有1个,可能有多个,可能有无数个
分类讨论即可

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long ll;

const int maxn = 1005;

struct Node
{
    int w,v,s;
}a[maxn];

int f[maxn];
int n,m;

void Zeroonebag(int w,int v)
{
    for(int i = m;i >= w;i--)f[i] = max(f[i],f[i - w] + v);
}

void completebag(int w,int v)
{
    for(int i = w;i <= m;i++)f[i] = max(f[i],f[i - w] + v);
}

void multiplebag(int w,int v,int s)
{
    if(w * s >= m)
    {
        completebag(w, v);
        return;
    }
    
    for(int k = 1;k <= s;k <<= 1)
    {
        Zeroonebag(w * k, v * k);
        s -= k;
    }
    Zeroonebag(w * s, v * s);
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d%d%d",&a[i].w,&a[i].v,&a[i].s);
    }
    
    for(int i = 1;i <= n;i++)
    {
        if(a[i].s == 0)completebag(a[i].w, a[i].v);
        else if(a[i].s == -1)Zeroonebag(a[i].w, a[i].v);
        else if(a[i].s > 0)multiplebag(a[i].w,a[i].v,a[i].s);
    }
    
    printf("%d\n",f[m]);
}

二维费用的背包问题: 和普通背包一样,多开一维状态即可。

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

int f[105][105];
int v[105],m[105],w[105];

int main()
{
    int N,V,M;scanf("%d%d%d",&N,&V,&M);
    for(int i = 1;i <= N;i++)
    {
        scanf("%d%d%d",&v[i],&m[i],&w[i]);
    }
    
    for(int i = 1;i <= N;i++)
    {
        for(int j = V;j >= v[i];j--)
        {
            for(int k = M;k >= m[i];k--)
            {
                f[j][k] = max(f[j][k],f[j - v[i]][k - m[i]] + w[i]);
            }
        }
    }
    
    printf("%d\n",f[V][M]);
    return 0;
}

分组背包问题: 每个组别有多个物品,每个组最多只能选一个
只知道暴力方法

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

int val[105][105],w[105][105],cnt[105];
int dp[10005];

int main()
{
    int n,v;scanf("%d%d",&n,&v);
    for(int i = 1;i <= n;i++)
    {
        int k;scanf("%d",&k);
        cnt[i] = k;
        for(int j = 1;j <= k;j++)
        {
            scanf("%d %d",&w[i][j],&val[i][j]);
        }
    }
    
    for(int i = 1;i <= n;i++)
    {
        for(int j = v;j >= 0;j--)
        {
            for(int k = 1;k <= cnt[i];k++)
            {
                if(w[i][k] <= j)dp[j] = max(dp[j],dp[j - w[i][k]] + val[i][k]);
            }
        }
    }
    
    printf("%d\n",dp[v]);
    return 0;
}

有依赖的背包问题: 你要选x物品,就必须选择某个其他物品(x的父节点)

和 ACWING286. 选课 一模一样。本质就是个树上背包

定义状态F[i][j],代表以第i个物品为根节点时且容量为j的最大获得价值(且这个物品必须选择)。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

vector<int>G[105];

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

void DP(int x)
{
    for(int i = 0;i < G[x].size();i++)
    {
        int y = G[x][i];
        DP(y);
        for(int j = m;j >= 0;j--)
        {
            for(int t = 0;t <= j;t++)
            {
                f[x][j] = max(f[x][j],f[x][j - t] + f[y][t]);
            }
        }
    }
    
    for(int i = m;i >= v[x];i--)f[x][i] = f[x][i - v[x]] + w[x];
    for(int i = 0;i < v[x];i++)f[x][i] = 0;
    
}

int main()
{
    scanf("%d%d",&n,&m);
    int rt = 1;
    for(int i = 1;i <= n;i++)
    {
        int p;
        scanf("%d%d%d",&v[i],&w[i],&p);
        if(p == -1)rt = i;
        else G[p].push_back(i);
    }
    
    DP(rt);
    
    printf("%d\n",f[rt][m]);
    return 0;
}

背包问题求方案数: 求最优结果的方案数
和求最短路数目一样
用一个数组维护一下方案数就好了

#include <cstdio>
#include <cstring>
#include <algorithm>

using namespace std;

const int mod = 1e9 + 7;
int v[1005],w[1005];
int f[1005],c[1005];

int main()
{
    int n,m;scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d%d",&v[i],&w[i]);
    }
    
    for(int i = 0;i <= m;i++)c[i] = 1;
    for(int i = 1;i <= n;i++)
    {
        for(int j = m;j >= v[i];j--)
        {
            if(f[j] < f[j - v[i]] + w[i])
            {
                c[j] = c[j - v[i]];
                f[j] = f[j - v[i]] + w[i];
            }
            else if(f[j] == f[j - v[i]] + w[i])
            {
                c[j] += c[j - v[i]];
                c[j] %= mod;
            }
        }
    }
    
    printf("%d\n",c[m] % mod);
    return 0;
}

背包问题求具体方案: 需要保证字典序最小
感觉这个比较麻烦了。

方案输出实际上就是回溯。我们知道了结果F[n][m],那么求这是由哪些状态转移过来的。
比较方便控制的是,能转移就转移(这个物品能取就取),但这样字典序就不一定是最小的了。

可以反过来背包。从第n个物品开始,dp到第一个物品。
那么回溯的时候是从第一个物品回溯到第n个物品。

这样字典序就是最小的了

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <vector>
#include <iostream>
using namespace std;

const int mod = 1e9 + 7;
vector<int>ans;
int v[1005],w[1005];
int f[1005][1005],c[1005][1005];

int main()
{
    int n,m;scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++)
    {
        scanf("%d%d",&v[i],&w[i]);
    }
    
    for(int i = n;i >= 1;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]);
        }
    }

    int i = 1,j = m;
    while(i <= n && j >= 0)
    {
        if(i == n && j >= v[i])//背包可能有剩余
        {
            printf("%d",i);
            break;
        }
        if(j - v[i] >= 0 && f[i][j] == f[i + 1][j - v[i]] + w[i])
        {
            printf("%d ",i);
            j -= v[i];
        }
        i = i + 1;
    }
    
    return 0;
}

发布了676 篇原创文章 · 获赞 18 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/tomjobs/article/details/104202283
今日推荐