动态规划
动态规划问题的一般形式就是求最值。
求解动态规划的核心问题是穷举。
动态规划的穷举有点特别,因为这类问题存在「重叠子问题」,如果暴力穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
而且,动态规划问题一定会具备「最优子结构」,才能通过子问题的最值得到原问题的最值。
关键就是状态转移方程,写出正确的状态转移方程,才能正确地枚举。
解决问题步骤:
明确「状态」 -> 定义 dp 数组/函数的含义 -> 明确「选择」(做出选择改变当前状态-> 明确 base case。
0/1背包问题
描述:
有N件物品和一个容量为V的背包。第i件物品的费用(即体积,下同)是w[i],价值是val[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
思路:
用动态规划的思路,阶段就是“物品的件数”,状态就是“背包剩下的容量”,那么很显然f [ i , v ] 就设为从前 i 件物品中选择放入容量为 v 的背包最大的价值。那么状态转移方程为:
f[i][v]=max{ f[i-1][v],f[i-1][v-w[i]]+val[i] }。
这个方程可以如下解释:只考虑子问题“将前 i 个物品放入容量为 v 的背包中的最大价值”那么考虑如果不放入 i ,最大价值就和 i 无关,就是 f[ i - 1 ][ v ] , 如果放入第 i 个物品,价值就是 f[ i - 1][ v - w[i] ] + val[ i ],我们只需取最大值即可。
优化:
上述状态表示,我们需要用二维数组,但事实上我们只需要一维的滚动数组就可以递推出最终答案。考虑到用f[ v ]来保存每层递归的值,由于我们求f[ i ][ v ] 的时候需要用到的是f[ i-1 ][ v] 和 f[ i-1 ][v - w[i] ] 于是可以知道,只要我们在求f[ v ]时不覆盖f[ v - w[i] ],那么就可以不断递推至所求答案。所以我们采取倒序循环,即v = m(m为背包总容积)伪代码如下:
for i = 1..N
for v = V..0
f[ v ] = max{ f[ v ],f[ v-w[i] ]+val[ i ] };
完全背包问题
描述:
有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是w[i],价值是val[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。
思路:
完全背包问题与0/1背包问题不同之处在于其每个物品是无限的,从每种物品的角度考虑,与它相关的策略就变成了取0件、1件、2件...。我们可以根据0/1背包的思路,对状态转移方程进行改进,令f[i][v]表示前 i 种物品恰放入一个容量为 v 的背包的最大权值。状态转移方程就变成了:
f[ i ][ v ] = max{ f[ i-1 ][ v-kw[i] ] + kval[ i ] | 0 <= k*w[i] <= v}。
我们通过对0/1背包的思路加以改进,就得到了完全背包的一种解法,这种解法时间复杂度为O(n3),空间复杂度为O(n2)。
时间优化:
根据上述f[ i ][ v ]的定义,其为前 i 种物品恰好放入容量为 v 的背包的最大权值。根据上述状态转移方程可知,我们假设的是子结果f[ i-1 ][ v-k*w[i] ]中并没有选入第 i 种物品,所以我们需要逆序遍历(像0/1背包一样)来确保该前提;但是我们现在考虑“加选一件第 i 种物品”这种策略时,正需要一个可能已经选入第 i 种物品的子结果f[ i ][ v-w[i] ],于是当我们顺序遍历时,就刚好达到该要求。这种做法,使我们省去了一层循环,即第 i 种物品放入的件数k,从而时间复杂度优化为O(n^2)。
空间优化:
正如0/1背包的空间优化,上述状态转移方程已经优化为:
f[i][v]=max{f[i-1][v],f[i][v-w[i]]+val[i]}
将这个方程用一维数组实现,便得到了如下伪代码:
for i = 1..N
for v = 0..V
f[v] = max{f[v],f[v-w[i]] + val[ i ] };
代码:
int coinchange(vector<int>& coins, int amount)
{
//所有值初始化为amount+1,因为最多amount枚coin
vector<int> dp(amount + 1, amount + 1);
for (int i = 1; i <=amount ; i++)
{
for (int j = 0; j < coins.size(); j++)
{
if (i - coins[j] >= 0)
dp[i] = min(dp[i], dp[i - coins[j]]); //dp[i]为当前最小值
}
}
return dp[amount] == amount + 1 ? -1 : dp[amount];
}
动态标准套路
第一步:明确 状态 和 选择
状态:背包的容量和可选择的物品
选择:装入背包和不装入背包
for 状态1 in状态1的所有取值:
for 状态2 in 状态2的所有取值:
dp[状态1][状态2]=择优(选择1,选择2)
第二步:明确DP数组的定义
dp数组表示状态。
dp[i][w]的定义:对于前i个物品,当前背包的容量为w,这种情况下可以装的最大价值为dp[i][w]。
这样做是为了便于状态转移。
我们想求的最终答案是dp[N][W]。base case就是dp[0][..]=dp[..][0]=0,因为没有物品或者没有背包空间时,能装的最大价值为0.
int dp[N+1][W+1]
dp[0][..]=dp[..][0]=0
for i in[1..N]:
for j in [1,w]:
dp[i][j]=max(i装入背包,不把i装入背包)
return dp[N][W]
第三步:根据选择。思考状态转移的逻辑。
要结合dp数组的定义和算法逻辑
如果没有把第i个物品装入,那么dp[i][w]=dp[i-1][w](不装,继承之前的结果)
如果把第i个物品装入,那么dp[i][w]=dp[i-1][w-wt[i-1]]+val[i-1];(i从1开始,val和wt取值为i-1.
dp[i-1][w-wt[i-1]]:在装入第i个物品的前提下,背包能装的最大价值是多少?
显然应该寻求重量为w-wt[i-1]限制下能装入的最大价值,再加上第i个个物品的价值val[i-1]。
for i in[1..N]:
for j in [1,w]:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-wt[i-1]+val[i-1])
return dp[N][W]
最后处理边界情况。(j-wt[i-1])可能小于0
代码:
int _01(int W, int N, vector<int> & wt, vector<int> & val)
{
vector<vector<int>> dp(N + 1, vector<int>(W + 1,0));
for (int i = 1; i <= N; i++)//i从1开始,第i个数据下标是i-1,wt和val数组的下标从0到N-1
{
for (int j = 1; j <= W; j++)
{
if (j - wt[i - 1] >= 0)
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - wt[i - 1]] + val[i - 1]);
else
dp[i][j] = dp[i - 1][j];
}
}
return dp[N][W];
}
简单背包问题
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int n, s;
cin >> n >> s;
int dp[101][1001];
vector<int> Weight(n, 0);
for (int i = 0; i < n; i++)
{
cin >> Weight[i];
}
dp[0][0] = 1;
dp[0][1] = 0;
dp[1][0] = 1;
for(int j=1;j <= s;j++)
{
dp[0][j]= 0;
}
for(int i = 1;i <= n;i++)
{
dp[i][0]= 1;
}
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <=s; j++)
{
if (j - Weight[i - 1] >= 0)
{
dp[i][j] = max(dp[i-1][j-Weight[i - 1]], dp[i-1][j]); //能装下,可以选择装,也可以不装
}
else
{
dp[i][j] = dp[i - 1][j]; //装不下,dp[i][j]的状态和dp[i-1][j]一样
}
}
}
if (dp[n][s] == 1)
{
cout << "YES";
}
else
{
cout << "NO";
}
return 0;
}