加载中...

字符串dp+区间dp 核心[i,j] (1)枚举长度 左边端点为i 右边端点为j(区间合并) 有时候for k切割区间 (2)表示i为第一个串第i个位置 j为第二个串第j个位置 (3).

串问题

最短编辑距离https://www.acwing.com/problem/content/904/

f[i][j] i表示a的第i个位置 j表示b的第j个位置 让从原序列 a[i]和b[j]开始 让他们相等的最小操作
输入两个字符串 求让a和b相等的最小操作

1)删除操作:把a[i]删掉之后 a[1i]和b[1j]匹配
说明之前已经让a[1(i-1)]和b[1j]匹配 情况+1
f[i-1][j] + 1
2)插入操作:对a插入一个 之后a[i]与b[j]完全匹配,所以插入的就是b[j]
那说明之前a[1i]和b[1(j-1)]匹配
f[i][j-1] + 1
3)替换操作:把a[i]改成b[j]之后想要a[1i]与b[1j]匹配
那么修改这一位之前,a[1(i-1)]应该与b[1(j-1)]匹配
f[i-1][j-1] + 1
但是如果本来a[i]与b[j]这一位上就相等,那么不用改,即
f[i-1][j-1] + 0

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 1010;

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

int main()
{
    scanf("%d%s", &n, a + 1);
    scanf("%d%s", &m, b + 1);

    for (int i = 0; i <= m; i ++ ) f[0][i] = i;//a初始长度0 b长度i i多长增加多少次
    for (int i = 0; i <= n; i ++ ) f[i][0] = i;

    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= m; j ++ )
        {
            f[i][j] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);//先写必然满足的情况 在a[i-1]和b[j]相等的时候 只需要 a[i-1]补一个就行了 
            if (a[i] == b[j]) f[i][j] = min(f[i][j], f[i - 1][j - 1]);//这里就要求 i-1 和 b[j-1]相等的情况下 这里i和j相等就是继承之前的情况 
            else f[i][j] = min(f[i][j], f[i - 1][j - 1] + 1);// [i]和[j]不相等的情况下 需要在[i-1]和[j-1]相等的情况下 将i修改为和j相等 
        }

    printf("%d\n", f[n][m]);

    return 0;
}

最长公共子序列https://www.acwing.com/problem/content/899/

#include <iostream>
using namespace std;
const int N = 1010;
int n, m;
char a[N], b[N];
int f[N][N];//表示i,j 对到达 a[i]和b[i]最长公共子序列的长度  f[i][j]
int main() {
  cin >> n >> m >> a + 1 >> b + 1;
  for (int i = 1; i <= n; i++) {
    for (int j = 1; j <= m; j++) {
      if (a[i] == b[j]) {
        f[i][j] = f[i - 1][j - 1] + 1;//如果a[i]和b[i]字符相等 那么就在他们前面情况基础上a[i-1] b[j-1] +1 
      } else {
        f[i][j] = max(f[i - 1][j], f[i][j - 1]);//不相等的话 是不用+1 的只需要继承之前的情况就行因为两个字符一定有一个可以+1 表示遍历到这个字符和之前情况一样
      }
    }
  }
  cout << f[n][m] << '\n';
  return 0;
}

区间合并 i j 表示区间【i,j】

先枚举区间len为2的区间

石子合并https://www.acwing.com/problem/content/284/

i和j 表示i个石头到j个石头合并的最小价值
合并但这能选择相邻两个合并

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 310;

int n;
int s[N];
int f[N][N];

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &s[i]);

    for (int i = 1; i <= n; i ++ ) s[i] += s[i - 1];//用于快速求前缀和

    for (int len = 2; len <= n; len ++ )//长度从2开始
        for (int i = 1; i + len - 1 <= n; i ++ )
        {
            int l = i, r = i + len - 1;//r是长度
            f[l][r] = 1e8;//因为求最小值 先设置个较大值
            //到上面都是模板 

            for (int k = l; k < r; k ++ )//k是分割每个区间从中间切开 为两部分 i~k k+1~j
                f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);//l到k 和 k+1到r区间的最小值 +合并的消耗
        }

    printf("%d\n", f[1][n]);
    return 0;
}


