3-17 字符串比较问题(算法设计与分析)

image-20240329113439067

示例

A = "cmc"B = "snmn",空格与其他字符的距离定值为 k = 2

可以做如下扩展(_ 表示空格):
{ A ′ = _ c _ m c _ B ′ = s _ n m _ n \rm\begin{cases}\rm A^\prime=\_c\_mc\_\\\\\rm B^\prime=s\_nm\_n\end{cases} A=_c_mc_B=s_nm_n
对应的距离是 k + k + k + 0 + k + k = 2 ∗ 5 = 10 k+k+k+0+k+k=2*5=10 k+k+k+0+k+k=25=10


这道题求的是所有扩展中距离最小的,是一个求最值的问题,可以先考虑贪心,然后递归、动态规划



题目分析

状态是什么?

这道题中,每做一次选择(是否插入空格),将会使得两个字符串的距离值变化

因此,距离值是状态,是需要转移的对象


如何改变状态?

要改变状态,需要插入空格;要插入空格,就要知道在什么地方插入

设置两个指针ij分别指向两个字符串,用来索引位置

现在出现一个问题,指针ij 应该是前到后遍历,还是后往前遍历?

  • 前到后    ⟹    \implies 自底向上:动态规划
  • 后到前    ⟹    \implies 自顶向下:递归分治

一般地,可以从递归法过渡到动态规划

每一次决策,有三种情况:

① 都不插入,此时计算当前位的差的绝对值

② 字符串 A 插入空格,B 不插入

③ 字符串 B 插入空格,A 不插入

选择哪一种情况,递归和动态规划的想法不同:

  • 递归:看一下①、②、③选择之后的情况如何
  • 动态规划:看一下①、②、③选择之前的情况如何


解法

递归法

递归法的双指针从后方开始遍历,并不断缩减问题范围(缩减字符串大小)

函数定义

int minDistance(string& A, string& B, int k, int i, int j) {
    // 终止条件
    
    // 递归
}

终止条件

从后往前遍历,因为字符串长度不一,因此会有一者提前到达字符串开头

由于从后往前遍历,后面的字符串都达到了最小距离,对于当前走到头的字符串来说,只能往前面补空格

可以用下面的语句实现:

if (i == 0 || j == 0) {
    // 因为其中一者为0,所以i + j只有 i 或 j
    return (i + j) * k;
}

例如:

image-20240414110947586

此时在字符串 A 前面补空格即可,补 (i + j) = 1 个即可


递归内容

对于三种决策:

① 都不插入,指针都往前移;

② A插入空格和B对应,B 的指针前移,A的不移动;

③ B插入空格和A对应,A的指针迁移,B的不移动

递归三种决策,最终返回其中的最小值,就是最小距离

// 不插入时,距离为当前位的差的绝对值
int cost = abs(A[i - 1] - B[j - 1]);
int skip = minDistance(A, B, k, i - 1, j - 1) + cost;
// A 插入空格和 B 对应,B指针前移,A的不移动
int insertA = minDistance(A, B, k, i, j - 1) + k;
// B 插入空格和 A 对应,A指针前移,B的不移动
int insertB = minDistance(A, B, k, i - 1, j) + k;
// 返回最小的
return min(skip, min(insertA, insertB));

代码

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

int minDistance(string& A, string& B, int k, int i, int j) {
    if (i == 0 || j == 0) {
        // 因为其中一者为0,所以i + j只有 i 或 j
        return (i + j) * k;
    }
	// 不插入时,距离为当前位的差的绝对值
    int cost = abs(A[i - 1] - B[j - 1]);
    int skip = minDistance(A, B, k, i - 1, j - 1) + cost;
    // A 插入空格和 B 对应,B指针前移,A的不移动
    int insertA = minDistance(A, B, k, i, j - 1) + k;
    // B 插入空格和 A 对应,A指针前移,B的不移动
    int insertB = minDistance(A, B, k, i - 1, j) + k;
	// 返回最小的
    return min(skip, min(insertA, insertB));
}

int main() 
{
    string A, B;
    int k;
    cin >> A >> B >> k;
    // i、j实际上是当前位的后一指针,所以这里input是length()
    int result = minDistance(A, B, k, A.length(), B.length());
    cout << result;
}

时间复杂度分析

每一次递归,要进行 3 次操作(3 个决策),因此递归树的分支是 3,是指数级别的复杂度

树的高度取决于字符串A、B的长度 m 、 n m、n mn

在最坏情况下,时间复杂度为 O ( 3 m + n ) \large O(3^{m+n}) O(3m+n)




动态规划

DP 数组定义

DP 定义为二维数组,DP[i][j] 表示指针 ij 所指向情况的最小距离值

以示例输入为例,需要做的就是填这张表:

image-20240414113202673


初始化

在头部的字符是动态规划的初始情况(因为动态规划是从前往后迭代)

和递归解法一样,此时往字符串前面填空格即可:

if (i == 0 || j == 0)
    DP[i][j] = (i + j) * k;

状态转移方程

从递归法其实可以看出状态转移方程了,要转移状态,需要做出决策,因此状态转移和决策密切相关

决策有三个,因此有三种转移状态的方式,直接给出
D P [   i   ] [   j   ] = min ⁡ { D P [   i − 1   ] [   j − 1   ] + c o s t D P [   i − 1   ] [   j   ] + k D P [   i   ] [   j − 1   ] + k \rm DP[\ i\ ][\ j\ ]=\min\begin{cases}\rm DP[\ i-1\ ][\ j-1\ ]+cost\\\\\rm DP[\ i-1\ ][\ j \ ]+k\\\\\rm DP[\ i\ ][\ j-1\ ]+k\end{cases} DP[ i ][ j ]=min DP[ i1 ][ j1 ]+costDP[ i1 ][ j ]+kDP[ i ][ j1 ]+k

代码

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

void dp(vector<vector<int>>& DP, int k, string& A, string& B) {
    // 填写行
    for (int i = 0; i <= DP.size() - 1; ++i) {
        // 填写列
        for (int j = 0; j <= DP[0].size() - 1; ++j) {
            // 初始化情况
            if (i == 0 || j == 0) {
                // 直接在前面添加空格
                DP[i][j] = (i + j) * k;
            } else {
                // 三种决策选择最优
                int cost = abs(A[i - 1] - B[j - 1]);
                DP[i][j] = min(DP[i - 1][j - 1] + cost, 
                                 min(DP[i - 1][j] + k, 
                                     DP[i][j - 1] + k));
            }
        }
    }
}

int main()
{
    string A, B;
    int k;
    cin >> A >> B >> k;

    int lengthMax = max(A.length(), B.length());
    // DP表弄得大些
    vector<vector<int>> DP(lengthMax + 1, vector<int>(lengthMax + 1, 0));

    dp(DP, k, A, B);

    cout << DP[A.length()][B.length()];
}

时间复杂度分析

主要的复杂度在于 dp() 函数中的双重循环,填写行时遍历行数 m m m,填写列时遍历列数 n n n

因此时间复杂度是 O ( m × n ) \large O(m\times n) O(m×n)

  • 16
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值