动态规划02-线性 DP

数字三角形

给定一个如下图所示的数字三角形,从顶部出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点,一直走到底层,要求找出一条路径,使路径上的数字的和最大。
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输入格式
第一行包含整数 n n n,表示数字三角形的层数。接下来 n n n行,每行包含若干整数,其中第 i i i行表示数字三角形第 i i i层包含的整数。
输出格式
输出一个整数,表示最大的路径数字和。
数据范围
1 ≤ n ≤ 500 1≤n≤500 1n500, − 10000 ≤ 三角形中的整数 ≤ 10000 −10000≤三角形中的整数≤10000 10000三角形中的整数10000
输入样例
5
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
输出样例
30

解决思路

这个题的解决思路比较简单,对于任意一个位置的数来讲,仅可以从左上方和右上方走到该位置,因此状态转移方程为 d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j − 1 ] , d p [ i − 1 ] [ j + 1 ] ) + g [ i ] [ j ] dp[i][j] = max(dp[i-1][j-1],dp[i-1][j+1]) + g[i][j] dp[i][j]=max(dp[i1][j1],dp[i1][j+1])+g[i][j]

代码实现

#include <iostream>
using namespace std;

const int N = 510, M = 1010;
int n;
int g[N][M];
bool st[N][M];
int dp[N][M];

int main()
{
    cin >> n;
    //使用双指针完成输入
    int l, r;
    l = r = n - 1;
    for(int i = 0; i < n; i++)
    {
        int x;
        for(int j = l; j <= r; j = j + 2)
        {
            cin >> x;
            g[i][j] = x;
            st[i][j] = true;
        }
        l--;
        r++;
    }
    int res = -1e9;
    l = r = n - 1;
    dp[0][n-1] = g[0][n-1];
    l--;
    r++;
    for(int i = 1; i < n; i++)
    {
        for(int j = l; j <= r; j = j + 2)
        {
            if(j != l && j != r)
            {
                dp[i][j] = max(dp[i-1][j-1],dp[i-1][j+1]) + g[i][j];
            }else if(j == l){
                dp[i][j] = dp[i - 1][j + 1] + g[i][j];
            }else if(j == r)
            {
                dp[i][j] = dp[i - 1][j - 1] + g[i][j];
            }
        }
        l--;
        r++;
    }
    for(int j = 0; j < 2*n -1; j = j + 2)
    {
        
        res = max(res,dp[n-1][j]);
    }
    cout << res << endl;
    return 0;
}

最长上升子序列 I

给定一个长度为 N N N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N N N。 第二行包含 N N N个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1 ≤ N ≤ 1000 1≤N≤1000 1N1000
− 1 0 9 ≤ 数列中的数 ≤ 1 0 9 −10^9≤数列中的数≤10^9 109数列中的数109
输入样例
7
3 1 2 1 8 5 6
输出样例
4

解决思路

关于一个 d p dp dp问题我们首先要搞清楚当前的集合是几维,要满足可以解决该问题同时维数最小这一要求,对于本问题我们仅需要一个一维数组 d p [ i ] dp[i] dp[i],接着我们需要知道当前的集合代表的意义,比如在背包问题中image.png d p [ i ] [ j ] dp[i][j] dp[i][j]往往代表的是在只从前 i i i个物品中选,且体积不大于 j j j的选法的集合,在三角形问题中 d p [ i ] [ j ] dp[i][j] dp[i][j]代表的是所有可以走到最底层的路径。而在本问题中 d p [ i ] dp[i] dp[i]代表的含义则是以第 i i i个数结尾的所有上升子序列。接着我们需要找到该集合的属性,对于本问题就是最大值 m a x max max。接下来确定了集合的状态表示之后我们需要对该集合进行计算,首先来进行集合划分,对于本问题因为所有的子序列的最后一个数都是第 i i i个数,那么可以使用最后一个数的前一个数进行表示,对 j ( 0 , 1 , 2 , . . . , i − 1 ) j(0,1,2,...,i-1) j(0,1,2,...,i1)进行遍历,得到以第 i i i个数结尾的所有上升子序列长度的最大值。状态转换方程为 d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) dp[i] = max(dp[i],dp[j]+1) dp[i]=max(dp[i],dp[j]+1),最终从这 i i i个数中选择最大值得到答案。

