ACWing---DP

96 阅读3分钟

1、线性DP

898. 数字三角形

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。

        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5

顺序dp

  • f[i][j]:表示从第1行第1列到第i行第j列的路径最大值
  • f[i][j] = max(f[i-1][j],f[i-1][j-1]) + a[i][j]
  • 初始化:f[i][j] = -INF, f[1][1] = a[1][1]
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 510, INF = 0x3f3f3f3f;
int a[N][N], f[N][N];

int main()
{
    int n;
    cin >> n;
    
    for(int i = 1; i <= n; i++){
        for (int j = 1; j <= i; j ++ ){
            cin >> a[i][j];
        }
    }
   
      for(int i = 0; i <= n; i++){
        for (int j = 0; j <= n; j ++ ){
            f[i][j] = -INF;
        }
    }
    
    f[1][1] = a[1][1];

    for (int i = 2; i <= n; i ++ ){
        for (int j = 1; j <= i; j ++ ){
              
              f[i][j] = max(f[i-1][j],f[i-1][j-1]) + a[i][j];
        }
    }
    
    int maxn = -INF;
    for (int i = 1; i <= n; i++)
    {
        maxn = max(maxn, f[n][i]);
    }
    
    cout << maxn;
}

倒序dp

  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= i; j++) {
      cin >> f[i][j];
    }
  }

  for (int i = n; i >= 1; i--) {
    for (int j = 1; j <= n; j++) {
      f[i][j] += max(f[i + 1][j], f[i + 1][j + 1]);
    }
  }

895. 最长上升子序列

给定一个长度为 N 的数列,求数值严格单调递增的子序列的长度最长是多少。

一、

  • f[i]表示以w[i]为结尾的严格单调递增的子序列的长度
  • f[i] = max(f[j]+1, f[i]);
  • 初始化:f[i] = 1;
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;
int f[N],w[N];
//f[i] 表示以w[i]结尾的最大长度
int main() {
    int n;
    cin >> n;
    for (int i = 1; i <= n; i ++ ){
        cin >> w[i];
    }
    
    
    int maxn = 0;
    for(int i = 1; i <= n; i++){
        f[i] = 1;
        for(int j = 1; j < i; j++){
            if(w[i] > w[j]){
                f[i] =  max(f[j]+1,f[i]);
            }
        }
       maxn = max(maxn, f[i]);
    }
   cout << maxn;
    return 0;
}

二、优化o(nlogn)o(n*logn) dp+二分

  • f[i] 表示最长严格递增子序列的长i,的最小末尾元素w[i];
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;
int f[N], w[N];
// f[i] 表示最长严格递增子序列的长i,的最小末尾元素w[i];
int main() {
  int n;
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> w[i];
  }

  int j = 0;
  f[j++] = w[1];

  for (int i = 2; i <= n; i++) {
    if (w[i] > f[j - 1])
      f[j++] = w[i];
    else {
      // 在f找到第一个大于等于w[i],并更新值
      int l = 0, r = j - 1;

      while (l < r) {
        int mid = (l + r) / 2;
        if (f[mid] < w[i]) {
          l = mid + 1;
        } else {
          r = mid;
        }
      }
      f[l] = w[i];
    }
  }
  cout << j;
  return 0;
}

可简化

#include <bits/stdc++.h>
using namespace std;

const int N = 1e5+10;
int w[N];
vector<int>f;
// f[i] 表示最长严格递增子序列的长i,的最小末尾元素w[i];
int main() {
  int n;
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> w[i];
  }

  f.push_back(w[1]);

  for(int i = 2; i <= n; i++){
     if(w[i] > f.back()) f.push_back(w[i]);
     else{
        //找到第一个大于等于w[i]
        *lower_bound(f.begin(), f.end(), w[i]) = w[i];
     }
  }

  cout << f.size();
  return 0;
}

897. 最长公共子序列

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

  • f[i][j]表示A的前i个字符,和B的前j个字符的最长公共子序列长度
  • f[i][j] = max(f[i-1][j], f[i][j-1]); if(a[i] == b[j]){ f[i][j] = max(f[i-1][j-1]+1, f[i][j]);
  • 初始化: 0
    • a[i] b[j] 都不选 f[i-1][j-1]
    • a[i]选 b[j]不选 f[i][j-1]
    • a[i]不选 b[j]选 f[i-1][j]
    • a[i]==b[j] f[i][j]
#include <bits/stdc++.h>
using namespace std;

const int N = 1e3 + 10;
int f[N][N]; 
/*
f[i][j]:表示a的前i个子字符串,b的前j个子字符串的最长公共字符串
*/

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

902. 最短编辑距离

给定两个字符串 A 和 B,现在要将 A 经过若干操作变为 B,可进行的操作有:

  1. 删除–将字符串 A 中的某个字符删除。
  2. 插入–在字符串 A的某个位置插入某个字符。
  3. 替换–将字符串 A中的某个字符替换为另一个字符。

现在请你求出,将 A 变为 B 至少需要进行多少次操作。

  • f[i][j] 表示将a[1~i]转化为b[1~j]的最小操作次数
#include <bits/stdc++.h>
using namespace std;

const int N = 1010;
int f[N][N];

//f[i][j] 表示将a[1~i]转化为b[1~j]的最小操作次数

int main() {
  int n, m;
  string a, b;
  cin >> n >> a >> m >> b;
  
  a = " " + a;
  b = " " + b;
  
  //初始化
  for(int i = 1; i <= n; i++) f[i][0] = i;
   for(int i = 1; i <= m; i++) f[0][i] = i;
  
  for(int i = 1; i<= n; i++){
      for (int j = 1; j <= m; j ++ ){
          //删除
          f[i][j] = f[i-1][j] + 1;
          //插入
          f[i][j] = min(f[i][j],f[i][j-1]+1);
          //替换
          if(a[i] == b[j])
          f[i][j] = min(f[i][j],f[i-1][j-1]);
          else
          f[i][j] = min(f[i][j],f[i-1][j-1]+1);
      }
  }
  cout << f[n][m];
  return 0;
}

2、区间DP

区间DP模板

所有的区间dp问题枚举时,第一维通常是枚举区间长度,并且一般 len = 1 时用来初始化,枚举从 len = 2 开始;第二维枚举起点 i (右端点 j 自动获得,j = i + len - 1)

模板代码如下:

for (int len = 1; len <= n; len++) {         // 区间长度
    for (int i = 1; i + len - 1 <= n; i++) { // 枚举起点
        int j = i + len - 1;                 // 区间终点
        if (len == 1) {
            dp[i][j] = 初始值
            continue;
        }

        for (int k = i; k < j; k++) {        // 枚举分割点,构造状态转移方程
            dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + w[i][j]);
        }
    }
}

