SRM 731, D1, 250-Pointer (TreesAndBrackets)

James S. Plank

Sun Nov 14 14:50:42 EST 2021

In case Topcoder's servers are down

Here is a summary of the problem:

The examples

Don't spend too much time on these just yet. I convert them into pictures below.

Example t1 t2 Answer
0 "(())" "()" "Possible"
1 "()" "()" "Possible"
2 "(()()())" "((()))" "Impossible"
3 "((())((())())())" "(()(())())" "Possible"
4 "((())((())())())" "((()()()()()))" "Impossible"
5 (I added this one) "(()((()()))(()(()))((()()))()(()(()())(()))())" "((())((()()))(()))" "Possible"


Testing yourself

Like the Cryptography Problem, I have a shell script tests.sh, that you can use to test your program. When you run tests.sh, your answer should be identical to answers.txt.

Hints

This is a great practice problem for CS202 in getting experience with trees and recursion.

Let's start off by looking at the examples. When it's "Possible", I show the nodes that you delete in blue. Make sure you see how examples 3 and 5 return "Possible". I've labeled some subtrees in those examples to help with the explanation.

Example 0:
Possible
t1

t2

Example 1:
Possible
t1

t2

Example 2:
Impossible
t1

t2

Example 3:
Possible
t1

t2

Example 4:
Impossible
t1

t2

Example 5:
Possible
t1

t2


Observations

Let's make some observations that help us with a solution:

Observation #1: While the problem description talks ./a.out deleting leaf nodes, in reality, you are deleting subtrees of T1 to match T2. Look again at example 5 -- there you should be able to see that repeatedly deleting leaf nodes is equivalent to deleting subtrees. I've labeled the subtrees in the first level of example 5, so that it is easier to talk ./a.out that example.

Observation #2: When you are matching t1 and t2, you cannot delete the root nodes. This is because you delete subtrees of t1. If you delete the root of t1, then you will have deleted the entire tree.

Observation #3: If t1 is a single node, and t2 is more than a single node, then t1 will not match t2. That is the case if you try to match a1 and b1 in example 5 above.

Observation #4: If t2 is a single node, then t1 will match t2. That's because you can delete all of t1 except for the root, and then it matches t2. That's what happens in example 0, and with subtrees c1 and d1 in example 3.

Observation #5: Let us suppose now that t1 and t2 are not single nodes. Each of them is the root of an ordered collection of subtrees. Let's call t1's subtrees x1, x2, ... and t2's subtrees y1, y2, ... If x1 matches y1, then you can delete both subtrees, and solve the problem on the remaining subtrees. You can see this best in example 3:

Observation #6: Let us suppose now that subtreesx1 and y1 don't match. Then, you delete x1, and try again. Let's illustrate this by showing how you do the matching in example 5:

Turning the observations into a solution

The cleanest way to solve this problem is to go ahead and turn the strings into trees. I defined the following class:

class Node {
  public:
    int left_paren, right_paren;
    vector <class Node *> children;
    void Print();
};

And then I wrote a procedure to turn strings into trees. I represent a tree with a single pointer to the tree's root node:

Node *StringToTree(const string &s, int lp);

The parameters are as follows:

The procedure will turn the subtree that starts with the given '(' character into a tree of Nodes. Here are some pictoral examples:

a = StringToTree("()", 0)
b = StringToTree("(())", 1)
c = StringToTree("(())", 0)


Job #1

So, Job #1 for you is to write StringToTree(). It will be recursive. If you want to check yourself, have StringToTree() print its arguments when it is called, and have it print left_paren, right_paren and children.size() of its return value when it returns. You can check it against the following for example. You should note how it makes the exact calls in the table/drawings above for both T1 and T2.
UNIX> ./a.out 0
Calling StringToTree on t1
STT("(())", 0) called.
STT("(())", 1) called.
STT("(())", 1) returning: left_paren=1, right_paren=2, children.size()=0
STT("(())", 0) returning: left_paren=0, right_paren=3, children.size()=1

Calling StringToTree on t2
STT("()", 0) called.
STT("()", 0) returning: left_paren=0, right_paren=1, children.size()=0

