【算法基础课】第四章《动态规划》背包系列模板总结

背包系列

  • 状态表示f(i,j)
    • 集合
      • 所有
      • 满足条件
    • 属性
      • 最大值
      • 最小值
      • 数量
  • 状态计算

一、01背包

特点: 每件物品最多只能用一次(也可以不用)

状态计算: 将集合划分为选i不选i

1.1 朴素写法(二维)

/**
* f[i][j]表示只看前i个物品,总体积是j的情况下,最大价值是多少。
* 需要返回的答案就是:max(f[n][0~v])
* f[i][j]递推:
* 	1. 不选第i个物品,f[i][j] = f[i-1][j]
* 	2. 选第i个物品,f[i][j] = f[i-1][j-v[i]] + w[i]; // 此处曲线救国
* 	f[0][0] = 0 //在一个物品也不选的情况下,最大价值为0
**/
#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 1000 + 10;

int f[N][N];
int w[N], v[N]; //默认初始化为0
int n, t;

int main() {
    scanf("%d%d", &n, &t);
    for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]);
    // 默认初始化为0,所以不需要从0开始
    for (int i = 1; i <= n; i ++) {
        for (int j = 0; j <= t; j ++) {
            f[i][j] = f[i - 1][j];
            // 若当前背包容量不够时,前i个物品的最大价值就是前i-1个物品的最大价值(即下边if中语句为false)
            // 反之,当前背包容量足够,则取选与不选第i个物品的最大价值(即if语句为true)
            if (j >= v[i]) f[i][j] = max(f[i][j], f[i-1][j-v[i]] + w[i]);
        }
    }
    
    printf("%d\n", f[n][t]);
    return 0;
}

1.2 优化后(一维 + 滚动数组)

/**
	我们定义的状态f[i][j]可以求得任意合法的i与j最优解,但题目只需要求得最终状态f[n][m],因此可以只用一维的空间来更新状态。就好比求斐波那契数列(f[n] = f[n-1] + f[n-2])的o(1)空间写法,用a、b、c三个变量来滚动计算。
	而在二维朴素写法中,可以发现求f[i][j]的时候只用到了f[i-1][j]、f[i-1][j-v[i]],利用滚动数组的原理,将二维空间将为一维空间。
	状态f[j]表示:在当前k件物品下,背包容量为j的最大价值。Ps:由于前边舍弃了二维数组的第一维度的信息(第i件物品的信息),所以循环结束后的状态f[j]数组相当于朴素写法中的f[n][j]。
	此处循环为什么要逆序?
	答:循环体中的f[j] = max(f[j], f[j-v[i]] + w[i]); 对应f[i][j] = max(f[i-1][j], f[i-1][j-v[i]] + w[i])语句。假设当前i = 2, j从1到5,则先求出i=2时的f[1]、然后利用i=2时的f[1]求出了f[2],但事实上我们求f[2]时需要用i=1时的f[1]。也就是说,正序更新f[j]用到的第i-1轮的状态被第i论污染了。
**/

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 1000 + 10;

int f[N], w[N], v[N]; //默认初始化为0
int n, t;

int main() {
    scanf("%d%d", &n, &t);
    for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]);

    for (int i = 1; i <= n; i ++) {
        for (int j = t; j >= v[i]; j --) {
            f[j] = max(f[j], f[j-v[i]] + w[i]);
        }
    }
    
    printf("%d\n", f[t]);
    return 0;
}

1.3 其他问题:

1.3.1 关于f[i][j]体积正好是j还是不超过j的问题

在这里插入图片描述

在这里插入图片描述

二、完全背包

特点: 每件物品可以用无限次(有无限个)

  • 状态表示f(i,j)

    • 集合: 所有只考虑前i个物品,且总体积不大于j的所有选法
    • 属性: 最大值
  • 状态计算

    • 集合划分: 按照第i个物品选0、1、2、…k个来划分。

在这里插入图片描述

  • f[i][j] = max(f[i - 1][j - k * v[i]] + w[i]*k)

2.1 朴素写法

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 1000 + 10;

