Dynamic Programming · 54 of 60
Boolean Parenthesization Problem
Count the ways to put brackets on a chain of true and false cards so the whole thing comes out true.
- Simple way
- time O(3^n)
room O(n) - Clever way
- time O(n^3)
room O(n^2)
The problem, simply
You get a string like T|T&F^T. The letters are T (true) and F (false). Between every two letters is an operator:
&(and) is true only when both sides are true.|(or) is true when at least one side is true.^(xor) is true when the two sides are different.
You may put brackets anywhere, which changes the order the operators are done in. Count how many different bracketings make the whole thing true. The count grows fast, so the judge usually wants it modulo 1003.
A story to remember it
Picture it
All five ways to put brackets on T|T&F^T, drawn as trees. The operator at the top is done last. Four of them come out true.
- answer
- rejected
Examples
A normal case
- In
S = 'T|T&F^T'- Out
4
There are 5 bracketings. Only (T|(T&F))^T is false, so 4 are true.
Every bracketing works
- In
S = 'T|F&T'- Out
2
(T|F)&T is true and T|(F&T) is T|F, also true. Both of the 2 bracketings count.
Xor at the front
- In
S = 'T^F|F'- Out
2
(T^F)|F is T|F, true. T^(F|F) is T^F, true.
One letter
- In
S = 'F'- Out
0
No operators, so there is one way to read it, and it is false. A single T would give 1.
Never true
- In
S = 'F&F'- Out
0
Both sides are false, so & can never be true.
Way 1: the simple way
Pick the operator that is done last. Everything to its left becomes one value and everything to its right becomes another. Ask the same question about each side, for both true and false, and combine.
- A single letter:
Tis true in 1 way,Fis false in 1 way. - For a longer piece, try every operator as the last cut.
- Get
(leftTrue, leftFalse)and(rightTrue, rightFalse)by asking the smaller pieces. - Add up the ways the operator gives true. All other pairs give false.
We must count false ways too: ^ is true when one side is false, and | is true even if one side is false.
Each operator is a place to cut. Cutting gives a left piece and a right piece, and each piece is solved by cutting again. F^T is solved under T&F^T and then again under the & cut. T|T&F will solve T&F again too.
- seen
class Solution {
// returns {ways to be true, ways to be false} for s[i..j]
// i and j are positions of letters, operators sit at odd positions
int[] count(String s, int i, int j) {
if (i == j) return s.charAt(i) == 'T' ? new int[]{1, 0} : new int[]{0, 1};
int t = 0, f = 0;
for (int k = i + 1; k < j; k += 2) { // k is an operator
int[] L = count(s, i, k - 1);
int[] R = count(s, k + 1, j);
int all = (L[0] + L[1]) * (R[0] + R[1]);
char op = s.charAt(k);
int tr;
if (op == '&') tr = L[0] * R[0];
else if (op == '|') tr = L[0] * R[0] + L[0] * R[1] + L[1] * R[0];
else tr = L[0] * R[1] + L[1] * R[0]; // '^'
t += tr;
f += all - tr;
}
return new int[]{t, f};
}
public int countWays(int n, String s) {
return count(s, 0, n - 1)[0];
}
}How long it takes: a piece of m letters calls every shorter piece on each side, and the work about triples with each extra letter, O(3^n) for n letters. Extra room: the chain of waiting calls, O(n).
Way 2: the clever way
There are only about n * n / 2 different pieces (a start letter and an end letter). Keep two tables, T[i][j] and F[i][j]: the ways letters i..j come out true and false. Fill short pieces first, because a long piece only uses shorter ones.
- Fill the diagonal: one letter each.
- For length 2, 3, ... up to all letters, for every start
i(endj = i + len - 1): - Try every cut
kbetweeniandj. CombineT[i][k], F[i][k]withT[k+1][j], F[k+1][j]using the operator after letterk. - The answer is
T[0][n-1].
Fill the true and false tables, short pieces first
01 / 12
Box (i, j) counts the ways the piece from letter i to letter j can come out true (top) or false (bottom). A single T is true in 1 way. A single F is false in 1 way.
- looking now
- checking
- answer
- just added
class Solution {
static final int MOD = 1003; // the judge asks for the answer modulo 1003
public int countWays(int n, String s) {
int m = (n + 1) / 2; // number of T and F letters
int[][] T = new int[m][m]; // T[i][j]: ways letters i..j are true
int[][] F = new int[m][m]; // F[i][j]: ways letters i..j are false
for (int i = 0; i < m; i++) {
if (s.charAt(2 * i) == 'T') T[i][i] = 1; else F[i][i] = 1;
}
for (int len = 2; len <= m; len++) {
for (int i = 0; i + len - 1 < m; i++) {
int j = i + len - 1;
for (int k = i; k < j; k++) { // cut after letter k
char op = s.charAt(2 * k + 1);
int lt = T[i][k], lf = F[i][k], rt = T[k + 1][j], rf = F[k + 1][j];
int all = (lt + lf) * (rt + rf) % MOD;
int tr;
if (op == '&') tr = lt * rt % MOD;
else if (op == '|') tr = (lt * rt + lt * rf + lf * rt) % MOD;
else tr = (lt * rf + lf * rt) % MOD;
T[i][j] = (T[i][j] + tr) % MOD;
F[i][j] = (F[i][j] + all - tr + MOD) % MOD;
}
}
}
return T[0][m - 1];
}
}How long it takes: about n^2 / 2 boxes, and each tries up to n cuts, O(n^3). Extra room: two tables, O(n^2).
Watch out for
- Letters and operators share one string. Letter
iis at2 * i, and the operator after it is at2 * i + 1. - With a modulo,
all - trcan be negative. AddMODbefore taking%again. - Fill by length, not row by row from the top.
T[i][j]needsT[k + 1][j], which sits lower in the table.
What interviewers ask next
- Count the ways to get false instead. Return
F[0][n-1]. The table already has it. - What if every operator were the same? The number of bracketings of
nletters is the Catalan numberC(n-1), so the answer is a part of that. - "Different ways to add parentheses" with numbers and
+ - *asks for every result value. Same cut-at-the-last-operator idea, but each piece keeps a list of values instead of two counts. - How does this relate to matrix chain multiplication? Same shape: try every last split of a range, short ranges first. That family is called interval DP.
Remember this
Grown-up words: a piece of the expression is an interval, the tables T and F are the DP states, filling by length is interval DP, the last operator is the split point, and drawing an expression as a tree gives an expression tree.