代码实现

#include <iostream>
using namespace std;

const int N = 1010;
int dp[N], l[N];
int n;

int main()
{
    cin >> n;
    int res = 0;
    for(int i = 0; i < n; i++)
    {
        cin >> l[i];
    }
    //集合dp[i]:以第i个数结尾的所有的上升子序列
    //属性:最大值
    //状态转移方程dp[i] = max(dp[i],dp[j]+1);
    //j从0到i-1;
    for(int i = 0; i < n; i++)
    {
        //初始长度为 1
        dp[i] = 1;
        for(int j = 0; j < i; j++)
        {
            if(l[i] > l[j])
            {
                dp[i] = max(dp[i],dp[j] + 1);
            }
        }
        res = max(res,dp[i]);
    }
    cout << res << endl;
}

最长上升子序列 II

给定一个长度为 N N N的数列,求数值严格单调递增的子序列的长度最长是多少。
输入格式
第一行包含整数 N N N。 第二行包含 N N N个整数,表示完整序列。
输出格式
输出一个整数,表示最大长度。
数据范围
1 ≤ N ≤ 100000 1≤N≤100000 1N100000
− 1 0 9 ≤ 数列中的数 ≤ 1 0 9 −10^9≤数列中的数≤10^9 109数列中的数109
输入样例
7
3 1 2 1 8 5 6
输出样例
4

解决思路

对于这个问题,我们的数据的数据范围成为十万,那么对于之前的 n 2 n^2 n2的时间的复杂度的做法显然是会造成超时,因此我们需要去除一些冗余项。对于一定长度的上升子序列我们仅需要存储最后一位中最小的那个数,例如对于长度为 1 1 1的子序列,子序列 3 3 3和子序列 1 1 1相比显然在后面可以容纳的长度要小,因此我们就可以省略它。我们定义一个数组 q q q用来存储不同长度的子序列最后的一个数的最小值。
image.png
对于每个元素都要在 q q q数组中找到小于它的最大的一个元素,对于这个可以使用二分查找算法。然后将更新数组。以上图为例,由于小于 a i a_i ai的最大的数是 q 4 q_4 q4,此时由于 q 5 q_5 q5是大于等于 a i a_i ai的,因此只能让 a i a_i ai替换上 q 5 q_5 q5,而不是从中间插入,需要舍弃原来的 q 5 q_5 q5 q 6 q_6 q6

代码实现

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1e5 + 10;
int a[N], q[N];
int n;

int main()
{
    cin >> n;
    int res = 0;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }
    
    int len = 0;
    q[0] = -2e9;
    for(int i = 1; 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);
        q[l + 1] = a[i];
    }
    
    cout << len << endl;
    return 0;
}

最长公共子序列

给定两个长度分别为 N N N M M M的字符串 A A A B B B,求既是 A A A的子序列又是 B B B的子序列的字符串长度最长是多少。
输入格式
第一行包含两个整数 N N N M M M
第二行包含一个长度为 N N N的字符串,表示字符串 A A A
第三行包含一个长度为 M M M的字符串,表示字符串 B B B
字符串均由小写字母构成。
输出格式
输出一个整数,表示最大长度。
数据范围
1 ≤ N , M ≤ 1000 1≤N,M≤1000 1N,M1000
输入样例
4 5
acbd
abedc
输出样例
3

解决思路

集合表示:
第一个序列中前i个数和第二个序列中前j个数的公共子序列
集合属性:
max
集合划分:
以第i个数和第j个数是否被选择定为集合的划分条件,因此共有四种情况。
状态转移:

  • i和j都不选dp[i][j] = dp[i - 1][j - 1]
  • i和j都选dp[i][j] = dp[i-1][j-1] + 1
  • i和j选择其中一个,这个是这四种情况中最不好理解的类别,我们无法进行直接表示。以dp[i][j - 1]举例,它的含义是第一个序列中前i个数和第二个序列中前j-1个数中的所有公共子序列,但不是在dp[i - 1][j - 1]的基础上一定选择第i个数不选择第j个数的情况,但幸运的是,本题仅仅需要的是最大值,而我们需要的情况一定是在dp[i][j - 1]的情况中,而dp[i][j - 1]会包含在dp[i][j]之中,因此,我们不需要担心会超出,仅仅有一些重复,但是并不会对最终结果有影响。

