还是那句话,字符串类的动态规划可以考虑往考虑前i个字符的反应问题的状态靠,比如本题,定义 f ( i ) f(i) f(i)为考虑字符串s的前i个字符分割为回文子串的最小分割数,假设字符串s的长度是n,那么问题的答案就是 f ( n ) f(n) f(n).
考虑状态转移方程:如果此时从头到底i个字符已经是回文串了,那么不需要分割,分割数是0;否则可以这样把问题分解:把串从第j个字符分开( 0 < j < i 0
至于如何判断回文串,可以用双指针法。
本题的边界条件,显然 f ( 0 ) = 0 , f ( 1 ) = 0 f(0) = 0,f(1) = 0 f(0)=0,f(1)=0,0个字符不需要分割、1个字符也不用分割它自己就是回文串;其他的情况,由于我们要求的是最小值,一种方案是全都初始化为INT_MAX
,另一种方案是发现含有i个字符的区间,最糟情况是分割i-1下,使得分成i个单个字符,它们都是回文串了,因此初始条件也可以写成 f ( i ) = i − 1 f(i)=i - 1 f(i)=i−1.
代码:
class Solution {
public:
int minCut(string s)
{
/*
f(i)考虑第1个字符到第i个字符,分割为回文子串的最小分割数
状态转移方程:
如果整体是一个回文串 也就是j等于0时 第j+1=1个字符到第i个字符是回文串
则f(i)等于0
否则j从1开始,如果第j+1个字符到第i个字符为回文串
则方法数等于在第j个字符后且一刀 1加上前面到底j个字符的方法数f(j)
也就是 f(i) = f(j) + 1 j < i
所以做一个搜索 f(i) = min(check(f(j))) + 1
*/
int size = s.size();
if (check(s, 0, size - 1) == true)
{
return 0;
}
vector<int> dp(size + 1, INT_MAX);
/*
*这里初始状态也可以弄成dp[0] = 0 其他的i dp[i] = i - 1
*初始最多分割次数就是都分割成1个字符
*/
dp[0] = 0;
dp[1] = 0;
for (int i = 2; i <= size; ++i)
{
for (int j = 0; j < i; ++j)
{
if (check(s, j, i - 1) == true)
{
if (j == 0)
{
/*对应整体*/
dp[i] = 0;
break;
}
else
{
/*对应j=1开始到i-1*/
dp[i] = min(dp[i], dp[j] + 1);
}
}
}
}
return dp[size];
}
bool check(const string& s, int left, int right)
{
if (left == right)
{
return true;
}
while (left < right)
{
if (s[left] != s[right])
{
return false;
}
++left;
--right;
}
return true;
}
};
时间复杂度: O ( n 3 ) O(n^3) O(n3)
空间复杂度: O ( n ) O(n) O(n)
本题还可以再优化,判断子串是否为回文串这一操作我们可以提前做好,并把状态储存在一个二维矩阵 s t a t e state state中, s t a t e [ i ] [ j ] state[i][j] state[i][j]表示下标为i到下标为j的子串是否为回文串,关于判断是否为回文串,我们之前在最长回文子串的三种解法中介绍过一种动态规划的方法,即定义F(i,j)表示下标为i到下标为j的子串是否为回文子串,状态转移方程为 F ( i , j ) = ( s [ i ] = = s [ j ] ) & & F ( i + 1 , j − 1 ) F(i,j) = (s[i]==s[j])\&\&F(i+1,j-1) F(i,j)=(s[i]==s[j])&&F(i+1,j−1),初始状态为 F ( i , i ) = t r u e , F ( i , i + 1 ) = ( s [ i ] = = s [ i + 1 ] ) F(i,i)=true,F(i,i+1) = (s[i]==s[i+1]) F(i,i)=true,F(i,i+1)=(s[i]==s[i+1]),这里可以单独拿出来作为一个 O ( n 2 ) O(n^2) O(n2)的算法放在外面把每个子串是否为回文子串先判断好咯,然后进循环直接判断 [ j , i − 1 ] [j,i-1] [j,i−1]是否为回文串是直接看 s t a t e [ j ] [ i − 1 ] state[j][i-1] state[j][i−1]即可。
针对这个状态转移方程,本人了解的有两种写法,一种是让外层的循环i从大到小遍历,内层的循环j从i到n遍历,这也就是从对角线右下角往上遍历的思路;另一种写法是控制此时判断的回文子串长度,从L=2开始遍历到L=n,因为判断长度为L的串是否为回文串需要的关系是长度为L-2的串是否为回文串,所以这样遍历也保证了可以覆盖答案。
两种实现的代码都给出:
从对角线右下角往上遍历:
class Solution {
public:
int minCut(string s)
{
/*
刚刚的算法是一个O(N^3)的算法,其实可以提前把判断(j,i-1)是否是回文串给分割出来
放到外面写 至于判断是否是回文串 可以用动态规划
f(i,j) = (s[i] == s[j]) && f(i + 1, j - 1)
f(i,i) = true; f(i, i + 1) = (s[i] == s[i + 1])
*/
int size = s.size();
auto state = getstate(s);
/*
走原本的动态规划 F(i) = 0 if state(0, i - 1) == true
else F(i) = min(F(i), F(j) + 1) j满足 state(j,i - 1) == true
*/
vector<int> dp(size + 1);
for (int i = 1; i <= size; ++i)
{
/*最初的最少分割次数就是字符数减1 即全都分割为1个字符*/
dp[i] = i - 1;
}
for (int i = 2; i <= size; ++i)
{
for (int j = 0; j < i; ++j)
{
if (state[j][i - 1])
{
if (j == 0)
{
dp[i] = 0;
break;
}
else
{
dp[i] = min(dp[i], dp[j] + 1);
}
}
}
}
return dp[size];
}
vector<vector<bool>> getstate(const string& s)
{
int n = s.size();
vector<vector<bool>> ret(n, vector<bool>(n));
for (int i = n - 1; i >= 0; --i)
{
for (int j = i; j < n; ++j)
{
if (i == j)
{
ret[i][j] = true;
}
else if (j == i + 1)
{
ret[i][j] = (s[i] == s[j]);
}
else
{
ret[i][j] = (s[i] == s[j]) && ret[i + 1][j - 1];
}
}
}
return ret;
}
};
控制每轮循环判断的回文串长度:
class Solution {
public:
int minCut(string s)
{
/*
刚刚的算法是一个O(N^3)的算法,其实可以提前把判断(j,i-1)是否是回文串给分割出来
放到外面写 至于判断是否是回文串 可以用动态规划
f(i,j) = (s[i] == s[j]) && f(i + 1, j - 1)
f(i,i) = true; f(i, i + 1) = (s[i] == s[i + 1])
*/
int size = s.size();
auto state = getstate(s);
/*
走原本的动态规划 F(i) = 0 if state(0, i - 1) == true
else F(i) = min(F(i), F(j) + 1) j满足 state(j,i - 1) == true
*/
vector<int> dp(size + 1);
for (int i = 1; i <= size; ++i)
{
/*最初的最少分割次数就是字符数减1 即全都分割为1个字符*/
dp[i] = i - 1;
}
for (int i = 2; i <= size; ++i)
{
for (int j = 0; j < i; ++j)
{
if (state[j][i - 1])
{
if (j == 0)
{
dp[i] = 0;
break;
}
else
{
dp[i] = min(dp[i], dp[j] + 1);
}
}
}
}
return dp[size];
}
vector<vector<bool>> getstate(const string& s)
{
int n = s.size();
vector<vector<bool>> ret(n, vector<bool>(n));
for (int i = 0; i < n; ++i)
{
ret[i][i] = true;
}
for (int L = 2; L <= n; ++L)
{
for (int i = 0; i < n; ++i)
{
int j = i + L - 1;
if (j >= n)
{
break;
}
if (L == 2)
{
ret[i][j] = (s[i] == s[j]);
}
else
{
ret[i][j] = (s[i] == s[j]) && ret[i + 1][j - 1];
}
}
}
return ret;
}
};