背包问题讲解

背包问题

一、01背包

1.题目

n n n 种物品要放到一个袋子里,袋子的总容量为 m m m,第 i i i 种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi 的收益,每种物品至多能用一次,问如何选择物品,使得在物品的总体积不超过 m m m 的情况下,获得最大的收益?请求出最大收益。

输入格式

第一行两个整数 n , m n,m n,m

接下来 n n n 行,每行两个整数 v i , w i v_i,w_i vi,wi

输出格式

一个整数,表示答案。

2.分析

f [ i ] [ j ] f[i][j] f[i][j] 表示前 i i i 个物品,总体积为 j j j 时的最大收益。

转移 f [ i ] [ j ] f[i][j] f[i][j] =max( f [ i − 1 ] [ j ] f[i-1][j] f[i1][j], f [ i − 1 ] [ j − v i ] f[i-1][j-v_i] f[i1][jvi]+ w i w_i wi)。

考虑前 i i i 个物品的状态只和前 i − 1 i-1 i1 个物品的状态有关,前面 i − 2 i-2 i2 行都不用考虑。

转移 f [ i ] f[i] f[i]=max( f [ i ] f[i] f[i], f [ i − v j f[i-v_j f[ivj+ w j w_j wj),但是要倒序循环。

3.代码

#include <iostream>
using namespace std;
int n, m;
int v[1005], w[1005];
int f[1005];
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--)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m];
}

二、完全背包

1.题目

n n n 种物品要放到一个袋子里,袋子的总容量为 m m m,第 i i i 种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi 的收益,每种物品能用无限多次,问如何选择物品,使得在物品的总体积不超过 m m m 的情况下,获得最大的收益?请求出最大收益。

输入格式

第一行两个整数 n , m n,m n,m

接下来 n n n 行,每行两个整数 v i , w i v_i,w_i vi,wi

输出格式

一个整数,表示答案。

2.分析

状态转移: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v i ] + w i f[i][j]=max(f[i-1][j],f[i][j-v_i]+w_i f[i][j]=max(f[i1][j],f[i][jvi]+wi

然后压缩成一维:转移 f [ i ] f[i] f[i]=max( f [ i ] f[i] f[i], f [ i − v j f[i-v_j f[ivj+ w j w_j wj),但是要正序循环。

3.代码

#include <iostream>
using namespace std;
int n, m;
int v[1005], w[1005];
int f[1005];
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 = v[i]; j <= m; j++)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    cout << f[m];
}

三、多重背包1

1.题目

n n n 种物品要放到一个袋子里,袋子的总容量为 m m m,第 i i i 种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi 的收益,一共有 l i l_i li 个。问如何选择物品,使得在物品的总体积不超过 m m m 的情况下,获得最大的收益?请求出最大收益。

输入格式

第一行两个整数 n , m n,m n,m

接下来 n n n 行,每行三个整数 v i , w i , l i v_i,w_i,l_i vi,wi,li

输出格式

一个整数,表示答案。

2.分析

最简单的方法就是拆分成 1 1 1 1 1 1 个的,然后用 01 01 01 背包。这个就不赘述了。

前置知识:二进制拆分。从 1 , 2 , 3 , . . . , 2 m 1,2,3,...,2^m 1,2,3,...,2m中选一些数字相加,可以得出任意[0,2^{m+1}]的值,每个数字只能用一次。可以用归纳法证明。对于在 [ 2 m + 1 , n ] [2^{m+1},n] [2m+1,n] 内的值,我们取 n − 2 m + 1 n-2^{m}+1 n2m+1 后,剩下的数字在 [2{m+1}-n-1,2{m+1}] 内,可以从 1 , 2 , . . . , 2 m 1,2,...,2^m 1,2,...,2m中选一些数字相加得到。

3.代码

#include <iostream>
using namespace std;
int n, m, f[2001], v[2001], w[2001], l[2001];

int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i] >> l[i];
    for (int i = 1; i <= n; i++)
    {
        int res = l[i];
        for (int k = 1; k <= res; res -= k, k *= 2)
            for (int j = m; j >= v[i] * k; j--)
                f[j] = max(f[j], f[j - v[i] * k] + w[i] * k);
        for (int j = m; j >= v[i] * res; j--)
            f[j] = max(f[j], f[j - v[i] * res] + w[i] * res);
    }
    cout << f[m];
}

四、分组背包

1.题目

n n n 种物品要放到一个袋子里,袋子的总容量为 m m m。第 i i i 个物品属于第 a i a_i ai 组,每组物品我们只能从中选择一个。第 i i i 种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi 的收益。问如何选择物品,使得在物品的总体积不超过 m m m 的情况下,获得最大的收益?请求出最大收益。

输入格式

第一行两个整数 n , m n,m n,m

接下来 n n n 行,每行三个整数 a i , v i , w i a_i,v_i,w_i ai,vi,wi

输出格式

一个整数,表示答案。

2.分析

把考虑前 i i i 组物品以后,总体积为 0 , 1 , . . . , m 0,1,...,m 0,1,...,m时的最大收益都记下来。

考虑前 i i i 组物品,总体积为 j j j 时分为两种情况:
1.第 i i i 组物品一个没取,问题变成了考虑前 i − 1 i-1 i1 组物品,总体积为 j j j 时的情况;
2.第 i i i 组物品取了,我们枚举了其中的物品 k k k,问题变成了考虑前 i − 1 i-1 i1 组物品,总体积为 j − v k j-v_k jvk 时的情况;

状态:用 f [ i ] [ j ] f[i][j] f[i][j] 表示考虑了前 i i i 组物品,总体积为 j j j 时的最大收益。

转移: f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , m a x k ∈ c i ( f [ i − 1 ] [ j − v i ] + w k ) ) f[i][j]=max(f[i-1][j],max_{k∈c_i}(f[i-1][j-v_i]+w_k)) f[i][j]=max(f[i1][j],maxkci(f[i1][jvi]+wk)),其中 c i c_i ci 表示第 i i i 组物品的编号集合。

