简单的DP

闫姓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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值