【AcWing算法提高课】1.3.4背包模型(四)

一、货币系统I

1021.货币系统 题目链接

简单的完全背包求方案数问题。

f [ i , j ] = f [ i − 1 , j ] + f [ i , j − v ] f[i,j]=f[i-1,j]+f[i,j-v] f[i,j]=f[i1,j]+f[i,jv]

代码实现:

#include <cstdio>
using namespace std;

const int N = 3010;
typedef long long ll;

int n, m;
ll f[N];

int main(){
    scanf("%d %d", &n, &m);
    f[0] = 1;
    for (int i = 1; i <= n; i ++){
        int v;
        scanf("%d", &v);
        for (int j = v; j <= m; j ++)
            f[j] += f[j - v];
    }
    printf("%lld", f[m]);
    return 0;
}

二、货币系统II

532.货币系统 题目链接

NOIP提高组原题,一般NOIP提高组的题目中都会隐藏一些性质,发现这些性质后题目便迎刃而解。

最优解 ( m , b ) (m,b) (m,b) 有这样三个性质:

  1. ( n , a ) (n,a) (n,a) 中所有货币的面额均可用 ( m , b ) (m,b) (m,b) 中货币的面额表示
  2. ( m , b ) (m,b) (m,b) 中任意一种货币的面额不能用 ( m , b ) (m,b) (m,b) 中其他货币的面额表示
    反证法,若能,则去掉这种货币, ( n , a ) (n,a) (n,a) ( m − 1 , b ) (m-1,b) (m1,b) 仍等价, m m m 一定不是最小值,矛盾。
  3. ( m , b ) (m,b) (m,b) 中任意一种货币的面额都在 ( n , a ) (n,a) (n,a) 中出现过
    反证法,若 b [ x ] b[x] b[x] 未在 ( n , a ) (n,a) (n,a) 中出现过,有以下两种情况:
    (1) b [ x ] b[x] b[x] 不能用 ( n , a ) (n,a) (n,a) 表示,从而 ( n , a ) (n,a) (n,a) ( m , b ) (m,b) (m,b) 不等价,矛盾;
    (2) b [ x ] b[x] b[x] 可以用 ( n , a ) (n,a) (n,a) 表示,由1, b [ x ] b[x] b[x] 可以用 ( m , b ) (m,b) (m,b) 中其他货币的面额表示,与2矛盾。

有了以上三个性质,题目即求出 ( n , a ) (n,a) (n,a) 中不能用其他货币的面额表示出的面额数。

( n , a ) (n,a) (n,a) 从小到大排序,对于每种面额 a [ i ] a[i] a[i],先判断它是否能由比它小的其他面额来表示,如果不能,再用类似完全背包的方法,更新所有 (不超过 ( n , a ) (n,a) (n,a) 种最大面额) 能用前 i i i 种面额表示出的面额。

代码实现:

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

const int N = 25010;

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

int main(){
    int T;
    scanf("%d", &T);
    while (T --){
        scanf("%d", &n);
        
        int maxa = 0;
        for (int i = 0; i < n; i ++)
            scanf("%d", &a[i]), maxa = max(maxa, a[i]);
        
        sort(a, a + n);
        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 <= maxa; j ++)  //类似完全背包,更新所有能用前i中面额表示出的且小于maxa的面额
                    f[j] |= f[j - a[i]];
            }
        
        printf("%d\n", res);
    }
    
    return 0;
}

三、混合背包问题

7.混合背包问题 题目链接

01背包: f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w ) f[i,j]=max(f[i-1,j],f[i-1,j-v]+w) f[i,j]=max(f[i1,j],f[i1,jv]+w)
完全背包: f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i , j − v ] + w ) f[i,j]=max(f[i-1,j],f[i,j-v]+w) f[i,j]=max(f[i1,j],f[i,jv]+w)
多重背包: f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w , f [ i − 1 , j − 2 v ] + 2 w , . . . , f [ i − 1 , j − s v ] + s w ) f[i,j]=max(f[i-1,j],f[i-1,j-v]+w,f[i-1,j-2v]+2w,...,f[i-1,j-sv]+sw) f[i,j]=max(f[i1,j],f[i1,jv]+w,f[i1,j2v]+2w,...,f[i1,jsv]+sw)

三种背包问题状态表示相同,仅在单个物品的状态计算时不同,且不同物品之间互不影响,所以每一个物品分开来做,只能选一个就按01背包,能选无限个就按完全背包,能选有限个就按多重背包。

代码实现:

#include <cstdio>
#include <algorithm>
using namespace std;

const int N = 1010;

int n, m;
int f[N];

int main(){
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i ++){
        int v, w, s;
        scanf("%d %d %d", &v, &w, &s);
        
        if (!s)  //完全背包
            for (int j = v; j <= m; j ++)
                f[j] = max(f[j], f[j - v] + w);
        else{  //多重背包二进制优化
            if (s < 0) s = 1;  //01背包(看成s=1的多重背包)
            for (int k = 1; k <= s; k <<= 1){
                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);
        }
    }
    printf("%d", f[m]);
    return 0;
}

