动态规划 背包问题

详细解读背包问题

0/1背包问题:

有N件物品和一个容量为V的背包,第i件物品消耗的容量为Ci,价值为Wi,求解放入哪些物品使背包中总价值最大。
仔细想想,这里每个物品只有一个,对于每个物品而言,只有两种选择,装入或不装入,装入记为1,不装入为0,我们不能将物品进行分割,比如只拿半个是不允许的。这就是这个问题被称为0/1背包问题的原因。
在选择的过程中,我们每做出一种选择,都会将一种情况分裂成两种,最终我们会得到一张决策图:
在这里插入图片描述

  • 定义函数KS(i,j)
    代表当前背包剩余容量为j时,前i个物品最佳组合所对应的价值

对于第i个物品,有两种可能:

  1. 背包剩余的容量不足以装下第i个物品,此时KS(i,j)等于KS(i-1,j)
  2. 背包剩余容量可以装下第i个物品,此时需要进行判断,因为装下该物品不一定会使最终结果是最优组合,此时我们在两中情况中选择最优情况如果不装该物品,则价值为KS(i-1,j),装下该物品的话,价值为KS(i-1,j-wight[i])+value[i]
  • 状态转移方程
    f[i][v]=max{ f[i-1][v],f[i-1][v-w[i]]+val[i] },考录问题“将前i个物品放入背包中的最大价值”,可能存在两种情况,即存在第i个物品和不存在第i个物品,我们只需在其中取最优解即可。

对于这个问题的子问题,这里有必要详细说明一下。原问题是,将n件物品放入容量为c的背包,子问题则是,将前i件物品放入容量为j的背包,所得到的最优价值为KS(i,j),如果只考虑第i件物品放还是不放,那么就可以转化为一个只涉及到前i-1个物品的问题。如果不放第i个物品,那么问题就转化为“前i-1件物品放入容量为j的背包中的最优价值组合”,对应的值为KS(i-1,j)。如果放第i个物品,那么问题就转化成了“前i-1件物品放入容量为j-wi的背包中的最优价值组合”,此时对应的值为KS(i-1,j-wi)+vi。
这里为了方便处理,将数组ws和vs都增加了一个补位数0,防止数组越界

int value[] = { 0,2,4,3,7 };
int wight[] = { 0,2,3,5,5 };
int result[5][11];
int f[100];
int ks(int i, int c)
{
 if (i == 0 || c == 0)
  return 0;
 else if (c < wight[i])
  return ks(i - 1, c); //第i个装不下
 else
 {
  int temp1 = ks(i - 1, c - wight[i]) + value[i]; //装下第i个
  int temp2 = ks(i - 1, c);//不装下第i个
  int result = temp1 > temp2 ? temp1 : temp2;
  return result;
 }
}

另一种非递归的代码:

自上而下填表法:
在这里插入图片描述

int ks(int i, int c)
{
 for(int k=1;k<=i;++k)
  for (int j = 1; j <= c; ++j)
  {
   if (wight[k] > j)//容量不够
    result[k][j] = result[k - 1][j];
   else//容量足够
   {
    if (result[k-1][j] > result[k - 1][j - wight[k]] + value[i])
     result[k][j] = result[k - 1][j];//不装该珠宝,最优价值最大
    else
     result[k][j] = result[k - 1][j - wight[k]] + value[k];
   }
  }
}

空间优化:

int ks(int m, int n)
{
 for (int i = 1; i <= m; ++i)
  for (int j = n; j >=wight[i]; --j)
    f[j] = max(f[j], f[j - wight[i]] + value[i]);
 return f[n];
}

完全背包问题

有N种物品和一个容量为T的背包,每种物品都就可以选择任意多个,第i种物品的价值为P[i],体积为V[i],求解:选哪些物品放入背包,可卡因使得这些物品的价值最大,并且体积总和不超过背包容量。
跟01背包一样,完全背包也是一个很经典的动态规划问题,不同的地方在于01背包问题中,每件物品最多选择一件,而在完全背包问题中,只要背包装得下,每件物品可以选择任意多件。从每件物品的角度来说,与之相关的策略已经不再是选或者不选了,而是有取0件、取1件、取2件…直到取T/Vi(向下取整)件。

  • 状态转移方程
    ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k}; (0 <= k * V[i] <= t)