int f[N][N];
int w[N], v[N];
int n,t;

int main() {
    scanf("%d%d", &n, &t);
    for (int i = 1; i <= n; i++) scanf("%d%d", &v[i], &w[i]);

    for (int i = 1; i <= n; i ++) {
        for (int j = 0; j <= t; j ++) {
            for (int k = 0; k * v[i] <= j; k ++) {
                f[i][j] = max(f[i][j], f[i-1][j - k*v[i]] + k*w[i]);
            }
        }
    }
    
    printf("%d\n", f[n][t]);
    return 0;
}

2.2 优化(二层循环)

由上边知f[i][j] = max(f[i - 1][j - k * v[i]] + w[i]*k)即f[i][j]为在体积小于等于j的情况下,选k个第i类商品所能达到的最大价值。

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 − 3 v ] + 3 w . . . f [ i − 1 ] [ j − k v ] + k 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-3v] + 3w ... f[i-1][j-kv] + kw) f[i][j]=Max(f[i1][j],f[i1][jv]+w,f[i1][j2v]+2w,f[i1][j3v]+3w...f[i1][jkv]+kw)

f [ i ] [ j − v ] = M a x ( f [ i − 1 ] [ j − v ] , f [ i − 1 ] [ j − 2 v ] + w , f [ i − 1 ] [ j − 3 v ] + 2 w , f [ i − 1 ] [ j − 4 v ] + 3 w ) . . . f [ i − 1 ] [ j − k v ] + ( k − 1 ) w ) f[i][j-v] = Max(f[i-1][j-v], f[i-1][j-2v] + w, f[i-1][j-3v] + 2w, f[i-1][j-4v] + 3w)...f[i-1][j-kv] + (k-1)w) f[i][jv]=Max(f[i1][jv],f[i1][j2v]+w,f[i1][j3v]+2w,f[i1][j4v]+3w)...f[i1][jkv]+(k1)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]的状态与k的取值无关,所以可以去掉k这一层for循环。

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 1000 + 5;

int f[N][N];
int v[N], w[N];
int n, t;

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

2.3 优化(二层循环 + 一维状态)

由2.3优化后程序只剩两层循环,而f[i][j]的值由f[i-1][…]推出,所以我们可以利用滚动数组的思想,遵循等价代换的原则将二维变成一维。

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 1000 + 10;

int f[N];
int v[N], w[N];
int n, t;

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

优化过后可发现01背包完全背包优化后的代码区别在于——第二层循环(遍历体积)j的次序不同

  • 01背包:第i层的状态需要由第i-1层求出,为了避免第i-1层数据被污染,j采取逆序
  • 完全背包:第i层的状态需要由第i层求出,所以j采取正序

三、多重背包

特点: 每个物品的个数有限制,既不是只有1件、也不是只有无穷件。

  • 状态表示f(i,j)

    • 集合: 所有只考虑前i个物品,且总体积不大于j的所有选法
    • 属性: 最大值
  • 状态计算

    • 集合划分: 按照第i个物品选0、1、2、…k个来划分。

在这里插入图片描述

  • f[i][j] = max(f[i - 1][j - k * v[i]] + w[i]*k)

3.1 朴素写法

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 100 + 10;

int f[N][N];
int v[N], w[N], s[N];

int n, t;

int main () {
   scanf("%d%d", &n, &t);
   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 = 0; j <= t; j++) {
           for (int k = 0; k * v[i] <= j && k <= s[i]; k++) {
               f[i][j] = max(f[i][j], f[i - 1][j - k*v[i]] + k * w[i]);
           }
       }
   }
   printf("%d\n", f[n][t]);
   return 0;
}

3.3 优化(一维状态)

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 100 + 10;

int f[N], w[N], v[N], s[N];
int n, t;

int main()
{
    scanf("%d%d", &n, &t);
    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 = t; j >= 0; j --)
            for (int k = 0; k <= s[i] && j >= k * v[i]; k ++)
                f[j] = max(f[j], f[j - k*v[i]] + k * w[i]);
    
    printf("%d\n", f[t]);
    
    return 0;
}