四、有依赖的背包问题

10.有依赖的背包问题

有依赖的背包问题由金明的预算方案一题拓展而来。金明的预算方案一题中每个主件最多有 2 2 2 个附件,因此对于每个主件,最多有 2 2 2^2 22 种选择情况。然而,在更一般的有依赖的背包问题中,依赖关系组成一棵树 (即附件可能还有属于它的附件,且附件个数不限),这样我们就不能用分组背包思想来做,引入树形 DP:在 DFS 同时更新状态,状态转移时仅考虑上下两层 (父节点与子节点) 之间的状态关系。

闫氏 DP 分析法:

  1. 状态表示: f [ u , j ] f[u,j] f[u,j]
    (1) 集合:所有从以 u u u 为根的子树中选 ( u u u 一定要选),且总体积不超过 j j j 的选法的集合
    (2) 属性:Max
  2. 状态计算:金明的预算方案中,按照选择方案,“最后一步”至多有 4 4 4 种情况,但本题中,按照选择方案,“最后一步”情况太多,因此不能用最后一步的选择方案来划分集合。
    这里,我们先按照子节点 (子树的根) 划分集合,将每个子树看作一个物品组 (类似分组背包),物品组中按体积划分为 k k k 类物品,一个物品组中至多选一种物品:
    (1) 体积为 1 1 1 f [ u , j − 1 ] + f [ s o n , 1 ] f[u,j-1]+f[son,1] f[u,j1]+f[son,1]
    (2) 体积为 2 2 2 f [ u , j − 2 ] + f [ s o n , 2 ] f[u,j-2]+f[son,2] f[u,j2]+f[son,2]

    (k) 体积为 k k k f [ k , j − k ] + f [ s o n , k ] f[k,j-k]+f[son,k] f[k,jk]+f[son,k]
    f [ u , j ] f[u,j] f[u,j] 为所有子树中上述的最大值。

代码实现:

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

const int N = 110;

int n, m, root;
int f[N][N];
int v[N], w[N];
vector <int> e[N];

void dfs(int u){
    for (int i = 0; i < e[u].size(); i ++){  //循环物品组(子树)
        int son = e[u][i];
        dfs(son);  //树形DP:先递归求解子树
        
        //分组背包:
        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]);
    }       
    
    //将物品u加进去
    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(){
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i ++){
        int p;
        scanf("%d %d %d", &v[i], &w[i], &p);
        if (p < 0) root = i;
        else e[p].push_back(i);
    }
    
    dfs(root);
    
    printf("%d", f[root][m]);
    
    return 0;
}

五、背包问题求(最优解)方案数

以01背包求最优解方案数为例。

闫氏 DP 分析法:

  1. 状态表示: f [ i , j ] , g [ i , j ] f[i,j],g[i,j] f[i,j],g[i,j]
    (1) 集合:所有只从前 i i i 个物品中选,且总体积恰好 j j j 的选法的集合
    (2) 属性: f f f–Max, g g g–Count (最优解)
  2. 状态计算:
    (1) f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − v ] + w ) f[i,j]=max(f[i-1,j],f[i-1,j-v]+w) f[i,j]=max(f[i1,j],f[i1,jv]+w)
    (2) 上式中,如果取最大值的是前者, g [ i , j ] = g [ i − 1 , j ] g[i,j]=g[i-1,j] g[i,j]=g[i1,j];如果是后者, g [ i , j ] = g [ i − 1 , j − v ] g[i,j]=g[i-1,j-v] g[i,j]=g[i1,jv];如果两者相等, g [ i , j ] = g [ i − 1 , j ] + g [ i − 1 , j − v ] g[i,j]=g[i-1,j]+g[i-1,j-v] g[i,j]=g[i1,j]+g[i1,jv]

状态初始化: f [ 0 , 0 ] = 0 , f [ 0 , i ] = − ∞ ( i ≠ 0 ) , g [ 0 , 0 ] = 1 f[0,0]=0,f[0,i]=-\infty (i\ne 0),g[0,0]=1 f[0,0]=0,f[0,i]=(i=0),g[0,0]=1
最终答案:令 m a x { f [ n , i ] ∣ 0 ≤ i ≤ m } = r e s max\{f[n,i]|0\le i\le m\}=res max{f[n,i]∣0im}=res,最终答案为 c n t cnt cnt,则
c n t = ∑ 1 ≤ i ≤ m , f [ n , i ] = r e s g [ n , i ] cnt=\sum\limits_{1\le i\le m,f[n,i]=res}g[n,i] cnt=1im,f[n,i]=resg[n,i]

代码实现:

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

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

int n, m;
int f[N], g[N];

