第五章 动态规划(二)

本节目录

  1. 线性DP
  2. 区间DP

1. 线性DP

递推顺序是线性的

898. 数字三角形

分析

见寒假每日一题

code

#include <iostream>
using namespace std;
const int N = 510;
int n, a[N][N];

int main(){
    scanf("%d", &n);
    
    for (int i = 1; i <= n; i ++ )
        for (int j = 1; j <= i; j ++ ) 
            cin >> a[i][j];
            
    
    for (int i = n - 1; i >= 1; i -- )
        for (int j = 1; j <= i; j ++ )
            a[i][j] += max(a[i + 1][j], a[i + 1][j + 1]);
            
    cout << a[1][1] << endl;
    
    return 0;
}

895.最长上升子序列

分析

code

#include <iostream>
using namespace std;
const int N = 1010;
int a[N], n;
int f[N];

int main(){
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
        cin >> a[i];
        
    int res = 0;

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

    
}

将最长上升子序列保存下来

g[N]保存下, 每个转移是怎么转移过来的

在这里插入图片描述

code(带方案)

#include <iostream>
using namespace std;
const int N = 1010;
int a[N], n;
int f[N];
int g[N];

int main(){
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ )
        cin >> a[i];
        
    int res = 0;

    
    for (int i = 1; i <= n; i ++ ){    
        f[i] = 1;
        for (int j = 1; j < i; j ++ )
            if (a[i] > a[j])
                if (f[j] + 1 > f[i]){
                    f[i] = f[j] + 1;
                    g[i] = j; // g[i] 保存i是从哪个状态转移过来的
                }
            
    }
    
    // 找出最优方案
    int k = 1;
    for (int i = 1; i <= n; i ++ )
        if (f[k] < f[i])
            k = i;
    
    cout << f[k] << endl;
    
    for (int i = 1, len = f[k]; i <= len; i ++ ){ 
        // 因为最优方案长度为f[k], 所以建个变量len, 而且k在循环中会发生变化, 一定要新建变量
        cout << a[k] << ' '; // f[k]的定义就是以a[k]结尾的最长上升子序列, 先输出a[k]
        k = g[k]; // 再考虑k是从哪个方案转移过来的, 令k = g[k];
    }
    
    return 0;

    
}

896. 最长上升子序列 II (习题课)

分析

7
3 1 2 1 8 5 6
我们之前求的时候, 比如求以8结尾的最长上升子序列的长度, 记成f(5)
然后求的时候, 枚举下倒数第2个数是哪个数(以倒数第2个数为分类依据)
分别求下每一类的最大值, 然后对每一类取max

然后考虑下每次求的时候, 没有没冗余

比如 3 1两个数, 后面的每一个数, 如果可以接到3的后面, 那么肯定可以接到1的后面, 比方说8可以接到3的后面, 8也可以接到1的后面, 因为1比3更小

所以可以发现3开头的这个上升子序列, 就没有必要存了, 1比3更好(1比3的适用范围更大)

假设当前求到了第i个数, 前面所有的最长上升子序列可以按长度分类,
长度是1的上升子序列, 可以存一个结尾最小的
长度是2的…, 可以存一个结尾最小的

因为可以接到大的数后面的话, 一定可以接到小的数后面, 因为小的数适用范围更广

受此启发, 可以将前面每种最长上升子序列结尾最小值存到数组里去

所有不同长度的最长上升子序列, 结尾值可以存下来

猜测: 随着长度增加, 结尾的值一定单调增加

也就是猜测: 长度越长的话, 结尾的最小值一定越大

证明:
如果长度是6的最长上升子序列和长度是5的最长上升子序列, 结尾最小值一样的话; 更有甚者, 或者说长度是6的结尾更小

看下, 长度是6的上升子序列, 第5个数一定比当前结尾值要小, 也就是我们找到了长度是5的最长上升子序列的结尾, 比我们当前存的长度是5的最长上升子序列结尾值要小, 那就矛盾了, 因为我们存的是长度是5的最长上升子序列的最小值

在这里插入图片描述

因此, 我们证明了长度是6的最长上升子序列结尾比长度是5的最长上升子序列结尾值要大

综上所述, 如果存在前面一个长度比较小的上升子序列结尾值比后面要大, 就会矛盾

因此整个序列是严格单调上升的

那么以a[i]为结尾的最长上升子序列的长度, 应该怎么求呢
首先a[i]可以接到比自己小的数的末尾的, 要想长度比较长的话, 将a[i]接到最大的 < a [ i ] <a[i] <a[i]的数的后面数就可以啦
(简而言之, 就是找距离a[i]最近的, 且末尾值<a[i]的上升子序列)

比方说q[4]是最大的小于a[i]的上升子序列, 那么我们找到的接了a[i]后, 长度是5的最长上升子序列
并且q[4]是最大的<a[i]的数, q[5]>=a[i], 也就是说长度是5的最长上升子序列结尾一定>=a[i], 所以a[i]一定不能接到长度>=5的最长上升子序列后面

