acwing1222. 密码脱落
我们可以使用 f ( i , j ) f(i,j) f(i,j) 表示区间 [ i , j ] [i,j] [i,j] 内删除的字符个数,使得该区间成为镜像字符串。
如果 s i = s j s_i = s_j si=sj,那么 f ( i , j ) = f ( i + 1 , j − 1 ) f(i,j) = f(i+1,j-1) f(i,j)=f(i+1,j−1)。
如果 s i ≠ s j s_i \neq s_j si=sj,那么我们可以删除 s i s_i si 或者 s j s_j sj,然后递归处理剩下的区间。因此,我们有
f ( i , j ) = min f ( i + 1 , j ) , f ( i , j − 1 ) + 1 f(i,j) = \min{f(i+1,j),\ f(i,j-1)} + 1 f(i,j)=minf(i+1,j), f(i,j−1)+1
当 i = j i=j i=j 时, f ( i , j ) = 0 f(i,j) = 0 f(i,j)=0。
答案即为 f ( 0 , n − 1 ) f(0,n-1) f(0,n−1),其中 n n n 是字符串的长度。
以下是使用区间 DP 来解决该问题的 C++ 代码实现:
#include
#include
#include
#include
using namespace std;
int main() {
string s;
cin >> s;
int n = s.size();
vector<vector<int>> f(n, vector<int>(n, 0));
for (int len = 2; len <= n; len++) {
for (int i = 0, j = i+len-1; j < n; i++, j++) {
if (s[i] == s[j]) {
f[i][j] = f[i+1][j-1];
} else {
f[i][j] = min(f[i+1][j], f[i][j-1]) + 1;
}
}
}
cout << f[0][n-1] << endl;
return 0;
}
我们可以使用数学归纳法来证明该算法的正确性。
首先,当区间长度为 1 时,即 i = j i=j i=j,此时 f ( i , j ) = 0 f(i,j)=0 f(i,j)=0,显然满足条件。
然后,我们假设对于任意的区间长度 l e n ≤ k len \le k len≤k,算法都能够正确计算出对应的删除字符个数。我们需要证明当区间长度为 l e n = k + 1 len=k+1 len=k+1 时,算法也能够正确计算出对应的删除字符个数。
对于区间 [ i , j ] [i,j] [i,j],如果 s i = s j s_i=s_j si=sj,那么我们可以将 s i s_i si 和 s j s_j sj 分别看成字符串的开头和结尾,然后递归计算 f ( i + 1 , j − 1 ) f(i+1,j-1) f(i+1,j−1) 即可。由于 f ( i + 1 , j − 1 ) f(i+1,j-1) f(i+1,j−1) 也是一个区间的解,根据归纳假设,我们可以认为 f ( i + 1 , j − 1 ) f(i+1,j-1) f(i+1,j−1) 已经计算正确。因此,当 s i = s j s_i=s_j si=sj 时, f ( i , j ) f(i,j) f(i,j) 肯定能够被正确计算出。
如果 s i ≠ s j s_i \neq s_j si=sj,那么我们可以删除 s i s_i si 或者 s j s_j sj,然后递归处理剩下的区间。由于 s i s_i si 和 s j s_j sj 是两个不同的字符,因此我们可以将 s i s_i si 和 s j s_j sj 分别看成字符串的开头和结尾,然后递归计算 f ( i + 1 , j ) f(i+1,j) f(i+1,j) 和 f ( i , j − 1 ) f(i,j-1) f(i,j−1)。由于 f ( i + 1 , j ) f(i+1,j) f(i+1,j) 和 f ( i , j − 1 ) f(i,j-1) f(i,j−1) 都是区间的解,根据归纳假设,我们可以认为它们已经被正确计算出。因此,当 s i ≠ s j s_i \neq s_j si=sj 时, f ( i , j ) f(i,j) f(i,j) 肯定能够被正确计算出。
综上所述,使用数学归纳法可以证明该算法的正确性。
对于状态 f ( i , j ) f(i,j) f(i,j),如果 s i = s j s_i=s_j si=sj,那么 f ( i , j ) f(i,j) f(i,j) 的值与 f ( i + 1 , j − 1 ) f(i+1,j-1) f(i+1,j−1) 的值相同。因此,我们只需要考虑 s i ≠ s j s_i \neq s_j si=sj 的情况。
由于 s i ≠ s j s_i \neq s_j si=sj,我们只能删除 s i s_i si 或者 s j s_j sj 中的一个字符,然后递归计算 f ( i + 1 , j ) f(i+1,j) f(i+1,j) 或者 f ( i , j − 1 ) f(i,j-1) f(i,j−1)。那么,我们需要选择删除哪一个字符呢?
我们可以考虑两种情况:
如果删除 s i s_i si,那么 s i + 1 s_{i+1} si+1 和 s j s_j sj 可能成为镜像字符串的开头和结尾,因此我们需要计算 f ( i + 1 , j ) f(i+1,j) f(i+1,j)。
如果删除 s j s_j sj,那么 s i s_i si 和 s j − 1 s_{j-1} sj−1 可能成为镜像字符串的开头和结尾,因此我们需要计算 f ( i , j − 1 ) f(i,j-1) f(i,j−1)。
由于我们希望计算出的是最少的删除字符个数,因此我们需要取上述两种情况的最小值,即 f ( i , j ) = min f ( i + 1 , j ) , f ( i , j − 1 ) + 1 f(i,j) = \min{f(i+1,j),\ f(i,j-1)} + 1 f(i,j)=minf(i+1,j), f(i,j−1)+1。
该转移方程的含义是,我们选择删除 s i s_i si 或者 s j s_j sj 中的一个字符,然后递归计算剩下的区间的删除字符个数,再加上删除的这个字符。我们需要选择删除字符个数最少的那种情况。
因此,状态转移方程 f ( i , j ) = min f ( i + 1 , j ) , f ( i , j − 1 ) + 1 f(i,j) = \min{f(i+1,j),\ f(i,j-1)} + 1 f(i,j)=minf(i+1,j), f(i,j−1)+1 是合理的。
#include
#include
#include
#include
using namespace std;
int main()
{
string s;
cin >> s;
int n = s.size();
vector<vector<int>> f(n, vector<int>(n, 0));
for (int len = 2; len <= n; len++)
{
for (int i = 0, j = i + len - 1; j < n; i++, j++)
{
if (s[i] == s[j])
{
f[i][j] = f[i + 1][j - 1];//j>=1 -> i+len-1>=1,i>=0 -> len-1>=1 -> len>=2
}
else
{
f[i][j] = min(f[i + 1][j], f[i][j - 1]) + 1;
}
}
}
cout << f[0][n - 1] << endl;
return 0;
}
该算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2),其中 n n n 是字符串的长度。由于第一个 for 循环的循环次数为 n − 1 n-1 n−1,第二个 for 循环的循环次数为 n − l e n + 1 n-len+1 n−len+1,因此总的循环次数为:
∑ l e n = 2 n ( n − l e n + 1 ) = n ( n − 1 ) 2 \sum_{len=2}^{n} (n-len+1) = \frac{n(n-1)}{2} len=2∑n(n−len+1)=2n(n−1)
因此,该算法的时间复杂度为 O ( n 2 ) O(n^2) O(n2)。