javaalgorithmrecursiondynamic-programmingbottom-up

How to build the case for n=3 using bottom up recursion?


I am working on a problem from Cracking the Coding Interview, problem 9.6 page 110.

Here is the problem:
Implement an algorithm to print all valid (e.g., properly opened and closed combinations of n-pairs of parentheses. Examples
b(1) - "()"
b(2) - "(()), ()()"
b(3) - "((())), (()()), (())(), ()(()), ()()()"
I am trying to use the bottom up recursion approach that the author discusses on page 107 - "We start with knowing how to solve the problem for a simple case, like a list with only one element, and figure out how to solve the problem for two elements, then for three elements, and so on. The key here is to think about how you can build the solution for one case off the previous case"

Here is the code I have so far

static void print(int n) {
    print(n, new HashSet<String>(), "", "");
}

static void print(int n, Set<String> combs, String start, String  end) {
    if(n == 0) {
        if(!combs.contains(start + end)) {
            System.out.print(start + end);
            combs.add(start + end);
        }
    } else {
        print(n-1, combs, "(" + start, end +")");
        System.out.print(", ");
        print(n-1, combs, start, end + "()");
        System.out.print(", ");
        print(n-1, combs, "()" + start, end);
    }
}

To get this code, I worked from the first case to the second case. I saw that
b(2) = "(b(1)), b(1),b(1)" This code does work for the first two cases. I am really struggling with the third case though. Can someone give me a hint(not the whole answer, could turn to the back of the book for that), about how to go from case 2 to case 3, or in other words using case 2 to get to case 3? Like how would you go from (()), ()() to
((())), (()()), (())(), ()(()), ()()()? Would you abandon the pattern you saw from b(1) to b(2) because it doesn't work for b(2) to b(3)?


Solution

  • Thanks Khanna111 for pointing out the mistake I made in my original answer, which was incomplete and under-counted the string patterns. As a result, I have updated my answer accordingly.

    Please consider giving credit to Pham Trung for his answer with the correct recursive formula. My answer is essentially the same as his, with only a slight difference in the way I formulate the construction of patterns from smaller sub-problems (as I find it easier to explain the details in my approach).

    ========================================================================

    Update Solution

    For any valid pattern s of size n, s falls in exactly one of the following cases:

    For case 1, s must be of the form (_____), where _____ is a valid pattern of size n - 1. So in this case, for every valid pattern t of size n - 1, we simply construct a pattern s by concatenating t with ( and ) as prefix and suffix, respectively (i.e. s = (t)).

    For case 2, we can partition s into uv, where u and v are both valid patterns of smaller size. In this case, we have to consider all possible patterns of u and v, where u can be any valid pattern of size i = 1, 2, ..., n - 1, while v can be any valid pattern of size n - i.

    When n = 0, clearly only the empty string is a valid pattern, so we have dp(0) = { "" } as our base case. A complete implementation with caching to improve the performance is given below:

    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Map;
    import java.util.Set;
    
    public class BalancingBrackets {
    
        private static Map<Integer, Set<String>> dp = new HashMap<>();
    
        public static void main(String[] args) {
            Set<String> result = compute(4);
    
            boolean isFirst = true;
            for (String s : result) {
                if (isFirst) {
                    isFirst = false;
                    System.out.print(s);
                } else {
                    System.out.print(", " + s);
                }
            }
        }
    
        private static Set<String> compute(Integer n) {
            // Return the cached result if available
            if (dp.containsKey(n)) {
                return dp.get(n);
            }
    
            Set<String> set = new HashSet<>();
    
            if (n == 0) {
                // This is the base case with n = 0, which consists only of the
                // empty string
                set.add("");
            } else if (n > 0) {
                // For generating patterns in case 1
                for (String s : compute(n - 1)) {
                    set.add("(" + s + ")");
                }
    
                // For generating patterns in case 2
                for (int i = 1; i < n; i++) {
                    Set<String> leftPatterns = compute(i);
                    Set<String> rightPatterns = compute(n - i);
    
                    for (String l : leftPatterns) {
                        for (String r : rightPatterns) {
                            set.add(l + r);
                        }
                    }
                }
            } else {
                // Input cannot be negative
                throw new IllegalArgumentException("Input cannot be negative.");
            }
    
            // Cache the solution to save time for computing large size problems
            dp.put(n, set);
    
            return set;
        }
    
    }