【1.10】动态规划-解分割回文串 III

一、题目

给你一个由小写字母组成的字符串s,和一个整数k。
 
请你按下面的要求分割字符串:
首先,你可以将s中的部分字符修改为其他的小写英文字母。
接着,你需要把s分割成k个非空且不相交的子串,并且每个子串都是回文串。
 
请返回以这种方式分割字符串所需修改的最少字符数。

 

示例1:

d5e18a3e84114547bb55b2a77d51392c.png

示例2:

6c3babd5bba64074b2f8994b8bcb4fc9.png

示例3:

cbe29239c6484027beee99d1686396f7.png

 
提示:
1)1  <=  k  <=  s.length  <=  100
2)s 中只含有小写英文字母。

 

 

二、求解思路

        这道题目要求我们通过修改字符将给定的字符串分割成 `k` 个回文子串,并找出最少需要修改的字符数。

        最直观的方法是尝试将字符串 `s` 分割成 `k` 个子串的所有可能组合,然后计算每个组合中将子串变成回文串所需修改的字符数量,最终返回这些组合中所需修改字符数最小的那个。例如,在示例二中,我们可以考虑将字符串分割成不同的子串组合,并计算每种组合中将子串变成回文串所需的最小修改次数。

1ddd2c2071b244659e3ce4f4c432030e.png

 

        然而,当字符串 `s` 较长时,上述方法的运行效率会非常低下。为了提高效率,我们可以采用动态规划来解决这个问题。我们定义 `dp[i][j]` 表示将字符串 `s` 的前 `i` 个字符分割成 `j` 个子串所需修改的最小字符数。显然,`i` 必须大于等于 `j`,否则无法将字符串分割成 `j` 个子串。

        动态规划的核心在于找出递推公式。当我们要求 `dp[i][j]` 时,只需要找出前 `m` 个字符分割成 `j-1` 个子串所需修改的最小字符数,再加上最后一个子串(即前 `i` 个字符减去前 `m` 个字符)所需修改的字符数。这里的关键在于如何选择合适的 `m` 值,使得总修改字符数最小。

        这种思路与经典的背包问题非常相似,特别是基础背包问题。在之前的讲解中,我们提到了基础背包问题,以及多重背包和完全背包问题,这些内容将在后续的讲解中详细探讨。通过动态规划,我们可以高效地找到将字符串 `s` 分割成 `k` 个回文子串所需的最小修改字符数。

所以递推公式我们很容易想到
 
dp[i][j] = dp[m][j -1] + change( s ,m, i -1);
 
这里我们需要枚举 m 的值,但要注意 m 必须大于等于 j -1,如下图所示
 

cbfc68b1f809476baf1e247cd31c5436.png

 

 

三、代码实现

这题要求的是返回最小的字符修改数,所以我们记录最小的即可
 
dp[i][j] = std::min(dp[i][j], dp[m][j - 1] + change(s, m, i - 1));
 
其中change( s ,m, i -1)表示字符串s [m, i -1]变成回文串所需要修改的字符数。

 

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

int change(const std::string& s, int left, int right) {
    int count = 0;
    while (left < right) {
        if (s[left++] != s[right--]) {
            count++;
        }
    }
    return count;
}

int palindromePartition(const std::string& s, int k) {
    int length = s.length();
    // dp[i][j]表示s的前i个字符分割成k个子串所修改的最少字符数。
    std::vector<std::vector<int>> dp(length + 1, std::vector<int>(k + 1, length));

    for (int i = 1; i <= length; i++) {
        // 前i个字符最大只能分割成i个子串,所以不能超过i,
        // 我们取i和k的最小值
        int len = std::min(i, k);
        for (int j = 1; j <= len; j++) {
            if (j == 1) {
                // 如果j等于1,则表示没有分割,我们直接计算
                dp[i][j] = change(s, 0, i - 1);
            } else {
                // 如果j不等于1,我们计算分割所需要修改的最小字符数,因为m的值要
                // 大于等于j-1,我们就从最小的开始枚举
                for (int m = j - 1; m < i; m++) {
                    // 递推公式
                    dp[i][j] = std::min(dp[i][j], dp[m][j - 1] + change(s, m, i - 1));
                }
            }
        }
    }
    // 返回前length个字符分割成k个子串所需要修改的最少字符数
    return dp[length][k];
}

int main() {
    std::string s = "aabbaa";
    int k = 2;
    int result = palindromePartition(s, k);
    std::cout << "Minimum number of changes: " << result << std::endl;
    return 0;
}
代码优化:
        上面代码中都有注释,这里就不在过多介绍,我们来看一下change方法,因为这里会涉
及到字符的重复计算,导致效率不高,我们可以先计算所有的子串变成回文串需要修改的字符数,然后再使用,来看下代码

 

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
#include <climits>

int palindromePartition(const std::string& s, int k) {
    int length = s.length();

    // palindrome[i][j]表示子串[i,j]转化为回文串所需要的修改的字符数
    std::vector<std::vector<int>> palindrome(length, std::vector<int>(length, 0));

    // 一行一行的从下往上(只遍历右上部分)
    for (int i = length - 2; i >= 0; i--) {
        for (int j = i + 1; j < length; j++) {
            palindrome[i][j] = palindrome[i + 1][j - 1] + (s[i] == s[j] ? 0 : 1);
        }
    }

    // dp[i][j]表示s的前i个字符分割成k个回文子串的最少次数,
    // 第一行和第一列应该都是0。
    std::vector<std::vector<int>> dp(length + 1, std::vector<int>(k + 1, INT_MAX));
    for (int i = 0; i < dp.size(); i++) {
        dp[i][0] = 0;
    }

    // 前i个字符,分割成j个回文子串
    for (int i = 1; i <= length; i++) {
        int len = std::min(i, k);
        for (int j = 1; j <= len; j++) {
            if (j == 1) // 字符串的下标是从0开始的,所以这里要减1
                dp[i][j] = palindrome[0][i - 1];
            else
                for (int m = j - 1; m < i; m++) {
                    dp[i][j] = std::min(dp[i][j], dp[m][j - 1] + palindrome[m][i - 1]);
                }
        }
    }
    return dp[length][k];
}

int main() {
    std::string s = "aabbaa";
    int k = 2;
    int result = palindromePartition(s, k);
    std::cout << "Minimum number of changes: " << result << std::endl;
    return 0;
}
因为 dp[i][j] 中 i 必须大于等于 j,所以这里遍历的时候可以有两种方式,如下图所示

 

d88654934c20432395e4d66e9003e37c.png

 

9b0fbea301d64a8f9895aa72aec8f105.png

 

 

  • 8
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城狮7号

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值