记忆化搜索与动态优化与背包问题

文章目录

    • 01背包问题
      • 使用递归求解
      • 改进递归方法
      • 推导递推公式使用dp
    • 其他推导方法
      • 正向推导
      • 状态转移法
    • 01背包问题2

01背包问题

动态规划(DP)—— 算法设计方法之一。

使用递归求解

问题:有几个重量和价值分别为Wi和Vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和最大的值。
限制条件
1<=n<=100
1<=Wi,Vi<=100
1<=W<=10000

输入样例:
n = 4
(w ,v) = { (2,3), (1,2),(3,4), (2,2) }
W=5;
输出样例:
7(选择0,1,3号)

这种问题就是背包问题。背包问题看起来非常复杂,需要测试很多种组合。首先我们对每个物品是否放入背包进行搜索试试看。
代码如下:

#include 

#define MAX_N 100

using namespace std;

int n, W; //n个 物品, 总重量不超过W

int w[MAX_N], v[MAX_N];

//从第i个物品开始挑选总重量小于j的部分
int rec(int i, int j) {
    int res;
    if (i == n) {
        //已经没有剩下的了
        return res = 0;
    } else if (j < w[i]) {
        res = rec(i + 1, j);//这个物品超重了,尝试下一个
    } else {
        res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]); //在这里进行分支前一个是不包含第i个,后一个是包含第i个。
    }
    return res;
}

void init() {
    cin >> n ;
    for (int i = 0; i < n; i++) {
        cin >> w[i];
        cin >> v[i];
    }
    cin>>W;
}

int main() {
    init();
    cout << rec(0, W) << endl;
    return 0;
}

改进递归方法

虽然上述方法可以求解,但是显然这种方法不是很好。它的搜索深度为n,最坏情况需要 O ( 2 n ) O(2^n) O(2n)时间复杂度。该递归调用方法使用了遍历二叉树搜索的原理。

记忆化搜索与动态优化与背包问题_第1张图片

其实这里是有改进的地方,观察二叉树会发现rec(3,2)执行了两次,但是如果我们在执行第一次的时候将rec(3,2)的值保存起来,那么下次执行时就可以直接调用结果了(这便是记忆化搜索)。
来试试新的方法:增加一个二维数组dp[][],将执行结果没一步保存在其中。

代码如下:

#include 

#define MAX_N 100

using namespace std;

int n, W; //n个 物品, 总重量不超过W

int w[MAX_N], v[MAX_N];
int dp[MAX_N][MAX_N];

//从第i个物品开始挑选总重量小于j的部分
int rec(int i, int j) {
    if (dp[i][j] != 0) {
        return dp[i][j]; //如果有记录则直接返回结果
    }
    int res;
    if (i == n) {
        //已经没有剩下的了
        return res = 0;
    } else if (j < w[i]) {
        res = rec(i + 1, j);//这个物品超重了,尝试下一个
    } else {
        res = max(rec(i + 1, j), rec(i + 1, j - w[i]) + v[i]); //在这里进行分支前一个是不包含第i个,后一个是包含第i个。
    }
    dp[i][j] = res;  //结果保存
    return res;
}

void init() {
    cin >> n;
    for (int i = 0; i < n; i++) {
        cin >> w[i];
        cin >> v[i];
    }
    cin >> W;
}

int main() {
    init();
    cout << rec(0, W) << endl;
    return 0;
}

推导递推公式使用dp

仔细研究前面的算法用到的这个记忆数组。记dp[i][j]为根据rec的定义,从第i个物品开始挑选总重量小于j时,总价值最大的值。于是我们有一下递推公式。

