背包问题&&方案数&&具体方案

1. \(01\) 背包求恰好装满方案数

HERE
f[i][j]: 从前i个物品中选,体积正好为j的方案数
状态转移方程和 \(01\) 背包问题求最大价值是一样的

朴素版

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110, M = 10010;

int n, m, v[N];
int f[N][M];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )   cin >> v[i];
    
    // 从前i个物品中选,体积正好为0的方案数是0
    for(int i = 0; i <= n; i ++ )   f[i][0] = 1;
    
    for(int i = 1; i <= n; i ++ )
    {
        for(int j = 1; j <= m; j ++ )
        {
            // 不选
            f[i][j] = f[i - 1][j];
            // 选
            if(j - v[i] >= 0)    f[i][j] += f[i - 1][j - v[i]];
        }
    }
    
    cout << f[n][m] << endl;
    
    return 0;
}

优化版

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 110, M = 10010;

int n, m, v[N];
int f[M];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )   cin >> v[i];
    
    // 从前i个物品中选,体积正好为0的方案数是0
    f[0] = 1;
    
    for(int i = 1; i <= n; i ++ )
        for(int j = m; j >= v[i]; j -- )
            f[j] += f[j - v[i]];
    
    cout << f[m] << endl;
    
    return 0;
}

2. 完全背包求恰好装满方案数

HERE
f[i][j]: 从前i个物品中选,体积正好为j的方案数

朴素版

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int M = 1010, N = 4;

int m, n = N;
int f[1 + N][M];
int v[1 + N] = {0, 10, 20, 50, 100};

int main()
{
    cin >> m;
    // 体积为0时什么都不选也是一种方案
    for(int i = 0; i <= n; i ++ )   f[i][0] = 1;
    
    for(int i = 1; i <= n; i ++ )
    {
        for(int j = 1; j <= m; j ++ )
        {
            // 不选
            f[i][j] = f[i - 1][j];
            // 选,枚举选多少件
            for(int k = 1; j - k * v[i] >= 0; k ++ )
                f[i][j] += f[i - 1][j - k * v[i]];
        }
    }
    cout << f[n][m] << endl;
    return 0;
}

优化版

思路和求最大价值一样

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int M = 1010, N = 4;

int m, n = N;
int f[M];
int v[1 + N] = {0, 10, 20, 50, 100};

int main()
{
    cin >> m;
    // 体积为0时什么都不选也是一种方案
    f[0] = 1;
    
    for(int i = 1; i <= n; i ++ )
        for(int j = v[i]; j <= m; j ++ )
            f[j] += f[j - v[i]];
        
    cout << f[m] << endl;
    return 0;
}

3. 01 背包求最优选法方案数

HERE
f[i][j]:从前i个物品中选,体积为j的最大价值
cnt[i][j]:从前i个物品中选,体积为j时,取最大价值时的方案数

求恰好装满的方案数与最优选法时的方案数的做法大不相同。
求恰好装满的方案数是直接通过 \(f\) 数组,用 \(dp\) 来求,但求最优选法时的方案数是跟踪记录 \(dp\) 的过程来求的。
这一点能从 \(f\) 数组的含义清楚的看到,前者 \(f\) 数组就是方案数,后者 \(f\) 数组求的是最优方案
并且需要通过额外的 \(cnt\) 数组来跟踪记录

之所以有这样的不同是因为,在求装满时的方案数时,体积等于价值,由于任意转移过程中最大价值就等于此时的体积,因此我们无需专门的 f 数组来保存最大价值,所以我们可以省去一个数组。
而当物品体积不等于价值时,装满并不意味着就取得了最大价值,此时我们需要一个数组来专门记录这个最大价值,因为状态转移过程中需要用到。

另外,本题的一个难点还在于,如何初始化 \(cnt\) 数组,通常我们容易考虑到,当体积为 \(0\) 时,我们什么都没法选物品,是一种方案。
但我们通常忘记,物品个数为 \(0\) 时,我们没物品可选,这也是一种方案!

