I have been trying to solve the problem of circular palindrome all day, as part of a HackerRank challenge.
The traditional palindrome problem is basically to find the length of longest symmetric substrings (palindromes) within a bigger string. In this hackerRank challenge, the bigger string has a length limit of 105. It also adds another layer of complexity by asking us to find the lengths for each rotate string.
A similar question has been posted here, but I couldn't extract enough information to get my code working fast enough.
I have written the following Java code, which is a modification of the Manacher's algorithm in a rotated context:
package cards.myb.algorithms;
import java.io.*;
import java.util.*;
import java.text.*;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.math.*;
import java.util.regex.*;
public class CircularPalindrome {
public static void main(String[] args) {
/* Enter your code here. Read input from STDIN. Print output to STDOUT. Your class should be named Solution. */
boolean use_manacher = true;
int N = 0;
String S = "";
String inputFile = "D:\\Home\\Java\\AlgorithmPractice\\input16.txt";
String solutionFile = "D:\\Home\\Java\\AlgorithmPractice\\output16.txt";
System.out.println("Reading file " + inputFile);
File file = new File(inputFile);
try {
FileInputStream fis = new FileInputStream(file);
BufferedReader in = new BufferedReader(new InputStreamReader(fis));
N = Integer.valueOf(in.readLine());
S = in.readLine();
in.close();
} catch(IOException e) {
e.printStackTrace();
return;
}
// Convert string to char for efficiency
char[] sArr = S.toCharArray();
// Start timer
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long tStartNS = threadMXBean.getCurrentThreadCpuTime();
System.out.println("Starting clock");
// Allocate space for Sk, k=0...N-1
int[] lengths = new int[N];
int[] plenEvenArr = new int[N];
int[] plenOddArr = new int[N];
// ********************************************
// Part 1 : Even palindromes
// ********************************************
int best_right = N-1, best_left = 0, best_plen = 0;
for(int i=0; i<N; i++) {
// i points to the position at the palindrome center (between two elements)
boolean do_loop = true;
int left, right, plen;
// Mirror image optimization
// Manacher's algorithm
if(!use_manacher || i > best_right) {
left = i;
right = (i-1+N)%N;
plen = 0;
//System.out.println("plen=" + plen + ", right=" + right + ", left=" + left);
} else {
int i2 = (best_left + best_right - i + 1 + N) % N;
//System.out.println("i=" + i + ", best_left = " + best_left + ", best_right=" + best_right + ", i2=" + i2);
if(i2 >= i) {
left = i;
right = (i-1+N)%N;
plen = 0;
//System.out.println("plen=" + plen + ", right=" + right + ", left=" + left);
} else if(plenEvenArr[i2] < ((best_right - i + 1 + N) % N) * 2) {
plen = plenEvenArr[i2];
do_loop = false;
left = right = 0; // Avoid warnings
//System.out.println("use mirror image plenArr[i2]=" + plenArr[i2]);
} else {
plen = ((best_right - i + 1 + N) % N) * 2;
right = best_right;
left = (best_right - plen + 1 + N) % N;
//System.out.println("start from plen=" + plen + ", right=" + right + ", left=" + left);
}
}
// Find maximum even palindrome with center at i
for(; do_loop && plen < N-1; plen += 2) {
char expandleft = sArr[(left - 1 + N) % N];
char expandright = sArr[(right + 1) % N];
if(expandleft == expandright) {
left = (left - 1 + N) % N;
right = (right + 1) % N;
} else
break;
}
plenEvenArr[i] = plen;
// Keep track of best
if(plen > best_plen) {
best_right = right;
best_left = left;
best_plen = plen;
}
}
long tEvenNS = threadMXBean.getCurrentThreadCpuTime();
// ********************************************
// Part 2 : Odd palindromes
// ********************************************
best_right = 0; best_left = 0; best_plen = 1;
for(int i=0; i<N; i++) {
// i points to the middle element of the palindrome
boolean do_loop = true;
int left, right, plen;
// Mirror image optimization
// Manacher's algorithm
if(!use_manacher || i > best_right) {
left = right = i;
plen = 1;
// System.out.println("plen=" + plen + ", right=" + right + ", left=" + left);
} else {
int dist_to_best_right = (best_right - i + N) % N;
int i2 = (best_left + dist_to_best_right) % N;
int plen_best = dist_to_best_right * 2 + 1;
// System.out.println("i=" + i + ", best_left = " + best_left + ", dist_to_best_right=" + dist_to_best_right + ", best_right=" + best_right + ", i2=" + i2);
if(i2 >= i) {
left = right = i;
plen = 1;
// System.out.println("plen=" + plen + ", right=" + right + ", left=" + left);
} else if(plenOddArr[i2] < plen_best) {
plen = plenOddArr[i2];
do_loop = false;
left = right = 0; // Avoid warnings
// System.out.println("use mirror image plenArr[i2]=" + plenArr[i2]);
} else {
plen = plen_best;
right = best_right;
left = (i - dist_to_best_right + N) % N;
// System.out.println("start from plen=" + plen + ", right=" + right + ", left=" + left);
}
}
// Find maximum odd palindrome with center character at i
for(; plen < N-1 && do_loop; plen += 2) {
char expandleft = sArr[(left - 1 + N) % N];
char expandright = sArr[(right + 1) % N];
if(expandleft == expandright) {
left = (left - 1 + N) % N;
right = (right + 1) % N;
} else
break;
}
plenOddArr[i] = plen;
// Keep track of best
if(plen > best_plen) {
best_right = right;
best_left = left;
best_plen = plen;
}
}
long tOddNS = threadMXBean.getCurrentThreadCpuTime();
// ********************************************
// Part 3 : Find maximum palindrome for Sk
// ********************************************
for(int i=0; i<N; i++)
lengths[i] = 1;
for(int i=0; i<N; i++) {
int plenEven = plenEvenArr[i];
if(plenEven > 1) {
for(int k=0; k<N; k++) {
// Calculate length of the palindrome in Sk
int spaceLeft = (i >= k) ? (i - k) : (N + i - k);
int spaceRight = (i > k) ? (N + k - i) : (k - i);
// Corner case: i=k and plen=N
int len;
if(i==k && plenEven == N)
len = plenEven;
else
len = Math.min(plenEven, Math.min(spaceLeft*2, spaceRight*2));
// Update array
lengths[k] = Math.max(lengths[k], len);
}
}
}
for(int i=0; i<N; i++) {
int plenOdd = plenOddArr[i];
if(plenOdd > 1) {
for(int k=0; k<N; k++) {
// Calculate length of the palindrome in Sk
int spaceLeft = (i >= k) ? (i - k) : (N + i - k);
int spaceRight = (i >= k) ? (N + k - i - 1) : (k - i - 1);
int len = Math.min(plenOdd, Math.min(spaceLeft*2+1, spaceRight*2+1));
// Update array
lengths[k] = Math.max(lengths[k], len);
}
}
}
// End timer
long tEndNS = threadMXBean.getCurrentThreadCpuTime();
System.out.println("Clock stopped");
// Print result
for(int i=0; i<N; i++) {
System.out.println(lengths[i]);
}
// Read solution from file
int[] solution = new int[N];
System.out.println("Reading solution file " + solutionFile);
File solfile = new File(solutionFile);
try {
BufferedReader solin = new BufferedReader(new InputStreamReader(new FileInputStream(solfile)));
for(int i=0; i<N; i++) {
solution[i] = Integer.valueOf(solin.readLine());
}
solin.close();
} catch(IOException e) {
e.printStackTrace();
return;
}
// Check solution correctness
boolean correct = true;
for(int i=0; i<N; i++) {
if(lengths[i] != solution[i]) {
System.out.println(String.format("Mismatch to solution: lengths[%d] = %d (should be %d)", i, lengths[i], solution[i]));
correct = false;
}
}
if(correct)
System.out.println("Solution is correct");
// Calculate/print total elapsed time
System.out.println(String.format("Total CPU time : %.6f sec", (float)(tEndNS - tStartNS) / 1.0e9));
System.out.println(String.format(" Even palindromes took %.6f sec", (float)(tEvenNS - tStartNS) / 1.0e9));
System.out.println(String.format(" Odd palindromes took %.6f sec", (float)(tOddNS - tEvenNS) / 1.0e9));
System.out.println(String.format(" Length calculation took %.6f sec", (float)(tEndNS - tOddNS) / 1.0e9));
}
}
It works OK, but not fast enough. Here are the results with "use_manacher=true" and HackerRank input file input16.txt, which is a complex test case with almost 105 characters.
Solution is correct
Total CPU time : 79.841313 sec
Even palindromes took 18.220917 sec
Odd palindromes took 16.738907 sec
Length calculation took 44.881486 sec
"Solution is correct" means the output matches what's provided by HackerRank. With "use_manacher=false" so that it falls back to a straightforward O(n2) algorithm, where we start from each possible center point, and expand to both sides until we reach the length of the string we have:
Total CPU time : 85.582152 sec
Even palindromes took 20.451731 sec
Odd palindromes took 20.389331 sec
Length calculation took 44.741087 sec
What surprises me most is that optimization with Manacher's algorithm didn't help too much in a circular context (10-20% gain only). Also, finding the palindromes in a circular array (~35 sec) took less time than mapping them to lengths in rotated strings (~45 sec).
There are 100+ successful submissions with perfect score on HackerRank, which I think means there should be a solution that solves the same problem within 10 seconds on a typical CPU :)