282. 石子合并

合并 N 堆石子,每次只能合并相邻的两堆石子,求最小代价

  • f[i][j]表示将i到j的石子合并的最小代价
  • f[i][j] = min(f[i][j] = min(f[i][k] + f[k+1][j],f[i][j]) + s[j] - s[i-1],f[i][j])
  • 初始化:f[i][j] = 0 (i == j)
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 310;
int f[N][N],s[N],a[N];
//f[i][j]表示将i-j的石头子合并成一堆的最小代价
//f[i][j] = min(f[i][k] + f[k+1][j],f[i][j]) + s[j] - s[i-1];
//初始化:i==j f[i][j] = 0;

int main()
{
    int n;
    cin >> n;
    for (int i = 1; i <= n; i ++ ){
        cin >> a[i];
        s[i] += s[i-1]+a[i]; //前缀和
    }
    
    memset(f, 0x3f, sizeof f);
    
    for(int len = 1; len <= n; len++){
        for (int i = 1; i+len-1 <= n; i ++ ){
            int j = i+len-1;
            if(len == 1){
                f[i][j] = 0;
                continue;
            }
            
            for(int k = i; k+1<=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];
    return 0;
}

3、记忆化搜索

AcWing 901. 滑雪

给定一个 R 行 C列的矩阵,表示一个矩形网格滑雪场。矩阵中第 i行第 j 列的点表示滑雪场的第 i 行第 j列区域的高度。 一个人从滑雪场中的某个区域内出发,每次可以向上下左右任意一个方向滑动一个单位距离。

当然,一个人能够滑动到某相邻区域的前提是该区域的高度低于自己目前所在区域的高度。 求出可完成的最长滑雪长度

一、dfs暴力-超时

  • 由于在dfs过程中会重复计算点,导致超时
#include <bits/stdc++.h>
using namespace std;

const int N = 301;
int r, c;
int f[N][N], a[N][N], st[N][N];
int maxn = 0;
int dx[] = {-1, 0, 1, 0}, dy[] = {0, -1, 0, 1};

void dfs(int u, int v, int cnt) {
  st[u][v] = 1;
  maxn = max(maxn, cnt);
  for (int i = 0; i < 4; i++) {
    int x = dx[i]+u, y = dy[i]+v;
    if (x >= 1 && x <= r && y >= 1 && y <= c && !st[x][y] &&
        a[u][v] > a[x][y]) {
      dfs(x, y, cnt + 1);
    }
  }

  st[u][v] = 0;
}

int main() {
  cin >> r >> c;

  for (int i = 1; i <= r; i++) {
    for (int j = 1; j <= c; j++) {
      cin >> a[i][j];
    }
  }

  for (int i = 1; i <= r; i++) {
    for (int j = 1; j <= c; j++) {
      dfs(i, j, 1);
    }
  }

  cout << maxn;
  return 0;
}

二、记忆化搜索-DP

  • f[i][j] 表示从i,j出发可完成的最长滑雪长度

image.png

#include <bits/stdc++.h>
using namespace std;

const int N = 301;
int r, c;
int f[N][N], a[N][N], st[N][N];
int maxn = 0;
int dx[] = {-1, 0, 1, 0}, dy[] = {0, -1, 0, 1};


//f[i][j]表示从i,j出发,最长滑雪长度
int dfs(int x, int y){
    int& v = f[x][y];
    if(v) return v; //已经求出
    v = 1; //初始化
    for (int i = 0; i < 4; i ++ ){
        int xx = x+dx[i], yy = y+dy[i];
        if(xx >= 1 && xx <= r && yy >= 1 && yy <=c && a[x][y] > a[xx][yy]){
            v = max(dfs(xx,yy) + 1, v);
        }
    }
    
   return v;
    
}

int main() {
  cin >> r >> c;

  for (int i = 1; i <= r; i++) {
    for (int j = 1; j <= c; j++) {
      cin >> a[i][j];
    }
  }

  for (int i = 1; i <= r; i++) {
    for (int j = 1; j <= c; j++) {
      maxn = max(dfs(i, j), maxn);
    }
  }

  cout << maxn;
  return 0;
}