3.代码

#include <bits/stdc++.h>
using namespace std;
int n, m, v[1001], w[1001], a[1001], f[1001][1001];
vector<int> c[1001];

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
        scanf("%d%d%d", &a[i], &v[i], &w[i]), c[a[i]].push_back(i);
    for (int i = 1; i <= 1000; i++)
        for (int j = 0; j <= m; j++)
        {
            f[i][j] = f[i - 1][j];
            for (auto k : c[i])
                if (v[k] <= j)
                    f[i][j] = max(f[i][j], f[i - 1][j - v[k]] + w[k]);
        }
    printf("%d\n", f[1000][m]);
}

五、二维背包

1.题目

n n n 种物品要放到一个袋子里,袋子的总容量为 m m m,我们一共有 k k k 点体力值。第 i i i 种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi 的收益,并且消耗我们 t i t_i ti 点体力值,每种物品只能取一次。问如何选择物品,使得在物品的总体积不超过 m m m 并且花费总体力不超过 k k k 的情况下,获得最大的收益?请求出最大收益。

输入格式

第一行三个整数 n , m , k n,m,k n,m,k

接下来 n n n 行,每行三个整数 v i , w i , t i v_i,w_i,t_i vi,wi,ti

输出格式

一个整数,表示答案。

2.分析

跟01背包一样,变成二维的就行了。

3.分析

#include<bits/stdc++.h>
using namespace std;
int n,m,k,v[1001],w[1001],t[1001],f[1001][1001];

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

六、单调队列(方便后面优化)

1.题目

n n n 个生物,第 i i i 个生物会在第i到第 a i a_i ai 天出现,它的攻击力为 b i b_i bi。其中对于所有 i i i,满足 a i ≤ a i + 1 a_i≤a_{i+1} aiai+1。请输出每天出现的生物的攻击力的最大值。

输入格式

第一行一个整数 n n n

接下来 n n n 行,每行两个整数 a i , b i a_i,b_i ai,bi

输出格式

一共 n n n 行,每行一个数表示答案。

i i i 个整数表示第 i i i 天出现的生物的攻击力的最大值。

2.分析

假设第 i i i 天,对于第 j j j 个生物,如果 b j < b i b_j<b_i bj<bi,那么之后第 j j j 个生物都不用考虑了。

我们用一个队列,按编号从小到大的顺序,存一下到目前为止,哪些生物是需要被考虑的。

队列中的生物的攻击力是单调递减的,攻击力最高的生物在队首。

