目录
1、闫式DP分析法
闫式DP分析法是从集合的角度分析DP问题。DP问题实际上就是求有限集中的最值、数量、是否存在等问题,而有限集中的元素通常是非常多的,此时就可以使用DP来进行优化。
DP问题的分析分为两个阶段。
状态表示
第一个阶段是状态表示,这是一个化零为整的过程,每次处理问题时,不是一个元素一个元素处理,而是每次都枚举一个子集(一类东西)。就是把一些有相似点的元素,化称一个子集,然后用某一状态表示。
状态表示要从两个角度分析:集合和属性
首先,要搞清楚状态表示的f[i]表示的集合是什么。对于集合的描述通常是所有满足...条件/方案的集合。正因为f[i]表示的是集合,是一类东西,不是一个东西,所以可以优化时间复杂度。其次,f[i]表示的是一个集合,但f[i]是一个数。存的这个数与集合的关系称为属性。属性一般是最大值/最小值/数量。
状态计算
第二阶段是状态计算,这是一个化整为零的过程。如何求出f[i]?需要将f[i]表示的集合划分成若干子集,每个子集单独去求。划分子集时每个子集要求做到不重、不漏。当然,不漏是一定要有的,但是不重就不一定,如求数量时要不重,而求最值时可以重复。求f[i]时,就分别求每一个子集的f[i],最后综合每一个子集的f[i],就是结果。划分集合的依据时:寻找最后一个不同点。
2、背包问题
2.1 01背包问题
01背包问题就是每件物品只能选0次或1次
朴素版本
01背包问题是一个有限集中求最值的问题,因为物品数量、背包容量都是有限的,这也就限制了方案数是有限的,方案数是2^N个。问题就是从2^N个方案中选择一个符合题意的解法。若是暴力解法,就是枚举所有方案,时间复杂度是O(2^N)。而使用DP就可以优化时间复杂度
状态表示
背包问题本质是一个选择问题,选择问题的状态表示都是很类似的。第一维是只考虑前i个物品,后几维一般是题目的限制。在这道题中状态表示可以使用f(i, j)。f(i, j)表示的集合是所有只考虑前i个物品,且总体积不超过j的选法的集合。f(i, j)的属性是集合中所有方案的价值的最大值。当f(i, j)全算完后,答案就是f(N, V)
状态计算
状态计算时我们需要划分f(i, j)表示的集合。划分的依据是找最后一个不同点。在这道题中,选最后一个物品的方法,有选和不选两种,所以将集合划分为所有不选第i个物品的方案和所有选择第i个物品的方案这两个子集。每个子集要么选了第i个物品,要么没选。所以是不重不漏的。f(i, j)的取值就是取左边和右边值较大的哪一个。
现在来看如何求出左右两个子集的最大值。左边的集合表示的是所有不选第i个物品的方案的集合,很明显就是f(i - 1, j)。右边的集合表示的是所有选择第i个物品的方案的集合,若我们固定选择第i个物品,我们可以将选法分成变与不变两个集合,不变的是第i个物品组成的集合,因为每次都会选择,变的是前i - 1个物品,且总体积不超过j - vi的选法组成的集合,因为这i - 1个物品可以自由选择,即f(i - 1, j - vi)。所以,右边是f(i - 1, j - vi) + wi
f(i, j) = max(f(i - 1, j), f(i - 1, j - vi) + wi)
注意:当j < vi时,右边集合是不存在的
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int f[N][N], v[N], w[N], n, m;
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1;i <= n;i ++) cin >> v[i] >> w[i];
for(int i = 1;i <= n;i ++)
for(int j = 0;j <= m;j ++)
{
f[i][j] = f[i - 1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
代码中i、j的起始和结束看放在集合中是否有意义
优化版本
DP的优化和分析通常是分开的。DP的所有优化,都是对代码做的等价变形(与问题无关,只是对代码的实现进行了变形),只需保证优化完与原先是等价的即可。
这一题我们没办法对时间进行优化,只能对空间进行优化。
我们通过观察上面代码的状态转移方程回发现,我们在计算f[i][j]时,永远只会用到二维数组的i - 1层,所以我们没必要使用一个二维数组来存储,只需使用一个一维数组即可。倘若我们将上面代码的第一维删掉,我们可以得到:
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int f[N], v[N], w[N], n, m;
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1;i <= n;i ++) cin >> v[i] >> w[i];
for(int i = 1;i <= n;i ++)
for(int j = 0;j <= m;j ++)
{
f[j] = f[j]; // f[i][j] = f[i - 1][j];
if(j >= v[i])
f[j] = max(f[j], f[j - v[i]] + w[i]);
// f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
我们现在需要看一下,两断代码是否与原先是等价的。
(1)f[j] = f[j]与f[i][j] = f[i - 1][j]是否等价?
是等价的,在f[j] = f[j]中,我们算f[j]时,右边的f[j]是没有更新过的,也就是左边是第i层,右边是第i - 1层的,所以两者是等价的。并且因为f[j] = f[j]是恒成立的,所以这一步可以直接不要。
(2)下面两者是否等价?
对于max中的第一个参数并不需要管,因为只是与第二个参数作比较,看第二个参数即可。若j是从0到m递增遍历的,而j - v[i]一定小于j,此时的f[j - v[i]]是第i层的,与原先的代码不符合,为了让两者等价,需要让j从m到0递减遍历。这样才能保证f[j]是第i层时,f[j - v[i]]是i - 1层的。
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int f[N], v[N], w[N], n, m;
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1;i <= n;i ++) cin >> v[i] >> w[i];
for(int i = 1;i <= n;i ++)
for(int j = m;j >= 0;j --)
if(j >= v[i]) f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
此时可将判断条件放到for中
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int f[N], v[N], w[N], n, m;
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1;i <= n;i ++) cin >> v[i] >> w[i];
for(int i = 1;i <= n;i ++)
for(int j = m;j >= v[i];j --)
f[j] = max(f[j], f[j - v[i]] + w[i]);
cout << f[m] << endl;
return 0;
}
还可以将v和w这两个数组优化掉,因为每次只会用到v[i]和w[i]
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int f[N], v, w, n, m;
int main()
{
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1;i <= n;i ++)
{
cin >> v >> w;
for(int j = m;j >= v;j --)
f[j] = max(f[j], f[j - v] + w);
}
cout << f[m] << endl;
return 0;
}
注意:只有优化时才需要考虑j从前往后遍历还是从后往前遍历,二维数组时是不需要考虑这个的
2.2 完全背包问题
朴素版本
完全背包问题与01背包问题是类似的,只是01背包问题对于每一个物品只能选0次或1次,而完全背包问题,每一件物品都可以选无限次。
这里的状态表示与01背包问题是完全一样的,就不过多赘述了。不同点在于状态计算。在完全背包问题中,因为每个物品是可以选无限次的,所以不能以第i件物品选或不选来对集合进行划分,而是应该按第i件物品选几次来对区间进行划分。
(1)当第i件物品不选时,此时就是在i - 1件物品中选,并让体积<=j,f(i, j) = f(i - 1, j)
(2)当第i件物品选时,此时会分成第i个物品选几次,1,2,...,k,...,n
假设第i件最多选n次,为了不失一般性,我们看第i件物品选k次的情况,假设第i件物品的体积是v,价值是w。此时可以将选k次这个集合划分成两部分,前半部分是在前i - 1物品中选,并让体积
<=j- k*v,后半部分是k个i。所以f(i,j) = f(i - 1, j - k*v) + k*w
这里的k是从0到n的,所以
f(i,j) = max( f(i - 1, j), f(i - 1, j - v) + w, f(i - 1, j - 2*v) + 2*w),...,f(i - 1, j - k*v) + k*w),...
f(i - 1, j - n*v) + n*w) )
#include<iostream>
using namespace std;
const int N &