代码实现

#include <iostream>
using namespace std;

const int N = 1010;
int n, m;
char A[N], B[N];
int dp[N][N];

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

最短编辑距离

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

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

现在请你求出,将 A A A变为 B B B至少需要进行多少次操作。
输入格式
第一行包含整数 n n n,表示字符串 A A A的长度。
第二行包含一个长度为 n n n的字符串 A A A
第三行包含整数 m m m,表示字符串 B B B的长度。
第四行包含一个长度为 m m m的字符串 B B B
字符串中均只包含大小写字母。
输出格式
输出一个整数,表示最少操作次数。
数据范围
1 ≤ n , m ≤ 1000 1≤n,m≤1000 1n,m1000
输入样例
10
AGTCTGACGC
11
AGTAAGTAGGC
输出样例
4

解决思路

  • 集合状态

使用 d p [ i ] [ j ] dp[i][j] dp[i][j]表示将字符串 A A A的前 i i i个字符转换为字符串 B B B j j j个字符的转换方式

  • 集合属性

转换方式的最小值

  • 集合划分

分为插入操作,删除操作,替换操作
插入操作:
我们在分析时都以最后状态的前一个状态进行分析,如果当前字符串 A A A仅需要插入操作可以使得相同,那么也就代表字符串 A A A的前 i i i个字符和字符串 B B B的前 j − 1 j-1 j1个字符是相等的。
d p [ i ] [ j ] = d p [ i ] [ j − 1 ] + 1 dp[i][j] = dp[i][j-1] + 1 dp[i][j]=dp[i][j1]+1
删除操作:
字符串 A A A的前 i − 1 i-1 i1个字符和字符串 B B B的前 j j j个字符是相等的。
d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + 1 dp[i][j] = dp[i-1][j] + 1 dp[i][j]=dp[i1][j]+1
替换操作:
字符串 A A A的前 i − 1 i-1 i1个字符和字符串 B B B的前 j − 1 j-1 j1个字符是相等的。同时根据第 i i i个和第 j j j个字符是否相同进行判断是否进行操作。
d p [ i ] [ j ] = d p [ i − 1 ] [ j − 1 ] + 1 / 0 dp[i][j] = dp[i-1][j-1] + 1/0 dp[i][j]=dp[i1][j1]+1/0

  • 状态转移

d p [ i ] [ j ] = m i n ( d p [ i ] [ j − 1 ] + 1 , d p [ i − 1 ] [ j ] + 1 , d p [ i − 1 ] [ j − 1 ] + 1 / 0 ) dp[i][j] = min(dp[i][j - 1] + 1, dp[i - 1][j] + 1, dp[i - 1][j - 1] + 1/0) dp[i][j]=min(dp[i][j1]+1,dp[i1][j]+1,dp[i1][j1]+1/0)

代码实现

#include <iostream>
#include <algorithm>
using namespace std;

const int N = 1010;
char A[N], B[N];
int dp[N][N];
int n, m;

int main()
{
    cin >> n;
    for(int i = 1; i <= n; i++)
    {
        cin >> A[i];
    }
    cin >> m;
    for(int i = 1; i <= m; i++)
    {
        cin >> B[i];
    }
    //分别对于i为0和j为0的情况进行处理
    //初始化首行首列
    for(int i = 0; i <= m; i++)
    {
        dp[0][i] = i;
    }
    for(int i = 0; i <= n; i++)
    {
        dp[i][0] = i;
    }
    for(int i = 1; i <= n; i++)
    {
        for(int j = 1; j <= m; j++)
        {
            int c;
            if(A[i] != B[j])
            {
                c = dp[i - 1][j - 1] + 1;;
            }else
            {
                c = dp[i - 1][j - 1];
            }
            dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1);
            dp[i][j] = min(dp[i][j], c);
        }
    }
    cout << dp[n][m] << endl;
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值