闫姓DP分析法
优化一般就是优化状态转移方程
例题1:
01背包问题
二维
(1)状态 f[ i ][ j ] 定义:前 i 个物品,背包容量 j 下的最优解(最大价值):
当前的状态依赖于之前的状态,可以理解为从初始状态f[0][0] = 0开始决策,有 N 件物品,则需要 N 次决 策,每一次对第 i 件物品的决策,状态f[ i ][ j ]不断由之前的状态更新而来。
(2)当前背包容量不够(j < v[i]),没得选,因此前 i 个物品最优解即为前 i−1 个物品最优解:对应代码:f[ i ][ j ] = f[ i - 1 ][ j ]
(3)当前背包容量够,可以选,因此需要决策选与不选第 i 个物品:
选: f[ i ][ j ] = f[ i - 1 ][ j - v[ i ] ] + w[ i ]。
不选:f[ i ][ j ] = f[ i - 1 ][ j ] 。
我们的决策是如何取到最大价值,因此以上两种情况取 max() 。
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int v[MAXN]; // 体积
int w[MAXN]; // 价值
int f[MAXN][MAXN]; // f[i][j], j体积下前i个物品的最大价值
int main()
{
int n, m;
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 = 1; j <= m; j++)
{
// 当前背包容量装不进第i个物品,则价值等于前i-1个物品
if(j < v[i])
f[i][j] = f[i - 1][j];
// 能装,需进行决策是否选择第i个物品
else
f[i][j] = max(f[i - 1][j] , f[i - 1][j - v[i]] + w[i]);
}
cout << f[n][m] << endl;
return 0;
}
一维
将状态 f[ i ][ j ] 优化到一维 f[ j ] ,实际上只需要做一个等价变形。
为什么可以这样变形呢?我们定义的状态 f[ i ][ j ]可以求得任意合法的 i 与 j 最优解,但题目只需要求得最终状态f[ n ][ m ],因此我们只需要一维的空间来更新状态。
(1)状态f[ j ]定义:N 件物品,背包容量 j 下的最优解。
(2)注意枚举背包容量 j 必须从 m 开始 (逆序)。
(3)为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态f[ i ][ j ]是由上一轮 i - 1 的状态得来的,f[ i ][ j ]与f[ i - 1 ][ j ]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态。
(4)例如,一维状态第 i 轮对体积为 3 的物品进行决策,则 f[ 7 ]由 f[ 4 ]更新而来,这里的 f[ 4 ]正确应该是 f[ i - 1 ][ 4 ],但从小到大枚举 j 这里的 f[ 4 ]在第 i 轮计算却变成了 f[ i ][ 4 ]。当逆序枚举背包容量 j 时,我们求f[ 7 ]同样由f[ 4 ]更新,但由于是逆序,这里的f[ 4 ]还没有在第i轮计算,所以此时实际计算的f[4]仍然是f[i - 1][4]。
(5)简单来说,一维情况正序更新状态f[j]需要用到前面计算的状态已经被「污染」,逆序则不会有这样的问题。
状态转移方程为:f[ j ] = max( f[ j ], f[ j - v[ i ] ] + w[ i ] 。
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
int f[MAXN]; //
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i++) {
int v, w;
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;
}
例题2:
#include <iostream>
using namespace std;
const int N = 1010;
int r , c ,t; // r:行 c:列 t:数据组数
int f[N][N] , w[N][N]; // f[N][N] : 从起点出发,走到第i行第j列的所有方案 m[N][N]:该点的价值
int main()
{
cin >> t ;
//循环处理t组数据
for(int k=1;k<=t;k++)
{
cin >> r >> c ;
//读取每个点的价值
for(int i=1;i<=r;i++ )
{
for(int j=1;j<=c;j++)
{
cin >> w[i][j] ;
}
}
//状态转移
for(int i=1;i<=r;i++ )
{
for(int j=1;j<=c;j++)
{
f[i][j] = max(f[i-1][j] + w[i][j],f[i][j-1] + w[i][j]) ;
}
}
cout << f[r][c] << endl;
}
}
例题3
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int n ; //数组个数
int f[N]; //fi 表示从第一个数开始算,以第i个数结尾的最长上升子序列
int a[N]; //存储数组
int main()
{
//读取数组个数和数组
cin >> n;
for(int i=1;i<=n;i++)
{
cin >> a[i];
}
//状态转换
for(int i=1;i<=n;i++)
{
f[i] = 1 ; //初始化,当只有一个数时,序列长度为1
for(int k=1;k<=n;k++)
{
if(a[k]<a[i]) f[i] = max(f[i],f[k]+1);
}
}
//遍历寻找f[i]中最大值
int res = 0 ;
for(int i=1;i<=n;i++)
{
res = max(res,f[i]);
}
cout << res ;
}
习题1
在具体的状态转移中,如果不选,就直接从上一步完完整整地把状态 cnt 和 k 转移过来;如果选,那上一步就应该选了cnt−1个物品,而且上一步身上所有物品最大值只能是1……k−1,把这些状态的方案数累加,就可以成为选择当前物品的条件
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 55;
const int M = 15;
const int MOD = 1e9 + 7;
int n, m, c;
int a[N][N];
// f[i][j][cnt][k] 表示:在 (i, j) 这个点,拿了 cnt 个物品,这些物品中价值最大的是 k
int f[N][N][M][M];
int main() {
scanf("%d%d%d", &n, &m, &c);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
scanf("%d", &a[i][j]);
a[i][j]++;
}
}
// 两个边界初始化
// 在起点 (1, 1) 处
// 如果拿也只能拿 a[i][j] 这个物品,只有一种方案
// 如果不拿,那就是 0 个物品,也是一个方案数
// 由于物品价值已经增加了一个偏移量,现在价值的范围是 [1, 13]
// 所以价值为 0 并不代表物品的价值,而是一个边界点
f[1][1][0][0] = 1;
f[1][1][1][a[1][1]] = 1;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j <= m; j++)
{
for (int cnt = 0; cnt <= c; cnt++)
{
for (int k = 0; k < M; k++)
{
// 不拿物品
f[i][j][cnt][k] = (f[i][j][cnt][k] + f[i - 1][j][cnt][k]) % MOD;
f[i][j][cnt][k] = (f[i][j][cnt][k] + f[i][j - 1][cnt][k]) % MOD;
// 可以拿
if ( k == a[i][j]) //判断是否取
{
for (int s = 0; s < a[i][j]; s++) //循环f[i][j][cnt][k]前一步的价值
{
f[i][j][cnt][k] = (f[i][j][cnt][k] + f[i - 1][j][cnt - 1][s]) % MOD;
f[i][j][cnt][k] = (f[i][j][cnt][k] + f[i][j - 1][cnt - 1][s]) % MOD;
}
}
}
}
}
}
// 最后把在终点 (n, m) 处拿 c 个物品的方案数累加
int res = 0;
for (int i = 1; i < M; i++) {
res = (res + f[n][m][c][i]) % MOD;
}
printf("%d\n", res);
return 0;
}