同时,ks(0,t)=0;ks(i,0)=0;

其实只是在原来0/1背包问题的基础上加了一个循环

#include<iostream>
using namespace std;
int value[] = { 0,2,4,3,7 };
int wight[] = { 0,2,3,5,5 };
int nums[] = { 0,1,1,1,1 };
int ks(int m, int n)
{
 int result = 0;
 if (m == 0 || n == 0)
  return 0;
 if (wight[m] > n)
  result=ks(m - 1, n);
 else
 {
  for (int i = 0; i <= nums[m]; ++i)
  {
   if (ks(m - 1, n - i * wight[m]) + value[m] * i > result)
    result = ks(m - 1, n - i * wight[m]) + value[m] * i;
  }
 }
 return result;
}

填表法:

#include<iostream>
using namespace std;
int value[] = { 0,2,4,3,7 };
int wight[] = { 0,2,3,5,5 };
int nums[] = { 0,1,1,1,1 };
int result[5][11];
int ks(int m, int n)
{
 for (int i = 1; i <= m; ++i)
 {
  for (int j = 1; j <= n; ++j)
  {
   if (j < wight[i])
    result[i][j] = result[i - 1][j];
   else {
    /*if (result[i - 1][j] > result[i - 1][j - wight[i]] + value[i])
     result[i][j] = result[i - 1][j];
    else
     result[i][j] = result[i - 1][j - wight[i]] + value[i];*/
    for (int k = 0; k <= nums[i]; ++k)
     if (result[i][j] < result[i - 1][j - k * wight[i]] + k * value[i])
      result[i][j] = result[i - 1][j - k * wight[i]] + k * value[i];
     else
      result[i][j] = result[i - 1][j];
   }
  }
 }
 return result[m][n];
}
int main()
{
 cout << ks(4, 10);
 while (1);
}

优化空间:

#include<iostream>
using namespace std;
int value[] = { 0,2,4,3,7 };
int wight[] = { 0,2,3,5,5 };
int nums[] = { 0,1,1,1,1 };
int result[5][11];
int f[100];
int max(int a, int b)
{
 return a > b ? a : b;
}
int ks(int m, int n)
{
 for (int i = 1; i <= m; ++i)
 {
  for (int j = n; j >= wight[i]; --j)
   for (int k = 0; k <= nums[i]&&j>=k*wight[i]; ++k)
    f[j] = max(f[j], f[j - k * wight[i]] + k * value[i]);
 }
 return f[n];
}
int main()
{
 cout << ks(4, 10);
 while (1);
}

完全背包问题可以自底向上递推:

#include<iostream>
using namespace std;
int dp[100010];
int value[10010];
int time[10010];
int t, n;
int max(int a, int b)
{
 return a > b ? a : b;
}
int ks(int t)
{
 for (int i = 1; i <= t; ++i)
  for (int j = 0; j < n; ++j)
   if (i >= time[j])
    dp[i] = max(dp[i], dp[i - time[j]]+value[j]);
 return dp[t];
}
int main()
{
 cin >> t >> n;
 for (int i = 0; i < n; ++i)
  cin >> time[i] >> value[i];
 //for (int i = 1; i <= t; ++i)
  //dp[i] = -1;
 /*for (int i = 0; i < n; ++i)
  for (int j = t; j >= time[i]; --j)
   for (int k = 0; k*time[i] <= j; ++k)
    dp[j] = max(dp[j], dp[j - k * time[i]] + k * value[i]);
 cout << dp[t];*/
 cout << ks(t);
 //while (1);
 return 0;
}

洛谷:疯狂的采药

例题

贪吃的大嘴

