01背包和完全背包--动态规划(1)
今天开始看动规。
从最基本的背包问题开始:首先是基本的01背包。
简单的来说就是一个问题 可以分解成相似的若干个子问题 然后通过记录子问题的解来一步一步推出最终的最优解。
这样做的好处就是:可以通过记录状态值 重复利用(记忆结果)来减少传统递归的次数 从而优化时间复杂度。
动规中 最重要的莫过于列出对应的 状态转移方程。
假设有n件物品,用v[n]储存它们的价值,w[n]储存他们的重量。要放进一个总容量为total的背包里,最大价值是多少?
用一个数组dp[n][total]来储存每一步的状态。dp[i][j]表示的意义是将i个物品放进容量为j的背包里所能达到的最大价值。
那么状态转移方程就是dp[i][j] = max{dp[i-1][j],dp[i-1][j-w[i]]+v[i]}
这个打表过程也很好理解了,i从1到n,j从0到total,两个循环打表 最后输出dp[n][w]就可以了
空间优化:其实仔细观察就可以发现并不需要这么多的数组。在推dp[i][]时只需要dp[i-1][]这一层而已。也就是实际只需要两层。
再进一步。计算dp[i][j]只需要dp[i-1][j]和dp[i-1][j-w[i]]两个值。假设我们从后往前打表的话,就可以利用上一组的数据了。
你可以理解成 从后往前打 就将整个数组分为两个部分:前面是没有打的 隶属于dp[i-1]这一层。后面是打过的 隶属于dp[i]这一层。后面的计算需要利用前面的变量 这样的话,只需要一个一维数组就解决掉这个问题了。
改良后的状态转移方程为dp[i] = {dp[i],dp[i-w[i]]+v[i]}; 理解一下
下面 进一步 来看完全背包问题。
完全背包比起01背包问题 唯一的不同之处就是:每种物品都可以有无限个。
这样子的话 传统的状态转移方程就变为:dp[i][j] = max{dp[i-1][j],d[i-1][j-k*w[i]]+k*v[i]};
当背包容量很大的时候会变得复杂
为了便于理解我们可以把这个转换成01背包。每个物品最多只能放total/w[i]个。因此内部添加一个循环即可。
但是时间复杂度并没有降低。这时候我们回到之前空间优化过的01背包转移方程。在那里为了保证转移过来的状态来自dp[i-1],我们是从后往前打表的
如果从前往后打表会发生什么?很简单 每个状态都会被重复计算,这正是完全背包的思路。
例题 HDU1248 巫妖王的工资
用优化过的dp方法 代码如下:
# include<iostream> # include<algorithm> using namespace std; int dp[10001] = {0}; int main() { int T, N; cin >> T; int val[3] = { 150, 200, 350 }, w[3] = { 150, 200, 350 }; for (int i = 0; i < T; i++) { cin >> N; for (int j = 0; j < 3; j++) { for (int k = 1; k <= N; k++) { if (k >= w[i]) { dp[k] = max(dp[k-1],dp[k-w[i]]+val[i]); } } } cout << N - dp[N] << endl; } return 0; }
细节部分一定要多想想 尽量搞明白。
这周继续动规。很多事情都来不及做了。之前浪费掉的时间无论如何也要补救回来