-1

This is a classic algorithm problem.

The DP solution is indeed n^3.

I'm using recursion with memoization below.

I need some detailed explanation of what is the runtime of the code below? I'm not satisfied with the current answer. Can someone help?

 public static int countParenthesization(String expr, int begin, int end, boolean result, Map<String, Integer> lookup) {
    String lookupKey = begin + "-" + end + "-" + result;

    if (end - begin == 0) {
        String currenExpr = expr.charAt(begin) + "";
        int count = (currenExpr.equals("T") && result) || (currenExpr.equals("F") && !result) ? 1 : 0;
        lookup.put(lookupKey, count);
        return count;
    }

    if (lookup.containsKey(lookupKey)) {
        return lookup.get(lookupKey);
    }


    int count = 0;
    for (int i = begin + 1; i <= end; i = i + 2) {
        int leftBegin = begin;
        int leftEnd = i - 1;
        int rightBegin = i + 1;
        int rightEnd = end;

        switch (expr.charAt(i)) {
            case '|':
                if (result) {
                    count += countParenthesization(expr, leftBegin, leftEnd, true, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, true, lookup);
                    count += countParenthesization(expr, leftBegin, leftEnd, true, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, false, lookup);
                    count += countParenthesization(expr, leftBegin, leftEnd, false, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, true, lookup);
                } else {
                    count += countParenthesization(expr, leftBegin, leftEnd, false, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, false, lookup);
                }

                break;
            case '&':
                if (result) {
                    count += countParenthesization(expr, leftBegin, leftEnd, true, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, true, lookup);
                } else {
                    count += countParenthesization(expr, leftBegin, leftEnd, true, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, false, lookup);
                    count += countParenthesization(expr, leftBegin, leftEnd, false, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, true, lookup);
                    count += countParenthesization(expr, leftBegin, leftEnd, false, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, false, lookup);
                }

                break;
            case '^':
                if (result) {
                    count += countParenthesization(expr, leftBegin, leftEnd, true, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, false, lookup);
                    count += countParenthesization(expr, leftBegin, leftEnd, false, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, true, lookup);
                } else {
                    count += countParenthesization(expr, leftBegin, leftEnd, true, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, true, lookup);
                    count += countParenthesization(expr, leftBegin, leftEnd, false, lookup)
                            * countParenthesization(expr, rightBegin, rightEnd, false, lookup);
                }

                break;
        }
    }

    lookup.put(lookupKey, count);

    //System.out.println(lookup);

    return count;
}
itzmebibin
  • 9,199
  • 8
  • 48
  • 62

1 Answers1

1

As written, your code is O(n^4). The code is essentially the same as the DP solution, but whereas the DP solution is careful to use an O(1) index into the table (a pair of (i, j) of ints), this code uses a substring, the construction of which takes O(n) time, and the lookup in the hashtable also takes O(n) time. [Note: n here refers to the length of the currently sliced expression string, and not the size of the hashtable].

You can remedy the increased complexity by using the start/end indexes and avoiding string slicing (and hash table lookups), like in the DP solution.

Concretely:

  • the function will take extra arguments int i, int j of start/end indices rather than a pre-sliced expression.
  • your lookup hashtable's keys will change to the tuple <int i, int j, bool result> rather than a string.
  • your code will perform no string slicing at all.
Paul Hankin
  • 54,811
  • 11
  • 92
  • 118
  • Hi @PaulHankin, thanks for your answer. You said the runtime is O(n^4). Can you elaborate how it is O(n^4)? Imagine, you were not aware of a DP solution whose runtime is n^3. In other words, when you have three loops it's kinda easy to come up with n^3. But when you have a loop with 3 recursive calls with memoization, what's the best way to analyze? – Richard Abraham May 08 '16 at 07:04
  • Given an expression, assume sub-results are already computed and hashed, and then analyze the complexity of the code -- (it's O(n^2), or O(n) after the optimisations I suggest in the answer). Then note that each substring of the expression (in the worst case) is ultimately computed and there's O(n^2) substrings, so the overall complexity is O(n^4). Essentially you have to argue like in a mathematical proof, rather than do simple loop-based complexity analysis. Of course, this comment is informal and suggestive rather than careful and mathematically precise. – Paul Hankin May 08 '16 at 08:02
  • The argument is essentially constructing an equivalence between the top-down recursive memoizing solution, and the bottom-up DP solution. The argument works best if you ignore `|` and `&` and only consider `^` -- because the top-down solution can shortcut some work in the `|` and `&` cases. But the worst-case behavior remains the same. – Paul Hankin May 08 '16 at 08:07