有一只特别贪吃的大嘴,她很喜欢吃一种小蛋糕,而每一个小蛋糕有一个美味度,而大嘴是很傲娇的,一定要吃美味度和刚好为m的小蛋糕,而且大嘴还特别懒,她希望通过吃数量最少的小蛋糕达到这个目的.所以她希望你能设计一个程序帮她决定要吃哪些小蛋糕.
数据规模和约定
m ≤ 20000,小蛋糕总数量≤50.
输入
先输入一行包含2个整数m、n,表示大嘴需要吃美味度和为m的小蛋糕,而小蛋糕一共有n种,下面输入n行,每行2个整数,第一个表示该种小蛋糕的美味度,第二个表示蛋糕店中该种小蛋糕的总数
输出
输出一行包含一个整数表示大嘴最少需要吃的小蛋糕数量,若大嘴无法通过吃小蛋糕达到m的美味度和,则输出" > < “.

在这里插入图片描述

#include<iostream>
using namespace std;
int wight[55];
int nums[55];
int dp[20055];
const int INF = 0x3fffff;
int min(int a, int b)
{
 return a < b ? a : b;
}
int main()
{
 int m, n;
 cin >> m >> n;
 for (int i = 1; i <= n; ++i)
  cin >> wight[i] >> nums[i];
 dp[0] = 0;
 for (int i = 1; i <= m; ++i)
  dp[i] = INF;
 for (int i = 1; i <= n; ++i)
 {
  for (int j = m;j>=wight[i]; --j)
  {
   for (int k = 0; k <= nums[i]; ++k)
    if (dp[j - k * wight[i]] != INF)
     dp[j] = min(dp[j], dp[j - k * wight[i]] + k);
  }
 }
 if (dp[m]!=INF)
  cout << dp[m];
 else
  cout << "><";
 //while (1);
 return 0;
}

解题思路:
把这个问题看成是背包问题,美味度当做背包的容量,数量当做价值。
注意初始化数组,因为背包最后必须恰好装满,所以如果存在答案,最后的dp[m]必定能够分解成dp[0]+dp[m-j]+…,可以发现,每次求解 KS(i,j)只与KS(i-1,m) {m:1…j} 有关。也就是说,如果我们知道了K(i-1,1…j)就肯定能求出KS(i,j)

礼物盒

在这里插入图片描述
在这里插入图片描述
参考礼物盒数据

#include<iostream>
using namespace std;
int dp[101];
const int INF = -1;
int wight[36] = { 0,11,8,11,16,1,2,6,10,17,10,6,5,2,19,4,7,5,5,15,3,15,11,9,17,9,4,10,12,17,19,20,11,10,20,3 };
int max(int a, int b)
{
 return a > b ? a : b;
}
int main()
{
 dp[0] = 0;
 for (int i = 1; i <= 100; ++i)
  dp[i] = INF;
 for(int i=1;i<36;++i)
  for (int j = 100;j>=wight[i]; --j)
   if (dp[j - wight[i]] != INF) {
    dp[j] = max(dp[j], dp[j - wight[i]] + 1);
    printf("dp[%d]:%d\n", j, dp[j]);
   }
 cout << dp[100];
 //while (1);
 return 0;
}

小A点菜

求背包问题的方法数
在这里插入图片描述
定义f[i][j]为用前i道菜用光j元钱的办法总数,其状态转移方程如下:
(1)if(j==第i道菜的价格)f[i][j]=f[i-1][j]+1;
(2)if(j>第i道菜的价格) f[i][j]=f[i-1][j]+f[i-1][j-第i道菜的价格];
(3)if(j<第i道菜的价格) f[i][j]=f[i-1][j];

#include<iostream>
using namespace std;
int n, m;
int price[1010];
int f[110][10010];
int main()
{
 cin >> n >> m;
 for (int i = 1; i <= n; ++i)
  cin >> price[i];
 for(int i=1;i<=n;++i)
  for (int j = 1; j <= m; ++j)
  {
   if (j < price[i])
    f[i][j] = f[i - 1][j];
   else if (j == price[i])
    f[i][j] = f[i - 1][j] + 1;
   else
    f[i][j] = f[i - 1][j - price[i]] + f[i - 1][j];
  }
 cout << f[n][m];
 //while (1);
 return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值