加入一个生物,队列末尾比它攻击力低的生物都不用考虑了。

3.代码

#include <bits/stdc++.h>
using namespace std;
int n, a[100001], b[100001], c[100001][2];//c是队列
int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i] >> b[i];
    }
    int k = 0, l = 1;//l是头指针,k是尾指针
    for (int i = 1; i <= n; i++)
    {
        for (; k >= l && b[i] >= c[k][0]; --k)//如果当前生物攻击力比队尾的高的话,需要出队
            ;
        c[++k][0] = b[i];
        c[k][1] = a[i];//这里记录生物能出现到的时间
        printf("%d\n"), c[l][0];
        for (; k >= l && c[l][1] == i; ++l)//时间到了之后,生物就要离开,需要出队
            ;
    }
}

七、多重背包2

1.题目

n n n 种物品要放到一个袋子里,袋子的总容量为 m m m,第 i i i 种物品的体积为 v i v_i vi,把它放进袋子里会获得 w i w_i wi 的收益,一共有 l i l_i li 个。问如何选择物品,使得在物品的总体积不超过 m m m 的情况下,获得最大的收益?请求出最大收益。

输入格式

第一行两个整数 n , m n,m n,m

接下来 n n n 行,每行三个整数 v i , w i , l i v_i,w_i,l_i vi,wi,li

输出格式

一个整数,表示答案。

2.分析

j j j (总体积)按照 m o d   v i mod\ v_i mod vi(物品体积)分类,只有同一类的状态间可以进行转移。

v i = 4 v_i = 4 vi=4 时,其中一类为 1 , 5 , 9 , 13... 1,5,9,13... 1,5,9,13... ,因为中间都是差的物品体积的整数倍。

在某一类中下标为 k k k 的位置,可以转移到下标在 [ k + 1 , k + l i ] [k+1,k+l_i] [k+1,k+li]中的位置;

然后每个位置取最大值。

下标 k , x k,x k,x 之间转移的时候,求 m a x ( f [ k ] + ( x − k ) ∗ w i ) max(f[k]+(x-k)*w_i) max(f[k]+(xk)wi),因为 x ∗ w i x*w_i xwi是个定值,所以我们将 f [ k ] − k ∗ w i f[k]-k*w_i f[k]kwi 放进单调队列即可。

3.代码

#include <bits/stdc++.h>
using namespace std;
int n, m, f[100001], c[100001][2]; // f[i]表示体积不超过i时的最大价值
int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; i++)
    {
        int v, w, t;
        scanf("%d%d%d", &v, &w, &t);
        for (int j = 0; j < v; j++) // 循环的是余数
        {
            int k = 0, l = 1; // k是头指针,l是尾指针
            for (int p = j, x = 1; p <= m; p += v, ++x)
            {                                    // x是下标,p是值,用来转移的
                int e = f[p] - x * w, r = x + t; // 减去x*w是为了找潜在的最大值,相当于都到了同一起点,然后看哪个比较远,r是能转移到的
                for (; k >= l && c[k][0] <= e; --k)
                    ;
                c[++k][0] = e;
                c[k][1] = r;
                f[p] = c[l][0] + x * w;
                for (; k >= l && c[l][1] == x; ++l)
                    ;
            }
        }
    }
    printf("%d\n", f[m]);
}

八、数字组合

1.题目

给定 N N N 个正整数 A 1 , A 2 , … , A N A_1,A_2,…,A_N A1,A2,,AN,从中选出若干个数,使它们的和为 M M M,求有多少种选择方案。

输入格式

第一行包含两个整数 N N N M M M

第二行包含 N N N个整数,表示 A 1 , A 2 , … , A N A_1,A_2,…,A_N A1,A2,,AN

输出格式

包含一个整数,表示可选方案数。

2.分析

01背包的改编问题,把每个数看成一个物品, A i A_i Ai看成体积,求出总体积恰好是 M M M的方案数。

f [ i , j ] f[i,j] f[i,j] 表示所有只从前 i i i 个物品中选,且体积恰好是 j j j 的方案数。

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int n, m;
int f[N];
int main()
{
    cin >> n >> m;
    f[0] = 1; // 相当于f[0.0]=1,除此之外其他初值都为0
    for (int i = 0; i < n; i++)
    {
        int v;
        cin >> v;
        for (int j = m; j >= v; j--)
            f[j] += f[j - v];
    }
    cout << f[m] << endl;
}