还有就是需要注意,在装满背包求方案数问题当中,只能初始化 f[0]=0,而不能初始化 f[1~m]=0,因为此时背包没有装满
在非装满背包问题中则可以初始化。

朴素做法

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010, mod = 1e9 + 7;

typedef long long LL;

int n, m;
int f[N][N];
int cnt[N][N];
int v[N], w[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )   cin >> v[i] >> w[i];
    
    // 体积为0时,什么都选不了是一种方案
    for(int i = 0; i <= n; i ++ )   cnt[i][0] = 1;
    // 注意下面的初始化容易忽略,当没有物品可以选时,也是一种方案
    for(int i = 0; i <= m; i ++ )   cnt[0][i] = 1;
    
    for(int i = 1; i <= n; i ++ )
    {
        for(int j = 1; j <= m; j ++ )
        {
            // 不选
            f[i][j] = f[i - 1][j];
            cnt[i][j] = cnt[i - 1][j];
            // 选
            if(j >= v[i])
            {
                int t = f[i - 1][j - v[i]] + w[i];
                if(f[i][j] == t)    
                    cnt[i][j] = ((LL)cnt[i][j] + cnt[i - 1][j - v[i]]) % mod;
                else if(f[i][j] < t)
                {
                    f[i][j] = t;
                    cnt[i][j] = cnt[i - 1][j - v[i]];
                }
            }
        }
    }
    cout << cnt[n][m] << endl;
    return 0;
}

优化做法

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 1010, mod = 1e9 + 7;

typedef long long LL;

int n, m;
int f[N];
int cnt[N];
int v[N], w[N];

int main()
{
    cin >> n >> m;
    for(int i = 1; i <= n; i ++ )   cin >> v[i] >> w[i];
    
    /* 关于初始化的一个坑: 错误的认为只需要初始化 cnt[0]=0;
        即,考虑到当体积为0时,没法选物品,是一种方案
        但通常忘了,当没有物品时,即没有物品可选时,也是一种方案
        并且由于我们省去了第一维(物品),这一点更不容易发现了
        cnt[0]=0;   // 体积为0,没法选
        cnt[1~m]=1; // 物品个数为0,没得选
    */
    for(int i = 0; i <= m; i ++ )   cnt[i] = 1;
    
    for(int i = 1; i <= n; i ++ )
    {
        for(int j = m; j >= v[i]; j -- )
        {
            int t = f[j - v[i]] + w[i];
            if(f[j] == t)    
                cnt[j] = ((LL)cnt[j] + cnt[j - v[i]]) % mod;
            else if(f[j] < t)
            {
                f[j] = t;
                cnt[j] = cnt[j - v[i]];
            }
        }
    }
    cout << cnt[m] << endl;
    return 0;
}

4. 背包问题求具体方案

HERE
字典序最小:只需要按顺序遍历物品,所得字典序就是最小的

优化版

#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 1010, M = 1010;

int n, m;
int v[N], w[N];
int f[N];
vector<int> path[M];

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 -- )
        {
            int t = f[j - v[i]] + w[i];
            if(f[j] == t)
            {
                auto p = path[j - v[i]];
                // f相同,取字典序最小的,因为要与最终的结果进行比较,如果是由path[j-v[i]]经过i更新的话,需要push_back(i)
                // 如果不经由path[j-v[i]]更新的话,由于f不变,path[j]也无需更新
                p.push_back(i);
                if(p < path[j]) path[j] = p;
            }
            else if(f[j] < t)
            {
                f[j] = t;
                path[j] = path[j - v[i]];
                // f[j]由f[j-v[i]]通过i更新,需要push(i)
                path[j].push_back(i);
            }
        }
    }
    for(auto &x : path[m])  cout << x << ' ';
    cout << endl;
    return 0;
}
posted @ 2024-03-04 14:26  光風霽月  阅读(31)  评论(0编辑  收藏  举报