闫氏DP分析法 学习笔记

求解的过程

DP问题 == 有限集上的最值问题

DP的两个阶段:

  1. 化零为整,寻找共性——状态表示
    • 集合
      所谓 化零为整 是指我们对于零星的情况,不是一个一个去枚举,而是每次根据这些零星的情况的某些特性去枚举一类情况(即一个子集)

f(i)需要考虑的问题:

    • 属性
      表示的是一个怎样的集合?
      f(i)保存的值是什么意思(max/min/count/…)?与集合有什么关系?
  1. 化整为零,寻找不同——状态计算

如何去求f(i)呢?
我们要把它们划分成若干个不同的子集来求,每一个子集分别去求。需要满足的条件有:

      • 不重复(可以不满足,例如求min,max。但是求count的时候,就不能重复)
      • 不遗漏
        例如,我们假设f(i)表示的是某个集合的最大值,那么可以每个子集的最大值的最大值。
        那么问题来了,进行集合f(i)的划分的依据?
        寻找最后一个不同点。

01背包

问题:
有N件物品和一个容量式V的背包,每件物品只能使用一次

第i件物品的体积式vi,价值是wi

求将这些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大
分析:
是一个有限集合的最值问题

  1. 状态表示:f(i,j)
    • 集合:只考虑前i个物品,且总体积不超过j的选择方案集合
    • 属性:max f(N,V)
  1. 状态计算:
    • 集合1,不选第i个物品的方案,maxf(i-1,j)
    • 集合2,选第i个物品的方案,maxf(i-1,j-vi)+wi
      两个集合满足:不重复和不遗漏
    • 将集合1和集合2取最大值就是所求答案 f(i,j)=max(f(i-1,j),f(i-1,j-vi)+wi)
  1. 优化
    分析状态计算可知,f(i,j)只记录f(i-1)即可。

朴素代码:


#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define PII pair<int, int>
#define x first
#define y second
const int maxn = 1e6 + 10;
const int INF = 0x3f3f3f3f;
const ll M = 1e9 + 7;
const int N = 1010;
int v[N],w[N];
int f[N][N];
   int n,m;
int main()
{
 
    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];
    
    return 0;

}

DP问题的优化是对代码进行等价替换,和题目无关。

优化代码:


#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define PII pair<int, int>
#define x first
#define y second
const int maxn = 1e6 + 10;
const int INF = 0x3f3f3f3f;
const ll M = 1e9 + 7;
const int N = 1010;
int v[N], w[N];
int f[N];
int n, m;
int main()
{

    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]);
        }
        /*
    等价于for (int j = m; j >= v[i]; j--)
        {
                f[j] = max(f[j], f[j] - v[i] + w[i]);
        }

        */
    }
    cout << f[n][m];

    return 0;
}

完全背包

完全背包的每个物品可以用无限次,其他条件与01背包一致。

  1. 状态表示:
    • 集合:只考虑前i个物品,且总体积不超过j的选择方案集合
    • 属性:max
  1. 状态计算:
    • 集合0:选0个第i个物品,f(i-1,j)
    • 集合1:选1个第i个物品,f(i-1,j-Vi)+Wi
    • 集合 …
    • 集合K:选k个第i个物品,f(i-1,j-kVi)+kWi
    • 集合 …
      每个集合具有排他性

推导f(i-1,j-kVi)+kWi
我们容易得到
\(f(i,j) = max(f(i-1,j),f(i-1,j-V_i)+W_i,f(i-1,j-2V_i)+2W_i,…,f(i-1,j-kV_i)+kW_i)\)

但是对于\(f(i,j-v) = max(f(i-1,j-V_i),f(i-1,j-2V_i)+W_i,…,f(i-1,j-kV_i)+kW_i)\)

立即推:\(f(i,j) = max( f(i-1,j),f(i,j-V_i)+W_i)\)

朴素代码:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define PII pair<int, int>
#define x first
#define y second
const int maxn = 1e6 + 10;
const int INF = 0x3f3f3f3f;
const ll M = 1e9 + 7;
const int N = 1010;
int v[N],w[N];
int f[N][N];
   int n,m;
int main()
{
 
    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][j-v[i]]+w[i]);
        }
    }
    cout<<f[n][m];
    return 0;

}

优化代码