九、买书

1.题目

小明手里有 n n n 元钱全部用来买书,书的价格为 10 10 10 元, 20 20 20 元, 50 50 50 元, 100 100 100 元。
问小明有多少种买书方案?(每种书可购买多本)

输入格式
一个整数 n n n,代表总共钱数。

输出格式
一个整数,代表选择方案种数。

2.分析

多重背包问题的改编,类似上一道题。

f [ i , j ] f[i,j] f[i,j] 表示所有只从前 i i i 个物品中选,且总体积恰好是 j j j 的方案数。

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int m;
int v[5] = {0, 10, 20, 50, 100};
int f[5][N];
int main()
{
    cin >> m;
    f[0][0] = 1;
    for (int i = 1; i <= 4;i++)
        for (int j = 0; j <= m;j++)
        {
            f[i][j] = f[i - 1][j];
            if(j>=v[i])
                f[i][j] += f[i][j - v[i]];
        }
    cout << f[4][m] << endl;
}

优化后

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int m;
int v[5] = {0, 10, 20, 50, 100};
int f[N];
int main()
{
    cin >> m;
    f[0] = 1;
    for (int i = 1; i <= 4;i++)
        for (int j = 0; j <= m;j++)
        {
            if(j>=v[i])
                f[j] += f[j - v[i]];
        }
    cout << f[m] << endl;
}

十、潜水员

1.题目

潜水员为了潜水要使用特殊的装备。他有一个带 2 2 2 种气体的气缸:一个为氧气,一个为氮气。让潜水员下潜的深度需要各种的数量的氧和氮。潜水员有一定数量的气缸。每个气缸都有重量和气体容量。潜水员为了完成他的工作需要特定数量的氧和氮。他完成工作所需气缸的总重的最低限度的是多少?

输入格式

第一行有 2 2 2 个整数 m , n m,n mn。它们表示氧,氮各自需要的量。

第二行为整数 k k k 表示气缸的个数。

此后的 k k k 行,每行包括 a i , b i , c i a_i,b_i,c_i aibici 3 3 3 个整数。这些各自是:第 i i i 个气缸里的氧和氮的容量及气缸重量。

输出格式

仅一行包含一个整数,为潜水员完成工作所需的气缸的重量总和的最低值。

2.分析

f [ i , j , k ] f[i,j,k] f[i,j,k] 表示所有从前 i i i 个物品中选,且氧气含量至少是 j j j,氮气含量至少是 k k k 的所有选法。

三种类型的区别

  • 体积最多是 j j j,全部初始化为 0 0 0,体积大于等于 0 0 0
  • 体积恰好是 j j j f [ 0 , 0 ] f[0,0] f[0,0] 是0,其他是极大值,体积大于等于 0 0 0
  • 体积至少是 j j j f [ 0 , 0 ] f[0,0] f[0,0] 是0,其他是极大值,体积没要求

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 22, M = 80;
int n, m, q;
int f[N][M];//f[i][j]表示体积分别至少是i和j
int main()
{
    cin >> n >> m >> q;
    memset(f, 0x3f, sizeof(f));
    f[0][0] = 0;
    while (q--)
    {
        int v1, v2, w;
        cin >> v1 >> v2 >> w;
        for (int j = n; j >= 0; j--)
            for (int k = m; k >= 0; k--)
                f[j][k] = min(f[j][k], f[max(0, j - v1)][max(0, k - v2)] + w);
    }
    cout << f[n][m];
}

十一、背包问题求具体方案

1.题目

01背包问题求方案,要求输出字典序最小的

输入格式

第一行两个整数, N , V N,V NV,用空格隔开,分别表示物品数量和背包容积。

接下来有 N N N 行,每行两个整数 v i , w i v_i,w_i vi,wi,用空格隔开,分别表示第 i i i件物品的体积和价值。

输出格式

输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。

物品编号范围是 1 … N 1…N 1N

2.分析

