Palindrome II
Problem Statement
Given a string s, partition s such that every substring of the partition is a palindrome.
Return the minimum cuts needed for a palindrome partitioning of s.
For example, given s = "aab", Return 1 since the palindrome partitioning ["aa","b"] could be produced using 1 cut.
First we need to give some definitions. Then
Define
- $cut[k]$ to indicate the minimum number of cuts of the first $k$ characters in string $s$, {$s_0, s_1, ..., s_{k-1}$}.
- Thus, the index of cut is one more than index of s, which means the number of minimum cut of {$s_0, ..., s_i$} will be stored at $cut[s_i]$, or $cut[i+1]$. For simplicity, we use the terminology minimum cut of $s_i$ to indicate minimum cut of {$s_0, ..., s_i$}.
- So the problem can be expressed to compute $cut[n]$.
Obviously, the initial $cut[i]$ should be $i - 1$. Because every node itself is a palindrome. Thus $n$ nodes at most need $n-1$ partitions:
for(int i = 0; i < n; ++i) cut[i] = i - 1;
At first sight, the assignment $cut[0] = -1$ seems like a garbage value, but later we will explain it's very useful to assign $cut[0]$ to -1 to complete our problem.
In order to find some intuitions, let's think about the minimum cut of string {$s_0, s_1, ..., s_i$} with $cut[i+1]$ partitions.
Denote the cut like: {$s_0, s_1, ..., s_i$} = {$P_1, P_2, ..., P_k$} =
{
{$s_0, s_1, ..., s_{n_1}$},
{$s_{n_1+1}, s_{n_1+2}, ..., s_{n_2}$},
{...},
....,
{$s_{n_{k-2}+1}, s_{n_{k-2}+2}, ..., s_{n_{k-1}}$},
{$s_{n_{k-1}+1}, s_{n_{k-1}+2}, ..., s_{n_k}$}
}.
Immediately, we can get
- The node $s_i$ must be in $P_k$ with $s_i = s_{n_k}$. And $P_k$ is a palindrome.
- Also, consider the node $s_{n_{k-1}}$. We can state that {$P_1, P_2, ..., P_{k-1}$} is also the minimum cut of string {$s_0, s_1, ..., s_{n_{k-1}}$}.
(Otherwise, we can use {$s_0, s_1, ..., s_{n_{k'}}$}'s minimum cut {$P_1', P_2', ..., P_{k-1}'$} $\cup$ {$P_k$} to get {$s_0, s_1, ..., s_i$}'s smaller cut, which contradicates to {$P_1, P_2, ..., P_k$}'s minimum.)- Based on the above two statements, we also get $k = (k-1) + 1$, which means $cut[s_i] = cut[s_{n_{k-1}}] + 1$, or $cut[i+1] = cut[n_{k-1}+1] + 1$.
- The situation that the whole string {$s_0, ..., s_i$} is palindrome, i.e., $P_k =$ {$ s_0, ..., s_i $}, $P_{k-1} = \emptyset$ and $cut[i] = 0$, is also included.
As the deductions above, $cut[i] = cut[s_{-1}] + 1 = cut[0] + 1 = -1 + 1 = 0$, which matches the result of our problem.- Point 4 also explains that the initial assignment of cut[0] = -1 is useful and meaningful.
From these insights, we can get that for every minimum partition, let's say node $s_i$'s minimum cut, there exists some $j$, where $j \leq i$, such that
$s_i$'s minimum cut =
{$s_0, s_1, ..., s_{j-1}$}'s minimum cut
$\cup$
{$s_j, ..., s_i$}, where {$s_j, ..., s_i$} is a palindrome.
Thus, if we test a palindrome {$s_p, ..., s_q$}, it may be the minimum partition's last part $P_k$. So we may have a chance to update $s_q$'s minimum cut, i.e. updating cut[q+1] by $cut[s_q] = min(cut[s_q], cut[s_{p-1}]+1)$
if(isPalindrome(s, p, q)) cut[q+1] = max(cut[q+1], cut[p]+1)
As every node $s_i$'s $cut[i+1]$ can only be affected by its previous nodes, we can systematically detect palindrome and then update $cut[i+1]$ from the beginning of string s.
for(int point = 0; point < n; ++point){ //detect palindrome and update cut[i] ... }
For every node $s_i$, we consider palindrome centralized at $s_i$. Of course, there may be two cases of palindrome centralized at $s_i$:
- [$s_{i-j}, ..., s_i, ..., s_{i+j}$] with odd length $2j+1$.
- [$s_{i-(j-1)}, ..., s_i, s_{i+1}, ..., s_{i+j}$] with even length $2j$
We can expand the half length $j$ from zero until it reaches the boundary $s_0$ or $s_{n-1}$. And once the expansion of length stops at some length $j'$, there won't exist palindrome centralized at $i$ with length greater than $j'$.
// odd length for(int halfLength = 0; point-halfLength >= 0 && point+halfLength < n && s[point-halfLength] == s[point+halfLength]) cut[point+halfLength+1] = min(cut[point+halfLength+1], cut[point-halfLength]+1); // even length for(int halfLength = 1; point-halfLength+1 >= 0 && point+halfLength < n && s[point-halfLength+1] == s[point+halfLength]) cut[point+halfLength+1] = min(cut[point+halfLength+1], cut[point-halfLength+1]+1)
or if we use $i$ denote central point, and $j$ denote half length, we can get a piece of simply code:
for (int i = 0; i < n; ++i){ //odd length for (int j = 0; i-j >= 0 && i+j < n && s[i-j] == s[i+j]; ++j) cut[i+j+1] = min(cut[i+j+1], cut[i-j]+1); //even length for (int j = 1; i-j+1 >= 0 && i+j < n && s[i-j+1] == s[i+j]; ++j) cut[i+j+1] = min(cut[i+j+1], cut[i-j+1]+1); }
The complete code is:
int minCut(string s) { int n = s.size(); if(n <= 1) return 0; vector<int> cut(n+1, 0); for (int i = 0; i <= n; i++) cut[i] = i-1; for (int i = 0; i < n; ++i) { //odd length, i.e. i is the middle point, [i-j, ..., i, ..., i+j]; for (int j = 0; i-j >= 0 && i+j < n && s[i-j] == s[i+j]; ++j) cut[i+j+1] = min(cut[i+j+1], cut[i-j]+1); //even length, i.e. i is left side's endpoint, [i-(j-1), ..., i, i+1, ..., i+j]; for (int j = 1; i-j+1 >= 0 && i+j < n && s[i-j+1] == s[i+j]; ++j) cut[i+j+1] = min(cut[i+j+1], cut[i-j+1]+1); } return cut[n]; }
The running time is about two parts. The first is the assignment of $cut[i]$, $O(n)$.
The second part is divided by two for loop:
- $O(1 + 2 + 3 + 4 + ... + n/2) = O(n^2)$
- $O(1 + 2 + 3 + 4 + ... + n/2) = O(n^2)$
So the total running time is $O(n^2)$.
And obviously the space is $O(n)$.