int main(){
    scanf("%d %d", &n, &m);
    
    memset(f, -0x3f, sizeof f);
    f[0] = 0, g[0] = 1;
    
    for (int i = 1; i <= n; i ++){
        int v, w;
        scanf("%d %d", &v, &w);
        for (int j = m; j >= v; j --){
            int t = max(f[j], f[j - v] + w), c = 0;
            if (t == f[j]) c = g[j];
            if (t == f[j - v] + w) c = (c + g[j - v]) % MOD;
            f[j] = t, g[j] = c;
        }
    }
    
    int res = 0, cnt = 0;
    for (int i = 0; i <= m; i ++) res = max(res, f[i]);
    
    for (int i = 0; i <= m; i ++)
        if (f[i] == res) cnt = (cnt + g[i]) % MOD;
    
    printf("%d", cnt);
    
    return 0;
}

当然,也可以不按照y总的“体积恰好是”状态表示,直接照搬01背包问题的状态表示,将 f [ i , j ] , g [ i , j ] f[i,j],g[i,j] f[i,j],g[i,j] 的集合定义为所有只从前 i i i 个物品中选,且总体积不超过 j j j 的选法的集合,这样的状态的递推式与上述完全相同,仅在状态初始化和最终答案上有少许不同:
状态初始化: g [ 0 , i ] = 1 g[0,i]=1 g[0,i]=1
最终答案: g [ n , m ] g[n,m] g[n,m]

代码实现:

#include <cstdio>
#include <algorithm>
using namespace std;

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

int n, m;
int f[N], g[N];

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

六、能量石

734.能量石 题目链接

贪心+01背包

为什么不能直接01背包求解?因为对于同一堆石头,若吃的顺序不同,则石头损失的能量不同,最后吃到的总能量也不同。相较于传统的01背包问题,本题需要考虑两个问题:按照什么样的顺序吃,吃哪些石头。

  1. 选择吃哪样的能量石:肯定不会吃能量已经降为 0 0 0 的能量石
  2. 按照什么样的顺序吃:
    对任意两个相邻的能量石 i , i + 1 i,i+1 i,i+1,先假定吃这两块能量石时它们的能量未降为 0 0 0,那么先吃 i i i 再吃 i + 1 i+1 i+1 所获得能量为 E i , + E i + 1 , − S i ∗ L i + 1 E_i^,+E_{i+1}^,-S_i*L_{i+1} Ei,+Ei+1,SiLi+1,先吃 i + 1 i+1 i+1 再吃 i i i 所获得能量为 E i + 1 , + E i , − S i + 1 ∗ L i E_{i+1}^,+E_i^,-S_{i+1}*L_i Ei+1,+Ei,Si+1Li ( E i , , E i + 1 , E_i^,,E_{i+1}^, Ei,,Ei+1, 代表刚开始吃这两块能量石时各自剩下的能量)。因此,先吃 i i i 再吃 i + 1 i+1 i+1 所获得能量更多当且仅当 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 + 1 L i + 1   ( L ≠ 0 ) \frac{S_i}{L_i}<\frac{S_{i+1}}{L_{i+1}}\ (L\ne 0) LiSi<Li+1Si+1 (L=0)

综合1、2可得,存在一组最优解,选择了一部分石头,按照 S i L i \frac{S_i}{L_i} LiSi 从小到大的顺序吃,且选择的所有石头能量均不会降为 0 0 0。接下来要求的就是最优解选择了哪些石头,用01背包问题求解。

  1. 状态表示: f [ i , j ] f[i,j] f[i,j]
    (1) 集合:所有只从前 i i i 块石头中选,且总体积 (时间) 恰好是 j j j 的选法的集合
    (2) 属性:Max
  2. 状态计算: f [ i , j ] = m a x ( f [ i − 1 , j ] , f [ i − 1 , j − S ] + E − ( j − S ) ∗ L ) f[i,j]=max(f[i-1,j],f[i-1,j-S]+E-(j-S)*L) f[i,j]=max(f[i1,j],f[i1,jS]+E(jS)L)
  3. 状态初始化: f [ 0 , 0 ] = 0 , f [ 0 , i ] = − ∞   ( i ≠ 0 ) f[0,0]=0,f[0,i]=-\infty\ (i\ne 0) f[0,0]=0,f[0,i]= (i=0)

代码实现:

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

const int N = 110, M = N * N;

int n;
int f[M];

struct Stone{
    int s, e, l;
    
    bool operator < (const Stone &W) const{  //结构体内部重载小于号
        return s * W.l < W.s * l;
    }
}stone[N];
/*
bool cmp(Stone a, Stone b){
	return a.s * b.l < a.l * b.s;  //自定义比较函数,返回值是1等价于a排在b前面
}
*/
int main(){
    int T;
    scanf("%d", &T);
    for (int C = 1; C <= T; C ++){
        int m = 0;
        scanf("%d", &n);
        for (int i = 0; i < n; i ++){
            int s, e, l;
            scanf("%d %d %d", &s, &e, &l);
            stone[i] = {s, e, l};
            m += s;
        }
        
        sort(stone, stone + n);
        //若是定义比较函数应为 sort(stone, stone + n, cmp);
        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);
    }
    
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值