判断出每个物品是否被选。倒推一下,从第一个开始,必须选的一定要选,可选可不选的也要选,因为要满足字典序的要求。

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int f[N][N], v[N], w[N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
        cin >> v[i] >> w[i];
    for (int i = n; i >= 1; i--)
        for (int j = 0; j <= m; j++)
        {
            f[i][j] = f[i + 1][j];
            if (j >= v[i])
                f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);
        }
    //f[1][m]存放最大值。
    int j = m;
    for (int i = 1; i <= n;i++)
    if(j>=v[i]&&f[i][j]==f[i+1][j-v[i]]+w[i])
    {
            cout << i << " ";
            j -= v[i];
    }
    return 0;
}


十二、机器分配

1.题目

总公司拥有高效设备 M M M 台,准备分给下属的 N N N 个分公司。各分公司若获得这些设备,可以为国家提供一定的盈利。问:如何分配这 M M M 台设备才能使国家得到的盈利最大?求出最大盈利值。分配原则:每个公司有权获得任意数目的设备,但总台数不超过设备数 M M M.

输入格式

第一行有两个数,第一个数是分公司数 N N N,第二个数是设备台数 M M M

接下来是一个 N ∗ M N*M NM 的矩阵,矩阵中的第 i i i 行第 j j j 列的整数表示第 i i i 个公司分配 j j j 台机器时的盈利。

输出格式

第一行输出最大盈利值;

接下 N N N 行,每行有 2 2 2 个数,即分公司编号和该分公司获得设备台数。

2.分析

将每个公司当作一个物品组,按分组背包来做。然后再根据上一题记录一下路径。

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 11,M=16;
int n, m;
int f[N][M], w[N][M],way[N];
int main()
{
    cin >> n >> m;
    for (int i = 1; i <= n;i++)
        for (int j = 1; j <= m;j++)
            cin >> w[i][j];
    for (int i = 1; i <= n;i++)
        for (int j = 0; j <= m;j++)
            for (int k = 0; k <= j;k++)
                f[i][j] = max(f[i][j], f[i - 1][j - k] + w[i][k]);
    cout << f[n][m] << endl;
    int j = m;
    for (int i = n; i;i--)
        for (int k = 0; k <= j;k++)
            if(f[i][j]==f[i-1][j-k]+w[i][k])
            {
                way[i] = k;
                j -= k;
                break;
            }
    for (int i = 1; i <= n;i++)
        cout << i << ' ' << way[i] << endl;
    return 0;
}


十三、金明的预算方案

1.题目

金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过 N N N 元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:

主件   附件

电脑   打印机,扫描仪

书柜   图书

书桌   台灯,文具

工作椅  无

如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有 0 0 0 个、 1 1 1 个或 2 2 2 个附件。附件不再有从属于自己的附件。金明想买的东西很多,肯定会超过妈妈限定的 N N N 元。于是,他把每件物品规定了一个重要度,分为 5 5 5 等:用整数 1   5 1~5 1 5 表示,第 5 5 5 等最重要。他还从因特网上查到了每件物品的价格(都是 10 10 10 元的整数倍)。他希望在不超过 N N N 元(可以等于 N N N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。

输入格式

输入文件的第 1行,为两个正整数,用一个空格隔开:N m,其中N表示总钱数,m为希望购买物品的个数。

从第2行到第m+1行,第j行给出了编号为j-1的物品的基本数据,每行有3个非负整数v p q,其中v表示该物品的价格,p表示该物品的重要度(1~5),q表示该物品是主件还是附件。

如果q=0,表示该物品为主件,如果q>0,表示该物品为附件,q是所属主件的编号。
输出格式

输出文件只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)。

2.分析

本质还是分组背包的问题,每个主件和它的附件可分成一组。

3.代码

#include <bits/stdc++.h>
using namespace std;
#define v first
#define w second
typedef pair<int, int> PII;
const int N = 70, M = 32010;
int n, m;
int f[M];
PII master[N];
vector<PII> servent[N];
int main()
{
    cin >> m >> n;
    for (int i = 1; i <= n; i++)
    {
        int v, w, q;
        cin >> v >> w >> q;
        if (!q)
            master[i] = {v, v * w};
        else
            servent[q].push_back({v, v * w});
    }
    for (int i = 1; i <= n; i++)
        if (master[i].v)
        {
            for (int j = m; j >= 0; j--)
            {
                auto &sv = servent[i];
                for (int k = 0; k < 1 << sv.size(); k++)
                {
                    int v = master[i].v, w = master[i].w;
                    for (int u = 0; u < sv.size(); u++)
                        if (k >> u & 1)
                        {
                            v += sv[u].v;
                            w += sv[u].w;
                        }
                    if (j >= v)
                        f[j] = max(f[j], f[j - v] + w);
                }
            }
        }
    cout << f[m] << endl;
}