因此以a[i]为结尾的最长上升子序列就是4 + 1 = 5

在这里插入图片描述
如果找出<a[i]的最大的数呢, 可以用二分, 算完之后, 直接将a[i] 更新到q[5]

时间复杂度: 首先先要找到当前元素可以接到哪个上升子序列后面O(logn)
更新的话是O(1)
一共n个数, 每个数都要二分一下, 所以O(nlogn)
数据范围10,000, 总的也就10, 000 * log(100, 000) = 100, 000~200, 000
完全ok

code

#include <iostream>
using namespace std;
const int N = 1e5 + 10;
int a[N], n;
int q[N];

int main(){
    cin >> n;
    for (int i = 0; i < n; i ++ ) cin >> a[i];
    q[0] = -1e9;
    int len = 0;
    for (int i = 0; i < n; i ++ ) {
        int l = 0, r = len;
        while (l < r){
            int mid = l + r + 1 >> 1;
            if (q[mid] < a[i]) l = mid; //一定要是<, 因为题目要求严格单调递增
            else r = mid - 1;
        }
        len = max(len, r + 1);// 这里找到了左边最大的小于a[i]的数f[r], 此时的序列长度是原来的长度r 加上 1
        q[r + 1] = a[i];        //长度是r + 1的严格上升子序列是以a[i]结尾的
    }
    cout << len << endl;
    
    return 0;
}

897. 最长公共子序列

分析

00 : 表示不选a[i], 不选b[j]
11 : 选择a[i], 选择b[j]

01 : 不选a[i], 选b[j]
注意⚠️: f[i - 1][j]的定义: 所有在第1个子序列的前i个字母中出现过, 且在第2个字母的前j个字母中出现过
前j个字母中出现过, 并不代表b[j]一定出现
在这里插入图片描述

在这里插入图片描述
所以f[i - 1][j]严格包含01的情况, 而且f[i - 1, j]的所有情况一定包含在f[i][j]的情况中

即以下关系
01 ⊂ f [ i − 1 ] [ j ] ⊂ f [ i ] [ j ] 01 \subset f[i - 1][j] \subset f[i][j] 01f[i1][j]f[i][j]
其他情况类似, 因此对 f [ i − 1 ] [ j − 1 ] , f [ i − 1 ] [ j ] , f [ i ] [ j − 1 ] , f [ i − 1 ] [ j − 1 ] + 1 f[i - 1][j - 1], f[i - 1][j], f[i][j - 1], f[i - 1][j - 1] + 1 f[i1][j1],f[i1][j],f[i][j1],f[i1][j1]+1取最大值就是 f [ i ] [ j ] f[i][j] f[i][j]的最大值
因为以上四种情况包括了f[i][j]的所有情况, 所以取max是正确的

就比如以下

在这里插入图片描述

求最大值分成的子集可以重复, 但是求数量分成的子问题, 一定不能重复

解释:代码中一般只分了3种情况的原因

虽然分成了4种情况, 但是在看最长公共子序列代码的时候, 第1种情况f[i - 1][j - 1]一般都不写, 因为f[i - 1][j - 1]这个子序列表示在a的前i - 1个子序列中出现, 在b的前j - 1个子序列中出现的子序列, 这些一定包含在了f[i - 1][j] 和f[i][j - 1]
f [ i − 1 ] [ j − 1 ] ⊂ f [ i − 1 ] [ j ] f [ i − 1 ] [ j − 1 ] ⊂ f [ i ] [ j − 1 ] f[i - 1][j - 1] \subset f[i - 1][j]\\ f[i - 1][j - 1] \subset f[i][j - 1]\\ f[i1][j1]f[i1][j]f[i1][j1]f[i][j1]

所以一般情况下, 看最长公共子序的代码的时候, 只有3种情况

在这里插入图片描述

code

#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
int f[N][N];
char a[N], b[N];
int n, m;

int main(){
    scanf("%d%d", &n, &m);
    
    scanf("%s%s", 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;
}

282. 石子合并

分析

区间dp, 状态表示的时候, 表示的是某一个区间

在这里插入图片描述

code

#include <iostream>
using namespace std;
const int N = 310, INF = 0x3f3f3f3f;
int f[N][N];
int w[N], s[N];
int n, m;

int main(){
    cin >> n;
    for (int i = 1; i <= n; i ++){
        cin >> w[i];
        s[i] = s[i - 1] + w[i];
    }
    
    for (int len = 2; len <= n; len ++)//区间dp首先枚举区间长度
        for (int l = 1; l + len - 1 <= n; l ++){ //枚举左端点
            int r = l + len - 1;
            f[l][r] = INF;
            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]);
        }
    cout << f[1][n] << endl;
    return 0;
}

902. 最短编辑距离(习题课)

分析


删: 删掉a[i], 那么a[1~i] 与 b[1~j]匹配, 那么在此之前a[1~i - 1]与b[1~j]匹配, f[i - 1][j]
因此f[i][j] = min(f[i][j], f[i - 1][j] + 1);


