Longest Palindromic Subsequence (not substring, for the substring solution, please find answers in my GitHub) is to find (one of) the longest palindromic subsequence in a string. It is short for LPS in this article (here we only need to find one of LPS). See an example from another article.
s = 1 5 2 4 3 3 2 4 5 1 LPS = 1 5 4 3 3 4 5 1
Ok, I think now you are clear about the definition of the LPS (again not substring). Let's start our related algorithms and let them evolve.
I. Naive Recursive Version, time complexity O(2^n) (worst case, O(n)=2O(n-1)+1), space complexity O(n) = O(h) for stack operation.
string longestPalindrome(const string &s, int begin, int end) {
if (begin > end) return "";
if (begin == end) return s.substr(begin, 1);
if (s[begin] == s[end]) return s[begin] + longestPalindrome(s, begin+1, end-1) + s[end];
string str1 = longestPalindrome(s, begin, end-1);
string str2 = longestPalindrome(s, begin+1, end);
return str1.size() > str2.size()? str1: str2;
}
string longestPalindrome(string s) {
return longestPalindrome(s, 0, s.size()-1);
}
Here we return immediately once we find the s[begin] == s[end] holds, ignoring s[begin+1, end] and s[begin, end-1].
Why? Does this mean s[begin] and s[end] are parts of the LPS we are seeking for if s[begin] == s[end] holds?
1. We can guarantee that s[begin, end] has the LPS.
- But it is possible we get the same LPS from s[begin+1, end] or s[begin, end-1], such as "asaa" (LPS: asa).
- However, LPS' from s[begin+1, end] or s[begin, end-1] is <= LPS from s[begin, end]. It is simply because s[begin, end] is longer than s[begin+1, end] or s[begin, end-1].
2. s[begin] and s[end] are parts of s[begin, end]'s LPS, head and tail respectively. If this does not hold, we have two situations.
- s[begin, end]'s LPS does not have s[begin] nor s[end]. We know it is not true as s[begin] + LPS + s[end] > LPS. So the real LPS should be s[begin] + LPS + s[end], not LPS.
- s[begin, end]'s LPS contains only one of s[begin] and s[end]. It is true in certain cases, such as "asaa" (LPS: asa). But In these cases, LPS.head == LPS.tail holdssince palindrome property. If LPS contains s[begin] (vice versa for s[end]), LPS.tail == LPS.head == s[begin] == s[end]. So rule 2 still holds.
string longestPalindrome(const string &s, int begin, int end, vector<vector<string>> &mem) {
if (begin > end) return "";
if (mem[begin][end] != "#") return mem[begin][end];
if (begin == end) return mem[begin][end].assign(s, begin, 1);
if (s[begin] == s[end]) return mem[begin][end] = s[begin] + longestPalindrome(s, begin+1, end-1, mem) + s[end];
string str1 = longestPalindrome(s, begin, end-1, mem);
string str2 = longestPalindrome(s, begin+1, end, mem);
return mem[begin][end] = str1.size() > str2.size()? str1: str2;
}
string longestPalindrome(string s) {
int N = s.size();
vector<vector<string>> mem(N, vector<string>(N, "#"));
return longestPalindrome(s, 0, N-1, mem);
}
string longestPalindrome(string s) {
if (s.empty()) return s;
int N = s.size();
vector<vector<string>> mem(N, vector<string>(N));
for (int i=N-1; i>=0; --i) {
for (int j=i; j<N; ++j) {
if (s[i] == s[j]) {
if (j-i <= 1) mem[i][j].assign(s, i, j-i+1);
else mem[i][j] = s[i] + mem[i+1][j-1] + s[j];
} else {
mem[i][j] = mem[i][j-1].size()>=mem[i+1][j].size()? mem[i][j-1]: mem[i+1][j];
}
}
}
return mem[0][N-1];
}
IV. Space Optimized Bottom-up Dynamic Programming Version, time complexity O(n^2), space complexity O(n).
string longestPalindrome(string s) {
if (s.empty()) return s;
int N = s.size();
vector<vector<string>> mem(2, vector<string>(N));
for (int i=N-1; i>=0; --i) {
for (int j=i; j<N; ++j) {
if (s[i] == s[j]) {
if (j-i <= 1) mem[i%2][j].assign(s, i, j-i+1);
else mem[i%2][j] = s[i] + mem[(i+1)%2][j-1] + s[j];
} else {
mem[i%2][j] = mem[i%2][j-1].size()>=mem[(i+1)%2][j].size()? mem[i%2][j-1]: mem[(i+1)%2][j];
}
}
}
return mem[0][N-1];
}
V. Tracing Bottom-up Dynamic Programming Version, time complexity O(n^2), space complexity O(n^2).
/***************** This version is used when string is very large ***************/
// time complexity O(n), space complexity O(n)
string getLPS(vector<vector<char>> &trace, int i, int j, const string &s) {
if (i > j) return "";
if (i == j) return s.substr(i, 1);
if (trace[i][j] == '#') return s[i] + getLPS(trace, i+1, j-1, s) + s[j];
if (trace[i][j] == '-') return getLPS(trace, i, j-1, s);
if (trace[i][j] == '|') return getLPS(trace, i+1, j, s);
}
// time complexity O(n^2), space complexity O(n^2)
int longestPalindrome(string s, string &res) {
if (s.empty()) return 0;
int N = s.size();
vector<vector<int>> mem(2, vector<int>(N));
vector<vector<char>> trace(N, vector<char>(N, '#'));
for (int i=N-1; i>=0; --i) {
for (int j=i; j<N; ++j) {
if (s[i] == s[j]) {
if (j-i <= 1) mem[i%2][j] = j - i + 1;
else mem[i%2][j] = mem[(i+1)%2][j-1] + 2;
} else {
mem[i%2][j] = max(mem[i%2][j-1], mem[(i+1)%2][j]);
trace[i][j] = mem[i%2][j-1]>=mem[(i+1)%2][j]? '-': '|';
}
}
}
res = getLPS(trace, 0, N-1, s);
return mem[0][N-1];
}
VI. Tracing Bottom-up Dynamic Programming Version, time complexity O(n^2), space complexity O(n^2).Only one mem!
/***************** This version is used when string is very large ***************/
// time complexity O(n), space complexity O(n)
string getLPS(const vector<vector<int>> &mem, int i, int j, const string &s) {
if (i > j) return "";
if (i == j) return s.substr(i, 1);
if (s[i] == s[j]) return s[i] + getLPS(mem, i+1, j-1, s) + s[j];
if (mem[i][j-1] >= mem[i+1][j]) return getLPS(mem, i, j-1, s);
if (mem[i][j-1] < mem[i+1][j]) return getLPS(mem, i+1, j, s);
}
// time complexity O(n^2), space complexity O(n^2)
int longestPalindrome(string s, string &res) {
if (s.empty()) return 0;
int N = s.size();
vector<vector<int>> mem(N, vector<int>(N));
for (int i=N-1; i>=0; --i) {
for (int j=i; j<N; ++j) {
if (s[i] == s[j]) {
if (j-i <= 1) mem[i][j] = j - i + 1;
else mem[i][j] = mem[i+1][j-1] + 2;
} else {
mem[i][j] = max(mem[i][j-1], mem[i+1][j]);
}
}
}
res = getLPS(mem, 0, N-1, s);
return mem[0][N-1];
}
VII. Tracing Top-down Dynamic Programming Version, time complexity O(n^2), space complexity O(n^2).Only one mem!
/***************** This version is used when string is very large ***************/
// time complexity O(n), space complexity O(n)
string getLPS(const vector<vector<int>> &mem, int i, int j, const string &s) {
if (i > j) return "";
if (i == j) return s.substr(i, 1);
if (s[i] == s[j]) return s[i] + getLPS(mem, i+1, j-1, s) + s[j];
if (mem[i][j-1] >= mem[i+1][j]) return getLPS(mem, i, j-1, s);
if (mem[i][j-1] < mem[i+1][j]) return getLPS(mem, i+1, j, s);
}
// time complexity O(n^2), space complexity O(n^2)
int longestPalindrome(const string &s, int begin, int end, vector<vector<int>> &mem) {
if (begin > end) return 0;
if (mem[begin][end] != 0) return mem[begin][end];
if (begin == end) return mem[begin][end] = 1;
if (s[begin] == s[end]) return mem[begin][end] = 2 + longestPalindrome(s, begin+1, end-1, mem);
return mem[begin][end] = max(longestPalindrome(s, begin, end-1, mem), longestPalindrome(s, begin+1, end, mem));
}
string longestPalindrome(string s) {
int N = s.size();
vector<vector<int>> mem(N, vector<int>(N, 0));
longestPalindrome(s, 0, N-1, mem);
return getLPS(mem, 0, N-1, s);
}