十四、货币系统

1.题目

在网友的国度中共有  n n n 种不同面额的货币,第  i i i 种货币的面额为  a [ i ] a[i] a[i],你可以假设每一种货币都有无穷多张。

为了方便,我们把货币种数为  n n n、面额数组为 a[1…n] 的货币系统记作 ( n , a n,a n,a)。

在一个完善的货币系统中,每一个非负整数的金额  x x x 都应该可以被表示出,即对每一个非负整数  x x x,都存在  n n n 个非负整数  t [ i ] t[i] t[i] 满足  a [ i ] × t [ i ] a[i] \times t[i] a[i]×t[i] 的和为  x x x

然而,在网友的国度中,货币系统可能是不完善的,即可能存在金额  x x x 不能被该货币系统表示出。

例如在货币系统  n = 3 n=3 n=3,  a = [ 2 , 5 , 9 ] a=[2,5,9] a=[2,5,9] 中,金额  1 , 3 1,3 1,3 就无法被表示出来。

两个货币系统  ( n , a ) (n,a) (n,a) 和  ( m , b ) (m,b) (m,b) 是等价的,当且仅当对于任意非负整数  x x x,它要么均可以被两个货币系统表出,要么不能被其中任何一个表出。

现在网友们打算简化一下货币系统。

他们希望找到一个货币系统  ( m , b ) (m,b) (m,b) ,满足  ( m , b ) (m,b) (m,b) 与原来的货币系统  ( n , a ) (n,a) (n,a) 等价,且  m m m 尽可能的小。

他们希望你来协助完成这个艰巨的任务:找到最小的  m m m

输入格式

输入文件的第一行包含一个整数  T T T,表示数据的组数。

接下来按照如下格式分别给出 T T T 组数据。

每组数据的第一行包含一个正整数  n n n

接下来一行包含  n n n 个由空格隔开的正整数  a [ i ] a[i] a[i]

输出格式

输出文件共有 T T T 行,对于每组数据,输出一行一个正整数,表示所有与 (n,a) 等价的货币系统 (m,b) 中,最小的 m。

2.分析

性质 1 1 1 a 1 , a 2 , … , a n a_1,a_2,\dots ,a_n a1,a2,,an 一定都可以被表示出来。

性质 2 2 2:在最优解中, b 1 , b 2 , … b n b_1,b_2,\dots b_n b1,b2,bn一定都是从 a 1 , a 2 , … a n a_1,a_2,\dots a_n a1,a2,an 中选择的。浅证一下,某个 b b b 不属于 a a a 序列,但是如果满足要求的话它是可以被 a a a 序列表示出来的,然后将 a a a 序列替换成 b b b 序列的数,所以这个就没必要了。

性质 3 3 3 b 1 , b 2 , … b n b_1,b_2,\dots b_n b1,b2,bn一定不能被其他的 b b b 表示出来。

先排序,然后从前往后判断,判断 a i a_i ai 能不能被前面的表示出来。可以转换成完全背包,判断是否恰好能组成 a i a_i ai

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 110, M = 25010;

int n;
int a[N];
int f[M];

int main()
{
    int T;
    cin >> T;
    while (T--)
    {
        cin >> n;
        for (int i = 0; i < n; i++)
            cin >> a[i];
        sort(a, a + n);
        int m = a[n - 1];
        memset(f, 0, sizeof f);
        f[0] = 1;
        int res = 0;
        for (int i = 0; i < n; i++)
        {
            if (!f[a[i]])
                res++;
            for (int j = a[i]; j <= m; j++)
                f[j] += f[j - a[i]];
        }
        cout << res << endl;
    }
    return 0;
}

十五、混合背包问题

1.题目

N N N 种物品和一个容量是 V V V的背包。