#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define PII pair<int, int>
#define x first
#define y second
const int maxn = 1e6 + 10;
const int INF = 0x3f3f3f3f;
const ll M = 1e9 + 7;
const int N = 1010;
int v[N], w[N];
int f[N];
int n, m;
int main()
{

    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 = v[i]; j <= m; j++)
        {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m];
    return 0;
}

区间DP

合并石子问题

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量和,合并后与这两堆石子相邻的石子和新堆相邻,合并时由于选择的顺序不同,合并的代价也不同

比如有4堆 1 3 5 2 ,我们可以先合并1、2堆,代价为4,得到4,5,2,又合并1,2堆代价为9,得到9,2,再合并得到11,总代价为4+9+11

找出一种合适的方法,使总合并代价最小

  1. 状态表示:
    • 集合:所有将[i,j]合并成一堆的方案的集合
    • 属性:min
  1. 状态计算:
    • f[i,j] = f[i,k]+f[k+1,j]+s[j]-s[i-1];(s为本步骤代价)
      分析:
      对于区间[i,j],可以划分为左边和右边,问题的每一步都是左边和右边进行合并
      我们把左半边的最后一堆设为分界点 k ,
      因此我们问题变成,如何合并[i,k]和[k+1,j](
      容易想到两部分是完全独立的,可以分开计算,
      对于求f(i,k)和f(k+1,j)就是在各个子集中求出最小值,
      然后再对它们取个 min 就是f(i,j)。

代码:


#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define PII pair<int, int>
#define x first
#define y second
const int maxn = 1e6 + 10;
const int INF = 0x3f3f3f3f;
const ll M = 1e9 + 7;
const int N = 1010;
int n;
int s[N];		//前n项和
int f[N][N];

int main(){
    cin>>n;
    for(int i=1;i<=n;i++) cin>>s[i],s[i]+=s[i-1];

    for(int len=2;len<=n;len++){
        for(int i=1;i+len-1<=n;i++){
            int j=i+len-1;
            f[i][j]=1e8;
            for(int k=i;k<j;k++)
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
        }
    }
    cout<<f[1][n]<<endl;
    return 0;
}

线性dp

求最长公共子序列
给定两个长度分别为N和M的字符串A,B,求即是A的子序列又是B的子序列的字符串长度是多少。

  1. 状态表示:
    • 集合:所有A[1,i]与B[1,j]的公共子序列的集合
    • 属性:max
  1. 状态计算:
    • f[i,j] = max(f[i-1,j],f[i,j-1],f[i-1,j-1]+1)
      对最后一个不同点可以划分四类:

公共子序列不包含A [i] 也不包含 B [j]
公共子序列不包含A [i] 包含 B [j]
公共子序列包含A [i] 不包含 B [j]
公共子序列包含A [i] 也包含 B [j]
四种状态不重复不遗漏具有排他性
四种状态分别用00,01,10,11表示。

    • 当情况为11时
       当情况为11时,其必满足A [ i ] = B [ j ](字符相等)
      将这一子集根据变与不变划分为两个部分:
      一是前面 A 的 i - 1个字符与 B 的 j - 1个字符的情况(变的部分),就是 f(i-1,j-1)
      二是A [ i ] = b [ j ](不变的部分),就是1
      此时f(i,j) =f(i-1,j-1)+1
    • 当情况是00时
      易得f(i,j) = f(i-1,j-1)
    • 当情况是01时
      由于f(i-1,j) 包含两种情况,一种是B [ j ] 在公共子序列中,一种是B [ j ] 不在公共子序列中 。
      但本题重复对于求最大值没有影响,所以可以直接使用
      即f(i,j) = f(i-1,j)
    • 当情况是10时
      f(i,j) = f(i,j-1) 。原因同上

由上可以看出,01子集已包含00情况下的式子,
所以我们在代码构造中只需要计算 01、10、11三个部分即可。

代码:


#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define PII pair<int, int>
#define x first
#define y second
const int maxn = 1e6 + 10;
const int INF = 0x3f3f3f3f;
const ll M = 1e9 + 7;
const int N = 1010;

int n,m;
char a[N],b[N];
int f[N][N];

int main(){
    cin>>n>>m>>a+1>>b+1;
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            f[i][j] = max(f[i-1][j],f[i][j-1]);
            if(a[i]==b[j]) f[i][j]=max(f[i][j],f[i-1][j-1]+1);
        }
    }
    cout<<f[n][m]<<endl;
    return 0;
}

posted @ 2022-01-21 17:27  kingwzun  阅读(179)  评论(0编辑  收藏  举报