示例
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=2∗5=10
这道题求的是所有扩展中距离最小的,是一个求最值的问题,可以先考虑贪心,然后递归、动态规划
题目分析
状态是什么?
这道题中,每做一次选择(是否插入空格),将会使得两个字符串的距离值变化
因此,距离值是状态,是需要转移的对象
如何改变状态?
要改变状态,需要插入空格;要插入空格,就要知道在什么地方插入
设置两个指针i
、j
分别指向两个字符串,用来索引位置
现在出现一个问题,指针i
、j
应该是前到后遍历,还是后往前遍历?
- 前到后 ⟹ \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;
}
例如:
此时在字符串 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 m、n
在最坏情况下,时间复杂度为 O ( 3 m + n ) \large O(3^{m+n}) O(3m+n)
动态规划
DP 数组定义
DP 定义为二维数组,DP[i][j]
表示指针 i
、j
所指向情况的最小距离值
以示例输入为例,需要做的就是填这张表:
初始化
在头部的字符是动态规划的初始情况(因为动态规划是从前往后迭代)
和递归解法一样,此时往字符串前面填空格即可:
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[ i−1 ][ j−1 ]+costDP[ i−1 ][ j ]+kDP[ i ][ j−1 ]+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)