物品一共有三类:

  • 第一类物品只能用 1 1 1 次(01背包);
  • 第二类物品可以用无限次(完全背包);
  • 第三类物品最多只能用 s i s_i si 次(多重背包);

每种体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。

接下来有 N行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和数量。

  • s i = − 1 s_i=−1 si=1 表示第 i i i 种物品只能用 1 1 1 次;

  • s i = 0 s_i=0 si=0表示第 i i i种物品可以用无限次;

  • s i > 0 s_i>0 si>0 表示第 i i i 种物品可以使用 s i s_i si 次;

输出一个整数,表示最大价值

2.分析

看类型,该是哪种就用哪种。

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m;
int f[N];
int main()
{
    cin >> n >> m;
    for (int i = 0; i < n;i++)
    {
        int v, w, s;
        cin >> v >> w >> s;
        if(s==0)//完全背包
        {
            for (int j = v; j <= m;j++)
                f[j] = max(f[j], f[j - v] + w);
        }
        else{
            if(s==-1)
                s = 1;
            for (int k = 1; k <= s;k*=2)
            {
                for (int j = m; j >= k * v;j--)
                    f[j] = max(f[j], f[j - k * v] + k * w);
                s -= k;
            }
            if(s)
            {
                for (int j = m; j >= s * v;j--)
                    f[j] = max(f[j], f[j - s * v] + s * w);
            }
        }
    }
    cout << f[m] << endl;
}

十六、有依赖的背包问题

1.题目

N N N 个物品和一个容量是 V V V 的背包。

物品之间具有依赖关系,且依赖关系组成一棵树的形状。如果选择一个物品,则必须选择它的父节点。

如下图所示:
i]

如果选择物品 5 5 5,则必须选择物品 1 1 1 2 2 2。这是因为 2 2 2 5 5 5 的父节点, 1 1 1 2 2 2 的父节点。

每件物品的编号是 i i i,体积是 v i v_i vi,价值是 w i w_i wi,依赖的父节点编号是 p i p_i pi。物品的下标范围是 1 … N 1…N 1N

求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。

输入格式

第一行有两个整数 N,V,用空格隔开,分别表示物品个数和背包容量。

接下来有 N行数据,每行数据表示一个物品。

第 i 行有三个整数 vi,wi,pi,用空格隔开,分别表示物品的体积、价值和依赖的物品编号。
如果 pi=−1,表示根节点。 数据保证所有物品构成一棵树。

输出格式

输出一个整数,表示最大价值。

2.分析

用递归的思想, f [ u , j ] f[u,j] f[u,j] 表示从以 u u u 为根的子树,总体积不超过 j j j 的方案。也用到了树形dp。

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;
int v[N], w[N];
int h[N],e[N],ne[N],idx;
int f[N][N];
void add(int a,int b)
{
    e[idx] = b;
    ne[idx] = h[a];
    h[a] = idx++;
}
void dfs(int u)
{
    for (int i = h[u]; ~i;i=ne[i])
    {
        int son = e[i];
        dfs(e[i]);//递归遍历子节点

        //分组背包
        for (int j = m - v[u]; j >= 0;j--)
            for (int k = 0; k <= j;k++)
                f[u][j] = max(f[u][j], f[u][j - k] + f[son][k]);
    }
    //更新剩下的部分
    for (int i = m; i >= v[u];i--)
        f[u][i] = f[u][i - v[u]] + w[u];
    //不满足条件的
    for (int i = 0; i < v[u];i++)
        f[u][i] = 0;
}
int main()
{
    cin >> n >> m;
    memset(h, -1, sizeof h);
    int root;
    for (int i = 1; i <= n;i++)
    {
        int p;
        cin >> v[i] >> w[i] >> p;
        if(p==-1)
            root = i;
        else
            add(p, i);
    }
    dfs(root);
    cout << f[root][m] << endl;
    return 0;
}

十七、背包问题求方案数

1.题目

N N N 件物品和一个容量是 V V V的背包。每件物品只能使用一次。

i i i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 最优选法的方案数。注意答案可能很大,请输出答案模 109 + 7 109+7 109+7的结果。

输入格式

第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。

接下来有 N行,每行两个整数 vi,wi,用空格隔开,分别表示第 i件物品的体积和价值。

输出格式