3.2 优化(二进制–>01背包)

任何一个正整数都可以用二进制来表示,即可以用 2 0 − 2 n 2^{0} - 2^{n} 202n其中一项或者多项的和来表示。

1111b //十进制为15 = 8(2^3) + 4(2^2) + 2(2^1) + 1(2^0)
xxxxb //x为0或1,任意的01组合构成的4位不全为0的二进制,该二进制得到的十进制一定小于等于15,且一定可以由{8、4、2、1}的任意元素相加而得。
    
// 这样的话,我们可以物品i按照二进制的形式分为多个物品。
// 例如物品i有15件,那么可以拆分为
// 物品a(等价于8件物品i)、物品b(等价于4件物品i)、物品c(等价于2件物品i)、物品d(等价于1件物品i)
// 这样的话,按照01背包的规则,每一件物品选或者不选,一定可以用物品a、物品b、物品c、物品d来表示0~15件物品i

// 那么我们用上边的方法对每一个商品都进行拆分,拆分后的几件商品的和等价于原商品i,那么就转化为了01背包问题。
// 实际上就是用空间换时间。
#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 11000 + 100;
int f[N], v[N], w[N];

int n, t;

int main () {
    scanf("%d%d", &n, &t);
    
    int cnt = 1;
    for (int i = 1; i <= n; i ++) {
        int a, b, s;
        scanf("%d%d%d", &a, &b, &s);
    	// 拆分商品    
        for (int j = 1; j <= s; j <<= 1) {
            v[cnt] = j * a;
            w[cnt] = j * b;
            cnt ++;
            s -= j;
        }
        if (s > 0) {
            v[cnt] = s * a;
            w[cnt] = s * b;
            cnt ++;
        }
    }
    // 01背包
    for (int i = 1; i < cnt; i ++)
        for (int j = t; j >= v[i]; j --)
            f[j] = max(f[j], f[j - v[i]] + w[i]);
    
    printf("%d\n", f[t]);
    
    
    return 0;
}

四、分组背包

特点: 每一组中只能选一个物品

  • 状态表示f(i,j)

    • 集合: 只从前i组物品中选,且总体积不大于j的所有选法
    • 属性: 价值最大值
  • 状态计算

    • 集合划分: 对于第i组物品,按照不选、选第1个、选第1个、…k个来划分。

在这里插入图片描述

  • f[i][j] = max(f[i - 1][j - k * v[i]] + w[i]*k)

4.1 朴素写法

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 100 + 10;

int f[N][N], v[N][N], w[N][N], s[N];
int n, t;

int main () {
    scanf("%d%d", &n, &t);
    for (int i = 1; i <= n; i ++) {
        scanf("%d", &s[i]);
        for (int j = 1; j <= s[i]; j ++) {
            scanf("%d%d", &v[i][j], &w[i][j]);
        }
    }
    
    for (int i = 1; i <= n; i ++) {
        for (int j = 0; j <= t; j ++) {
            f[i][j] = f[i - 1][j]; //注意他在第三层循环外,因为如果第三重循环有更新f[i][j]的话,会被这句话覆盖掉
            for (int k = 1; k <= s[i]; k ++) {
                if (j >= v[i][k]) f[i][j] = max(f[i][j], f[i - 1][j - v[i][k]] + w[i][k]);
            }
        }
    }
    
    printf("%d\n", f[n][t]);
    return 0;
}

4.2 优化(一维数组)

仿照01背包优化的例子

#include<cstdio>
#include<algorithm>

using namespace std;

const int N = 100 + 10;

int f[N], v[N][N], w[N][N], s[N];
int n, t;

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

经验

为什么DP中的下标一般都是从1开始?

如果转移方程中涉及到i-1,那么一般就让下标从1开始,这样可以避免越界问题

涉及两个字符串的动态规划问题

一般两个字符串的问题可以用i,j分别表示第一个字符串的前i个字母和第二个字符串的前j个字母。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Honyelchak

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值