增: 因为添加完a[i]后, a[i]和b[j]匹配, 所以添加的这个a[i]一定是b[j], 那么在添加a[i]之前, a[1 ~ i]一定已经匹配b[1 ~ j - 1], 所以前面一步是f[i][j - 1]
因此f[i][j] = min(f[i][j], f[i][j - 1] + 1);


改: 改完之后a[1 ~ i] 变成b[1~j], 那么改一定是a[i]变成b[j], 改的话可以分两种情况, 如果a[i]和b[j]相等了, 可以不用变, 那么其实不需要变; 如果不相等, 需要增加一步更改操作, 将a[i]变成b[j]

先不看最后一步操作, 应该让状态达到哪种程度, 应该先让a[1 ~ i - 1] 与 b[1 ~ j - 1]匹配上, 因此需要f[i - 1, j - 1]
做完之后, 再让a[i]变成b[j]
因此改的步骤f[i - 1][j - 1] + 1/0(视a[i] 是否= b[j])

code

#include <iostream>
using namespace std;
const int N = 1010;

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

int main(){
    scanf("%d%s", &n, a + 1);
    scanf("%d%s", &m, b + 1);
    
    for (int i = 1; i <= n; i ++ ) f[i][0] = i; // 将a[1 ~ i] 变成b[0]
    for (int i = 1; i <= m; i ++ ) f[0][i] = i; // 将b[1 ~ i] 变成a[0]
    
    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); // 注意 + 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] << endl;
    
    return 0;
    
}

899. 编辑距离(习题课)

分析

就是应用上面的函数

code

#include <iostream>
#include <cstring>
using namespace std;
const int N = 1010;
char str[N][N];
int n, m;
int f[N][N];

int edit_distance(char a[], char b[]){
    int n = strlen(a + 1), m = strlen(b + 1);
    
    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] = min(f[i - 1][j] + 1, f[i][j - 1] + 1);
            f[i][j] = min(f[i][j], f[i - 1][j - 1] + (a[i] != b[j]));
        }
        
    return f[n][m];
            
}
int main(){
    scanf("%d%d", &n, &m);
    for (int i = 0; i < n; i ++ )
        scanf("%s", str[i] + 1);
    
    while (m -- ){
        int cnt = 0, limit;
        char c[1010];
        scanf("%s %d", c + 1, &limit);
        for (int i = 0; i < n; i ++ )
            if (edit_distance(str[i], c) <= limit) cnt ++;
        cout << cnt  << endl;
    }
    
    return 0;
}

900. 整数划分(习题课)

分析

不考虑顺序的组合
4 = 1 + 2 + 1 = 2 + 1 + 1 = 1 + 1 + 2
这3种都是一样的

1.看成背包问题, 背包容量n, 物品的体积分别是1, 2, 3, …, n

求恰好装满背包的方案数, 每种物品用无限次, 因此是完全背包问题
因为不考虑顺序, 每种背包的选法都对应一种数字的划分方式
每种数字的划分方式都对应一种背包的选法

一一对应的, 所以方案是一样的

状态数量n^2, 转移数量O(n), 总时间复杂度不是O(n^3)
当体积是1的时候 s = 1000/ 1
2, s = 1000/ 2

1000, s = 1000/1000

1/1 + 1/2 + 1/3 + … + 1/n = log(n)

code

#include <iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int f[N];
int n;

int main(){
    cin >> n;
    f[0] = 1;
    
    for (int i = 1; i <= n; i ++ ) // i表示第i个物品的体积
        for (int j = i; j <= n; j ++ ) // j 表示总体积
            f[j] = (f[j] + f[j - i]) % mod;
            
    cout << f[n] << endl;
    
    return 0;
}

分析(按最小值为1划分)

f[i][j] : 所有总和是i, 并且恰好表示成j个数的和的方案
如果最小值是1, 那么可以将这个1去掉, f[i][j] 由 f[i- 1][j - 1]转化过来, 即:
和是i - 1, 并且恰好表示成j - 1个数的和

所有和是i - 1, 并且恰好表示成j - 1个数的和 + 1个1, 就是总和是i, 并且表示成j个数, 最小值是1的方案,
所以这两个集合是一一对应的

对于最小值 > 1的, 令每个数-1, 那么总和-j, 就变成f[i - j][j]

在这里插入图片描述

答案需要将f[n][1] + … f[n][n]

code

#include <iostream>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
int n;
int f[N][N];

int main(){
    scanf("%d", &n);
    
    f[0][0] = 1;
    
    for (int i = 1; i <= n; i ++ )  
        for (int j = 1; j <= i; j ++ )
            f[i][j] = (f[i - 1][j - 1] + f[i - j][j]) % mod;
            
    int res = 0;
    for (int i = 1; i <= n; i ++ ) res = (res + f[n][i]) % mod;
    
    cout << res << endl;
    
    return 0;
    
}

Week8 习题课 01:20:00高精度压位

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值