0

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 :)

jpyams
  • 4,030
  • 9
  • 41
  • 66
Darsen Lu
  • 623
  • 7
  • 10
  • 2
    Great question! Working code should probably be migrated to our sister site, http://codereview.stackexchange.com/ . – rajah9 Jul 29 '16 at 12:59
  • 2
    Can't you get rid of the % operations in the inner loops, which are time consuming ? –  Jul 29 '16 at 13:04
  • 1
    You definitely can't do double for-loops each iterating `N` times (total `N^2`) in your part 3. I guess the correct solution will involve some memoization. – justhalf Jul 29 '16 at 13:07
  • Sure, I will post the code there once its ready – Darsen Lu Jul 29 '16 at 13:29
  • Thanks Yves, the runtime now goes down to :Total CPU time : 53.976347 sec; Even palindromes took 3.759624 sec; Odd palindromes took 3.650423 sec; Length calculation took 46.566298 sec – Darsen Lu Jul 29 '16 at 13:30

2 Answers2

1

This is still very slow... all I know is DONT use recursive palindrome check

i.e.

static boolean isPali(String s) {
    if (s.length() == 0 || s.length() == 1)
        return true;
    if (s.charAt(0) == s.charAt(s.length() - 1))
        return isPali(s.substring(1, s.length() - 1));
    return false;
}

This was my answer:

import java.io.*;
import java.util.*;

public class Solution {

   // I found this on stackoverflow it was about 3 times faster for a test I ran
   static boolean isPali(String str){
       StringBuilder sb = new StringBuilder(str);
       return str.equals(sb.reverse().toString());
   }

   static int subs(String s){
       int max=0;
       for(int j = 0 ; j < s.length(); j++ ) {
           for (int i = 1; i <= s.length() - j; i++) {
               String sub = s.substring(j, j+i);
               if(isPali(sub) && sub.length()>max){
                   max = sub.length();
               }
           }

       }
       return max;
   }

   static void rotation(int k,String s) {
       for (int i = 0; i < k; i++) System.out.println(subs(s.substring(i, k) +s.substring(0, i)));
   }


    public static void main(String args[]) {
        Scanner in = new Scanner(System.in);
        int k = in.nextInt();
        String s = in.next();
        rotation(k,s);
    }
}
Bobas_Pett
  • 591
  • 5
  • 10
1

In addition to this you can avoid calling your palindrome method where the substring size is less than 2. The substring function which you are use gives all the possible substrings include a single character substring. For example if you consider string "abba". For this string your function will give you below 10 substrings:

a,ab,abb,abba,b,bb,bba,b,ba,a

Instead of calling palindrome function for all these 10 substring, you should call palindrome function only for those substrings whose lenght >= 2 characters. This way you will avoid calling palindrome function 4 times. Imagine if you have a string of 10^5 characters. Then you will reduce 10^5 calls to palindrome function. I hope this is useful for you.