d p [ n ] [ j ] = 0 dp[n][j]=0 dp[n][j]=0
d p [ i ] [ j ] = { d p [ i + 1 ] [ j ] ( j < w [ i ] ) m a x ( d p [ i + 1 ] [ j ] , d p [ i + 1 ] [ j − w [ i ] ] + v [ i ] ) dp[i][j]=\left\{ \begin{aligned} & dp[i+1][j] (jdp[i][j]={dp[i+1][j](j<w[i])max(dp[i+1][j],dp[i+1][jw[i]]+v[i])
不用递归函数,直接使用地推公式将各项值计算出来,然后用二重信息即可解决该问题。

int dp[MAX_N+1][MAX_N+1] {}; //初始化为全0
void solve2(){
    for(int i=n-1;i>=0;i--){
        for(int j=0;j<=W;j++){
            if(j<w[i]){
                dp[i][j]=dp[i+1][j];
            }else{
                dp[i][j]=max(dp[i+1][j] , dp[i+1][j-w[i]] + v[i]);
            }
        }
    }
}

虽然这个函数的时间复杂度与前一个相同 O ( n × W ) O(n×W) O(n×W) ,但是简明了许多。
动态规划问题(dp)可以分析其递推公式。

注意:全局数组和静态数组会被初始化为0;局部数据需要手动初始化为0,例如:int a[4]={} ; 或 int a[4] {} ; 或 int a[4] {0} 。如果括号里写0或什么都不写将会把数组全部初始化为0,但是如果这样写:int a[4] {1}; ,将会被初始化为1 0 0 0.

其他推导方法

递推公式有多种推导方法,使用不同的递推公式我们可以得到多种算法。

正向推导

刚讲到DP中关于i的循环是逆向进行的。如下递推公式是正向进行的。
d p [ i + 1 ] [ j ] : = dp[i+1][j]:= dp[i+1][j]:=从前i个物品中挑选出总重量不超过j的物品时,总价值的最大值

d p [ 0 ] [ j ] = 0 dp[0][j]=0 dp[0][j]=0
d p [ i + 1 ] [ j ] = { d p [ i ] [ j ] , ( j < w [ i ] ) m a x ( d p [ i ] [ j ] , d p [ i ] [ j − w [ i ] ] + v [ i ] ) dp[i+1][j]=\left\{ \begin{aligned} dp[i][j] ,(jdp[i+1][j]={dp[i][j],(j<w[i])max(dp[i][j],dp[i][jw[i]]+v[i])

仔细观察公式会发现,dp中的i和w和v中的i不同,dp中的i表示前i个物品,而w和v中的i表示物品的编号,即编号是从0开始的。

void solve() {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j <= W; j++) {
            if (j < w[i]) {
                dp[i + 1][j] = dp[i][j];
            } else {
                dp[i + 1][j] = max(dp[i][j], dp[i][j - w[i]] + v[i]);
            }
        }
    }
    cout<<dp[n][W];
}

状态转移法

除了用递推方式逐项求解外,还可以把状态转换想象成从“前i个物品中挑选出总重量不超过j时的状态” 向“前i+1个物品中选取总重量不超过j“ 和 ”前i+1个物品中选取总重量不超过j+w[i] 时的状态“的转移,于是可以实现如下形式。

记忆化搜索与动态优化与背包问题_第2张图片

void solve2() {
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < W; j++) {
            dp[i + 1][j] = max(dp[i + 1][j], dp[i][j]);
            if (j + w[i] <= W) {
                dp[i + 1][j + w[i]] = max(dp[i + 1][j + w[i]], dp[i][j] + v[i]);//  dp[i+1][j+w[i]]表示前i+1个物品,重量不超过j+w[i]的价值,dp[i][j]+v[i],表前i个物品重量不超过j的价值,加上第i+1个物品的价值,也就是说,它和dp[i + 1][j + w[i]]相比默认选择了第i+1个物品。
            }
        }
    }
    cout << dp[n][W];


上述问题中,从当前状态转移到下一状态的形式,需要注意初项之外也需要初始化(在本问题中,因为价值的初始值为0,所以没有显示的初始化,在有些问题中初始值为无穷大等,需要显示的初始化。)
同一个问题可能有很多不同的解法:搜索记忆法、递推关系dp、状态转移dp等。根据具体的问题选择较好的方法。

01背包问题2

问题:有几个重量和价值分别为Wi和Vi的物品。从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和最大的值。

限制条件:

1 < = n < = 100 1 < = w i ⩽ 1 0 7 1 ⩽ v i ⩽ 100 1 < = W < = 1 0 9 1<=n<=100 \\ 1<=w_i \leqslant 10^7 \\ 1 \leqslant v_i \leqslant 100 \\ 1<=W<=10^9 \\ 1<=n<=1001<=wi1071vi1001<=W<=109

输入样例:
n = 4
(w ,v) = { (2,3), (1,2),(3,4), (2,2) }
W=5;

输出样例:
7(选择0,1,3号)

它与文章开头的01背包问题的区别仅仅是限制条件的不同。求解问题的复杂度是 O ( n W ) O(nW) O(nW),显然现在的问题中w的范围非常大,如果继续使用前面的方法,那么dp数组将会非常大。

在之前的方法中用dp表示一定的重量下的最大价值,现在我们用dp表示一定价值下的最小重量。

定义:

d p [ i + 1 ] [ j ] : = 前 i 个 物 品 中 挑 选 出 价 值 总 和 为 j 时 总 重 量 最 小 值 ( 不 存 在 时 就 是 I N F ) dp[i+1][j] := 前i个物品中挑选出价值总和为j时总重量最小值(不存在时就是INF) dp[i+1][j]:=ijINF

分析:

由于前0个物品没有重量,所以:
d p [ 0 ] [ j ] = I N F d p [ 0 ] [ 0 ] = 0 dp[0][j] = INF \\ dp[0][0] = 0 dp[0][j]=INFdp[0][0]=0

和前文同理可得递推关系式:

d p [ 0 ] [ 0 ] = 0 d p [ 0 ] [ j ] = I N F , j ! = 0 d p [ i + 1 ] [ j ] = { m i n ( d p [ i ] [ j ] , d p [ i + 1 ] [ j − v [ i ] ] + w [ i ] ) , j > = v [ i ] d p [ i ] [ j ] , j < v [ i ] \begin{aligned} &dp[0][0] = 0 \\ &dp[0][j] = INF , j != 0 \\ &dp[i+1][j] = \left \{ \begin{aligned} & min( dp[i][j], dp[i+1][j-v[i]] +w[i] ) , \qquad j>=v[i]\\ & dp[i][j] ,\qquad jdp[0][0]=0dp[0][j]=INF,j!=0dp[i+1][j]={min(dp[i][j],dp[i+1][jv[i]]+w[i]),j>=v[i]dp[i][j],j<v[i]

问题最终的解为:

d p [ n ] [ j ] ⩽ W dp[n][j] \leqslant W dp[n][j]W 的最大的 j j j

int dp[MAX_N+1][MAX_N * MAX_V+1]; //能够容纳的最大价值MAX_N * MAX_V
int solve(){
	fill(dp[0],dp[0]+MAX_N * MAX_V +1 , INF);
	dp[0][0] = 0;
	for(int i =0; i< n ; i++){
		for(int j =0; j<= MAX_N*MAX_V; j++){
			if(j < v[i]){
				dp[i+1][j] = dp[i][j];
			else{
				dp[i+1][j] = dp[i+1][j-v[i]] +w[i];
			}
		}
	}
	// 在最后一行找到dp[n][j]<=W 时的j
	res = 0;
	for(int i = 0; i<= MAX_N*MAX_V;i++) 
		if(dp[n][i] <= W){
			 res =i-1;
			 break;
		}
	return res;
}

你可能感兴趣的:(程序设计)