UNIX>
UNIX> ./a.out 2
Calling StringToTree on t1
STT("(()()())", 0) called.
STT("(()()())", 1) called.
STT("(()()())", 1) returning: left_paren=1, right_paren=2, children.size()=0
STT("(()()())", 3) called.
STT("(()()())", 3) returning: left_paren=3, right_paren=4, children.size()=0
STT("(()()())", 5) called.
STT("(()()())", 5) returning: left_paren=5, right_paren=6, children.size()=0
STT("(()()())", 0) returning: left_paren=0, right_paren=7, children.size()=3

Calling StringToTree on t2
STT("((()))", 0) called.
STT("((()))", 1) called.
STT("((()))", 2) called.
STT("((()))", 2) returning: left_paren=2, right_paren=3, children.size()=0
STT("((()))", 1) returning: left_paren=1, right_paren=4, children.size()=1
STT("((()))", 0) returning: left_paren=0, right_paren=5, children.size()=1

UNIX> 
To help you out a little, here are examples 0, 2 and 3 with each node labeled by its left_paren value:

Example 0:
Possible
t1

t2

Example 2:
Impossible
t1

t2

Example 3:
Possible
t1

t2

And here is the program on example 2 -- the picture above should help you verify the output:

UNIX> ./a.out 2
Calling StringToTree on t1
STT("(()()())", 0) called.
STT("(()()())", 1) called.
STT("(()()())", 1) returning: left_paren=1, right_paren=2, children.size()=0
STT("(()()())", 3) called.
STT("(()()())", 3) returning: left_paren=3, right_paren=4, children.size()=0
STT("(()()())", 5) called.
STT("(()()())", 5) returning: left_paren=5, right_paren=6, children.size()=0
STT("(()()())", 0) returning: left_paren=0, right_paren=7, children.size()=3

Calling StringToTree on t2
STT("((()))", 0) called.
STT("((()))", 1) called.
STT("((()))", 2) called.
STT("((()))", 2) returning: left_paren=2, right_paren=3, children.size()=0
STT("((()))", 1) returning: left_paren=1, right_paren=4, children.size()=1
STT("((()))", 0) returning: left_paren=0, right_paren=5, children.size()=1

UNIX> 

Job #2

Job #2 is to write the Print() method of a Node(). What my method did was print the node's left_paren and right_paren and the left_parens of its children. Then it called Print() recursively on its children. Therefore, it does a preorder traversal of the tree, printing out information on each node. I changed my main code to print the root of each tree:


string TreesAndBrackets::check(string t1, string t2)
{
  Node *n1, *n2;

  n1 = StringToTree(t1, 0);
  n2 = StringToTree(t2, 0);

  printf("LP RP - Children\n");
  n1->Print();
  printf("\n");

  printf("LP RP - Children\n");
  n2->Print();
  printf("\n");

Here's the output on examples 0 and 2:

UNIX> ./a.out 0
LP RP - Children
 0  3 - 1
 1  2 -

LP RP - Children
 0  1 -

UNIX> ./a.out 2
LP RP - Children
 0  7 - 1 3 5
 1  2 -
 3  4 -
 5  6 -

LP RP - Children
 0  5 - 1
 1  4 - 2
 2  3 -

UNIX> 

Job #3

Job #3 is to solve the problem. You probably don't realize how close you are to solving the problem. Write a procedure Match() with the following prototype:

bool Match(const Node *n1, const Node *n2);

This returns true if the subtree rooted at n1 matches the subtree rooted at n2. Again, it will be recursive. Mine went through the following steps.

Note, you don't ever "delete" anything -- if you match, then you move on, and if you don't match, then you move on. I've annotated my code to print when Match is called, and when it returns. Here are examples 0, 2 and 3. Again, look at the pictures above with the nodes labeled -- that will help you walk through this example:
UNIX> ./a.out 0
Match(0,0) called.
Match(0,0) returns true.
Possible
UNIX> ./a.out 2
Match(0,0) called.
Match(1,1) called.
Match(1,1) returns false.
Match(3,1) called.
Match(3,1) returns false.
Match(5,1) called.
Match(5,1) returns false.
Match(0,0) returns false.
Impossible
UNIX> ./a.out 3
Match(0,0) called.
Match(1,1) called.
Match(1,1) returns true.
Match(5,3) called.
Match(6,4) called.
Match(6,4) returns true.
Match(5,3) returns true.
Match(13,7) called.
Match(13,7) returns true.
Match(0,0) returns true.
Possible
UNIX> 
When you're done, delete the extra print statements, and run tests.sh to see if you match answers.txt.