1

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

chiwangc
  • 3,566
  • 16
  • 26
  • 32
committedandroider
  • 8,711
  • 14
  • 71
  • 126

2 Answers2

3

We can generate from b(n) to b(n + 1) by using this recursive formula:

  • (b(n - x))b(x) with 0 <= x <= n

So, you can have all of your combinations by iterating through all x.

Code:

public static ArrayList<String> cal(int num){

    if(num == 0){
        ArrayList<String> list = new ArrayList();
        list.add("");
        return list;
    }else{
        ArrayList<String>result = new ArrayList();
        for(int i = 0; i <= num - 1; i++){
            ArrayList<String> a = cal(i);
            ArrayList<String> b = cal(num - 1 - i);
            for(String x : a){
                for(String y : b){
                    result.add("(" + x + ")" + y);
                }
            }
        }
        return result;
    }
}

Input: 3 Output: ()()(), ()(()), (())(), (()()), ((()))

Input: 4 Output: ()()()(), ()()(()), ()(())(), ()(()()), ()((())), (())()(), (())(()), (()())(), ((()))(), (()()()), (()(())), ((())()), ((()())), (((())))

Pham Trung
  • 11,204
  • 2
  • 24
  • 43
  • What does x stand for? – committedandroider Feb 27 '15 at 04:28
  • @committedandroider for example, if we have `n = 5`, `x` can be from 0 to 4, so we can split `n` into two parts `x` and `n - x`, look at the code for more info, in the code `x` is `i` of the for loop – Pham Trung Feb 27 '15 at 04:52
  • I ran this code for test cases of 1, 2, 3 and it worked, but I still don't understand how you got the intuition for this. Say n = 2, then x can be from 0 to 1. Then you have combinations of (x, n-x) of (0,2) and (1,1). What is the significance of (0,2), and (1,1)? – committedandroider Feb 27 '15 at 17:39
  • @committedandroider my intuition is, if we can break b(n - 1) into two parts, and then we can use the last parentheses to surround the first part. First part can be empty to (n - 1) parentheses, thus, cover all cases. – Pham Trung Feb 27 '15 at 18:28
  • Why break b(n-1), not b(n) into two parts? – committedandroider Feb 27 '15 at 18:47
  • @committedandroider that's work too, but you need to handle case n parentheses, `example, for n = 3, there will be this case ((())), which need to be added in separately` , and I personally don't like that as I don't think it is elegant. Just taste, nothing else :) – Pham Trung Feb 27 '15 at 19:14
2

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:

  • Case 1: s cannot be partitioned into two valid patterns of smaller size
  • Case 2: s can be partitioned into two valid patterns of smaller size

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;
    }

}
Community
  • 1
  • 1
chiwangc
  • 3,566
  • 16
  • 26
  • 32
  • With your algorithm how would you factor out the redundancy? – committedandroider Feb 27 '15 at 04:27
  • One approach is to have an internal hashset that stores the string pattern you have generated as far, whenever you generate a new pattern string you have to check whether such pattern string is already present in the hashset, discard it if necessary – chiwangc Feb 27 '15 at 04:32
  • Would a good approach be to call your overloaded method from mine in the case that n>1? Cause the client shouldn't have to pass these in. – committedandroider Feb 27 '15 at 04:34
  • Indeed, it is a good idea to have an overloaded method `recursive(n)` that calls `recursive(n, "", "")` – chiwangc Feb 27 '15 at 04:36
  • I updated my approach, to follow your algorithm. It does eliminate the duplicates but what you do about the testing if a comma should be printed out or not? Like the first call p(1) should not print out any commas but how would you test for that? – committedandroider Feb 27 '15 at 17:20
  • I have updated my solution for your reference. One simple approach is to keep a flag of whether this is the first string pattern you print. You can then use this flag to determine whether to output a comma or not – chiwangc Feb 28 '15 at 00:43
  • 1
    Does not print (())(()) for n = 4. – Khanna111 Mar 06 '15 at 08:56
  • Thank you very much for pointing out my mistake @Khanna111, I have corrected my answer accordingly – chiwangc Mar 06 '15 at 14:03
  • @committedandroider My previous answer is incomplete, please see my updated answer. Sorry for any confusion or inconvenience caused. – chiwangc Mar 06 '15 at 14:04