输出一个整数,表示 方案数 模 109+7的结果。

2.分析

用一个 g ( i , j ) g(i,j) g(i,j) 来存一下 f ( i , j ) f(i,j) f(i,j)取到最值的方案数。这里都表示恰好。

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1010,mod=1e9+7;
int n, m;
int f[N], g[N];
int main()
{
    cin >> n >> m;
    memset(f, -0x3f, sizeof f);
    f[0] = 0;
    g[0] = 1;
    for (int i = 0; i < n;i++)
    {
        int v, w;
        cin >> v >> w;
        for (int j = m; j >= v;j--)
        {
            int maxv = max(f[j], f[j - v] + w);
            int cnt = 0;
            if(maxv==f[j])
                cnt += g[j];
            if(maxv==f[j-v]+w)
                cnt += g[j - v];
            g[j] = cnt % mod;
            f[j] = maxv;
        }
    }
    int res = 0;
    for (int i = 0; i <= m;i++)
        res = max(res, f[i]);
    int cnt = 0;
    for (int i = 0; i <= m;i++)
    if(res==f[i])
            cnt = (cnt + g[i]) % mod;
    cout << cnt << endl;
    return 0;
}

十八、能量石

1.题目

岩石怪物杜达生活在魔法森林中,他在午餐时收集了 N N N 块能量石准备开吃。

由于他的嘴很小,所以一次只能吃一块能量石。

能量石很硬,吃完需要花不少时间。

吃完第 i i i 块能量石需要花费的时间为 S i S_i Si秒。

杜达靠吃能量石来获取能量。

不同的能量石包含的能量可能不同。

此外,能量石会随着时间流逝逐渐失去能量。

i i i 块能量石最初包含 E i E_i Ei 单位的能量,并且每秒将失去 L i L_i Li 单位的能量。

当杜达开始吃一块能量石时,他就会立即获得该能量石所含的全部能量(无论实际吃完该石头需要多少时间)。

能量石中包含的能量最多降低至 0 0 0

请问杜达通过吃能量石可以获得的最大能量是多少?

输入格式

第一行包含整数 T,表示共有 T组测试数据。

每组数据第一行包含整数 N,表示能量石的数量。

接下来 N行,每行包含三个整数 Si,Ei,Li。

输出格式

每组数据输出一个结果,每个结果占一行。

结果表示为 Case #x: y,其中 x是组别编号(从 1 开始),y 是可以获得的最大能量值。

2.分析

贪心+DP。选择吃哪些,按什么顺序吃。

E i + E i + 1 − S i ∗ L i + 1 E_i+E_{i+1}-S_i*L_{i+1} Ei+Ei+1SiLi+1 E i + E i + 1 − S i + 1 ∗ L i E_i+E_{i+1}-S_{i+1}*L_i Ei+Ei+1Si+1Li比,假设后面大,可以得到 S i ∗ L i + 1 < S i + 1 ∗ L i S_i*L_{i+1}<S_{i+1}*L_i SiLi+1<Si+1Li。所以最优解一定按照 S i / L i S_i/L_i Si/Li的比值从小到大来的。

f [ i , j ] f[i,j] f[i,j] 所有只从前 i i i 快能量石选,且体积不超过 j j j 的方案。

3.代码

#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int n;
struct Stone{
    int s, e, l;
    bool operator< (const Stone &W) const
    {
        return s * W.l < l * W.s;
    }
} stone[N];
int f[N];
int main()
{
    int T;
    cin >> T;
    for (int C = 1; C <= T;C++)
    {
        int m = 0;
        cin >> n;
        for (int i = 0; i < n;i++)
        {
            int s, e, l;
            cin >> s >> e >> l;
            stone[i] = {s, e, l};
            m += s;
        }
        sort(stone, stone + n);
        memset(f, -0x3f, sizeof f);
        f[0] = 0;
        for (int i = 0; i < n;i++)
        {
            int s = stone[i].s, e = stone[i].e,l = stone[i].l;
            for (int j = m; j >= s;j--)
                f[j] = max(f[j], f[j - s] + e - (j - s) * l);
        }
        int res = 0;
        for (int i = 0; i <= m;i++)
            res = max(res, f[i]);
        printf("Case #%d: %d\n", C, res);
    }
}

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值