回文串

由四种情况衍生出来

  1. 当i-1~j-1是回文串 并且i和j是相等的时候 f[i][j]=f[i+1][j-1]
    2.当i-1~j是回文串 因为多出了个i所以 f[i][j]=f[i-1][j]+1
    3.当i-1~j是回文串 因为多出了个j所以 f[i][j]=f[i][j-1]+1
    4.当i-1和j-1是回文串 因为ij不相等 多出了 i和j 的情况 所以应该是由 f[i][j]=f[i+1][j-1]+2 但是由于上面2和3的情况是也由f[i-1][j-1]衍生出来的 所以实际上计算的时候这一类是不显示的

密码脱落:最长回文子序列

i表示原来字符串的左端点 j表示原来字符串的右边端点
for 枚举长度
for 枚举左端点
f[i][j] i到j长度的 最长回文字符串长度

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1005;
int f[N][N];
char s[N];
int main()
{

    scanf("%s", &s);
    int n;n=strlen(s);
    for (int len = 1; len <= n; len ++ ){//枚举长度
        for (int i = 0; i+len-1 < n; i ++ ){//枚举左端点
            int j=i+len-1;
            if(len==1) f[i][j]=1;
            else {
                f[i][j]=max(f[i+1][j],f[i][j-1]);//左边少一个的状况 和 右边少一个字母的状况
                if(s[i]==s[j]){
                    f[i][j]=max(f[i][j],f[i+1][j-1]+2);
                }
            }
        }
    }
    
    cout << n-f[0][n-1];
    return 0;
}

括号配对 回文+区间匹配

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110,inf=1e9+10;
int f[N][N];
bool ismatch(char l,char r){
    if(l=='['&&r==']') return true;
    if(l=='('&&r==')') return true;
    return false;
    
}
int main()
{
    string s;
    cin >> s;
    int n=s.size();
    for (int i = 0; i < n; i ++ ) f[i][i]=1;//这里特别注意 因为len从2开始所以这里只能跟据实际意义 f[][]=1;

    for (int len = 2; len <= n; len ++ ){
        for (int i = 0; i+len-1 < n; i ++ ){
  //左右端点回文
            int l=i,j=i+len-1;
            f[i][j]=inf;
            if(ismatch( s[i],s[j] )) f[i][j]=f[i+1][j-1];
            f[i][j]=min(f[i][j],min(f[i][j-1],f[i+1][j])+1);            


            //下面是区间
            for (int k = l; k < j; k ++ ){
                f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);
            }
        }
    }
    
    cout << f[0][n-1];
    
    return 0;
    
}

区间dp

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 410, INF = 0x3f3f3f3f;

int n;
int w[N], s[N];
int f[N][N], g[N][N];

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

    for (int i = 1; i <= n * 2; i ++ ) s[i] = s[i - 1] + w[i];//用于快速求前缀和

    memset(f, 0x3f, sizeof f);//f最小是
    memset(g, -0x3f, sizeof g);//分别需要求最大值和最小值需要两个dp方程

    for (int len = 1; len <= n; len ++ )//枚举长度
        for (int l = 1; l + len - 1 <= n * 2; l ++ )
        {
            int r = l + len - 1;
            if (l == r) f[l][r] = g[l][r] = 0;//相等不能合并
            else
            {
                for (int k = l; k < r; k ++ )
                {
                    f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
                    g[l][r] = max(g[l][r], g[l][k] + g[k + 1][r] + s[r] - s[l - 1]);
                }
            }
        }

    int minv = INF, maxv = -INF;
    for (int i = 1; i <= n; i ++ )
    {
        minv = min(minv, f[i][i + n - 1]);//左端点是i 右端点n-1
        maxv = max(maxv, g[i][i + n - 1]);
    }

    cout << minv << endl << maxv << endl;

    return 0;
}


posted @ 2022-04-01 17:27  liang302  阅读(51)  评论(0编辑  收藏  举报