Longest segment of balanced parentheses -- an exercise in program inversion in a segment problem (Functional Pearl)
FFunctional Pearl
Longest Segment of Balanced Parentheses: an Exercise in Program Inversion in a Segment ProblemShin-Cheng Mu, Tsung-Ju Chiang
Institute of Information Science, Academia Sinica, Taiwan
Abstract
Given a string of parentheses, the task is to find a longest consecutiveegment that is properly bracketed. We find it an interesting problembecause it involves two techniques: the usual approach for solvingsegment problems, and the converse-of-a-function theorem — throughwhich we derived an instance of shift-reduce parsing.
Given a string of parentheses, the task is to find a longest consecutive segmentthat is properly bracketed. For example, for input "))(()())())()(" theoutput should be "(()())()" . We also consider a reduced version of theproblem in which we return only the length of the segment. For a specification, balanced parentheses can be captured by a number ofgrammars that are equivalent, for example S → (cid:101) | ( S ) | SS , or S → (cid:101) | ( S ) S .We choose the latter because it is both concise and unambiguous. Its parsetree can be represented in Haskell as below, with a function pr specifyinghow a tree is printed: data Tree = Null | Fork Tree Tree , pr :: Tree → String pr Null = "" pr ( Fork t u ) = "(" ++ pr t ++ ")" ++ pr u .The problem can thus be specified by ( lbp standing for “longest balancedparentheses”): lbp = maxBy size · filtJust · map parse · segments , segments = concat · map inits · tails , filtJust ts = [ t | Just t ← ts ] , size t = length ( pr t ) .The function segments :: [ a ] → [ [ a ] ] returns all segments of a list, with inits , tails :: [ a ] → [ [ a ] ] respectively compute all prefixes and suffixes of theinput list. The function parse :: String → Maybe Tree builds a parse tree — parse xs should return
Just t such that pr t = xs if xs is balanced, and return Nothing otherwise. It is related to the right inverse of pr , that is, the function pr − such that pr ( pr − xs ) = xs . The function pr − is partial (e.g. there is no The length-only version was possibly used as an interview problem, collected in, for example, https://leetcode.com/problems/longest-valid-parentheses/ . a r X i v : . [ c s . P L ] J a n converse - of - a - function theorem 2 t such that pr t = "(((" ), while parse is the “monadified” variation of pr − ,using a Maybe monad to represent partialty. We will construct parse moreformally in Section .The result of map parse is passed to filtJust :: [ Maybe a ] → [ a ] , whichchooses only those elements wrapped by Just . For this problem filtJust always returns a non-empty list, because the empty string can always beparsed to
Just Null . Given f :: a → b where b is a type that is ordered, maxBy f :: [ a ] → a picks a maximum element from the input.The length-only problem can be specified by lbpl = size · lbp . an initial derivation . To derive an algorithm, we proceed by theusual routine. Finding an optimal segment is often factored into finding, foreach suffix, an optimal prefix: maxBy size · filtJust · map parse · segments = { definition of segments } maxBy size · filtJust · map parse · concat · map inits · tails = { since map f · concat = concat · map ( map f ) , map fusion } maxBy size · filtJust · concat · map ( map parse · inits ) · tails = { since filtJust · concat = concat · map filtJust } maxBy size · concat · map ( filtJust · map parse · inits ) · tails = { since maxBy f · concat = maxBy f · map ( maxBy f ) } maxBy size · map ( maxBy size · filtJust · map parse · inits ) · tails .That is, for each suffix returned by tails , we attempt to compute the longest prefix of balanced parentheses (as in maxBy size · filtJust · map parse · inits ).The next step is usually to apply the “scan lemma”: Lemma map ( foldr ( ⊕ ) e ) · tails = scanr ( ⊕ ) e, wherescanr ( ⊕ ) e [ ] = [ e ] scanr ( ⊕ ) e ( x : xs ) = let ( y : ys ) = scanr ( ⊕ ) e xs in ( x ⊕ y ) : y : ys .If we can turn maxBy size · filtJust · map parse · inits into a foldr , where ( ⊕ ) is aconstant-time operation, we get a linear-time algorithm. Since inits is a foldr — inits = foldr ( λ x xss → [ ] : map ( x : ) xss ) [ [ ] ] , a reasonable attempt is to usethe fold-fusion theorem to fuse maxBy size · filtJust · map parse into inits , toform a single foldr . Trying to fuse map parse into inits , it will soon turn outthat we will need parse · ( x : ) = g x · parse for some g , that is, parse shall be a foldr too. Is that possible?Since parse is defined in terms of pr − , it would be helpful if there is amethod to construct the inverse of a function as a fold — as presented in thenext section. - of - a - function theorem Given a function f :: b → t , the converse-of-a-function theorem [Bird andde Moor, , de Moor and Gibbons, ] constructs the relational converse— a generalised notion of inverse — of f . The converse is given as a relationalfold whose input type is t , which can be any inductively-defined datatypewith a polynomial base functor. We specialize the general theorem for ourneeds: we use it to construct only functions, not relations, and only when t is a list. filtJust is called catMaybe in the standard library. the spine tree 3 t u v (cid:37821)(cid:37821) t ) u ) v ( ( ) ( ( ) (a) After having read t)u)v . t u v (cid:37821)(cid:37821) t ) u ) v ( ( ) ( ( ) (b) After reading two more symbols. Figure : The tree constructed while reading "(()(()t)u)v" . Fork is represented bya round node, and
Null the ground symbol.
Theorem Given f :: b → [ a ] , if we have base :: b and step :: a → b → b satisfying:f base = [ ] ∧ f ( step x t ) = x : f t ,then f − = foldr step base xs is a partial right inverse of f . That is, we havef ( f − xs ) = xs for all xs in the domain of f − . While the general version of the theorem is not trivial to prove, the versionabove specialized to functions and lists can be verified by an easy inductionon the input list.To find the right inverse of pr using Theorem , we have to find step :: Char → Tree → Tree such that pr ( step x t ) = x : pr t , where x is either ’(’ or ’)’ . One can see that there is no way this equality could hold: pr always returns strings containing balanced parentheses, but for all u suchthat pr u = x : pr t , it is not possible that both pr u and pr t are balanced.This is a hint that we should instead consider a generalisation of pr whoseinput are not necessarily fully built trees (that print to balanced parentheses).For pr ( step x t ) = x : pr t to hold, t should represent some partially builttrees that can still be extended from the left, while step x t extends t such thatits printout is preceded by an additional x . Our aim now is to construct a data structure that represent partially builttrees that can be extended from left. Figure a shows a tree constructedfrom "(()(()t)u)v" . The input is processed from right to left, and when wehave only read "t)u)v" , we should have construct the three trees t , u , and v under the dotted line. If we read two more symbols "()" , we should haveconstructed the three trees under the dotted line of Figure b, where u and v stay the same, while t is extended to Fork Null t , which prints to "()t" .One may infer that we should maintain a list of trees while reading an in-put halfway. In Figure a the list is [ t , u , v ] , and in Figure b [ Fork Null t , u , v ] .This spine representation — so called because the list contains subtrees alonethe left spine of the final tree — was also used by, for example Mu and Bird[ ], to efficiently build trees in a foldr . Let Spine be a non-empty list of
Tree s: type Spine = [
Tree ] .The following function rolls a spine back to an ordinary tree: the spine tree 4 roll :: Spine → Tree roll [ t ] = troll ( t : u : ts ) = roll ( Fork t u : ts ) For example, roll [ t , u , v , w ] = Fork ( Fork ( Fork t u ) v ) w .How do we print a spine? Inspired by Figure , [ t , u , v ] :: Spine should beprinted as "t)u)v" , where t and u , etc. in typewriter font denote pr t and pr u . More generally, the following function prS prints a Spine : prS :: Spine → String prS [ t ] = pr tprS ( t : ts ) = pr t ++ ")" ++ prS ts .To relate it to roll , for all ts we have pr ( roll ts ) = replicate ( length ts − ) ’(’ ++ prS ts . ( )where replicate n x returns a list containing n copies of x . Proof of ( ) is aroutine induction on ts .Before using Theorem , we construct an inductive definition of prS thatdoes not use (++) and does not rely on pr . For a base case, prS [ Null ] = "" . Itis also immediate that prS ( Null : ts ) = ’)’ : prS ts . When the spine containsmore than one tree and the first tree is not Null , we calculate: prS ( Fork t u : ts )= { definitions of pr and prS } "(" ++ pr t ++ ")" ++ pr u ++ ")" ++ prS ts = { definition of prS } ’(’ : prS ( t : u : ts ) .We have thus derived the following definition of prS : prS [ Null ] = "" prS ( Null : ts ) = ’)’ : prS tsprS ( Fork t u : ts ) = ’(’ : prS ( t : u : ts ) .We are now ready to invert prS by Theorem , which amounts to finding base and step such that prS base = "" and prS ( step x ts ) = x : prS ts for x = ’(’ or ’)’ . Now that prS has been transformed into the form above, we pick base = [ Null ] , and step is given by: step ’)’ ts = Null : tsstep ’(’ ( t : u : ts ) = Fork t u : ts .We have thus constructed prS − = foldr step [ Null ] , that is, prS − "" = [ Null ] prS − ( ’)’ : xs ) = Null : prS − xsprS − ( ’(’ : xs ) = case prS − xs of ( t : u : ts ) → Fork t u : ts ,which is pleasingly symmetrical to prS .For an operational explanation, a right parenthesis ’)’ indicates startinga new tree, thus we start freshly with a Null ; a left parenthesis ’(’ oughtto be the leftmost symbol of some "(t)u" , thus we wrap the two mostrecent siblings into one tree. When there are no such two siblings (that is, prS − xs = [ t ] ), the construction fails — prS − is a partial function. optimal prefix in a fold 5 Some readers might have noticed the similarity to shift-reduce parsing,in which, after reading a symbol we either "shift" the symbol by pushing itonto a stack, or "reduce" the symbol against a top segment of the stack. Here,the spine tree is the stack. This is a special case where the decision to shift orreduce can be made by looking ahead to a single symbol.We could proceed to work with prS − for the rest of this pearl but, forclarity, we prefer to observe partiality explicitly. Let parseS be the monadifiedversion of prS − , given by: parseS :: String → Maybe Spine parseS "" = Just [ Null ] parseS ( x : xs ) = parseS xs >> = stepM x , where stepM ’)’ ts = Just ( Null : ts ) stepM ’(’ [ t ] = Nothing stepM ’(’ ( t : u : ts ) = Just ( Fork t u : ts ) ,where stepM is monadified step — for the case [ t ] missing in step we return Nothing .To relate parseS to parse , notice that prS [ t ] = pr t . We therefore have parse = unwrapM < = < parseS , where ( < = < ) :: ( b → M c ) → ( a → M b ) → ( a → M c ) is (reversed) Kleisli composition, and unwrapM [ t ] = Just t , otherwise unwrapM returns Nothing . Recall our objective: to turn maxBy size · filtJust · map parse · inits into a foldr .We calculate: maxBy size · filtJust · map parse · inits = { since parse = unwrapM < = < parseS } maxBy size · filtJust · map ( unwrapM < = < parseS ) · inits = { see below } unwrap · maxBy ( size · unwrap ) · filtJust · map parseS · inits .The last step is a routine calculation whose purpose is to factor the post-processing unwarpM out of the main computation. We introduce unwrap :: Spine → Tree , defined by unwrap [ t ] = t and for all other input it returns Null , the smallest tree.To compute the optimal spine tree in a foldr , we need two more general-isations. Firstly, recall the definition of parseS . In the ( ’(’ : xs ) case, whenthe recursive call returns [ t ] , we abort the computation by returning Nothing .This means that the information computed so far is disposed of, while ifwe wish to process all prefixes in a single foldr , it helps to maintain someaccumulated results. The following function build returns [ Null ] in this case,allowing the computation to carry on: build :: String → Spine build = foldr bstep [ Null ] , where bstep ’)’ ts = Null : tsbstep ’(’ [ t ] = [ Null ] bstep ’(’ ( t : u : ts ) = Fork t u : ts .For example, while parseS "))(" = Nothing , we have build "))(" = [
Null , Null , Null ] — the same result build and parseS would return for "))" . In effect, while optimal prefix in a fold 6 inits parseS build "" J [ N ] [ N ] "(" Nothing [ N ] "()" J [ F N N ] [
F N N ] "())" J [ F N N , N ] [ F N N , N ] "())(" Nothing [ F N N , N ] "())()" J [ F N N , F N N ] [
F N N , F N N ] "())()(" Nothing [ F N N , F N N ] Figure : Results of parseS and build for each prefix of "())()(" . parseS is a partial function that attempts to parse an entire string, build is atotal function that parses a prefix of the string.We claim that the optimal prefix can be computed by build : maxBy ( size · unwrap ) · filtJust · map parseS · inits = maxBy ( size · unwrap ) · map build · inits . ( )An informal explanation is that using build instead of parseS does not generateanything new. Figure shows the results of parseS and build for each prefixof "())()(" , where Just , Null , and
Fork are respectively abbreviated to J , N ,and F . We can see that there are three prefixes for which parseS returns Nothing , while build yields a spine. All of these spines, however, are what parseS would return for some other prefix anyway.Formally proving ( ), however, is a tricky task. It turns out that weneed to prove a non-trivial generalisation of ( ), recorded in Appendix A forinterested readers.For the second generalisation, note that in maxBy ( size · unwrap ) , twosingleton spines [ t ] and [ u ] are compared by the sizes of t and u , while t : ts is treated the same as Null . We generalise the process to picking a maximumusing the ordering ( (cid:69) ) , defined below: [ ] (cid:69) us ∧ ( t : ts ) (cid:69) ( u : us ) ≡ size t (cid:54) size u ∧ ts (cid:69) us .That is, ts (cid:69) us if ts is no longer than us , and for every tree t in ts , wehave size t (cid:54) size u where u is the tree in us in corresponding position. The“smallest” spine under ( (cid:69) ) is [ Null ] . In our context where we choose anoptimal spine built from prefixes of the same list, it is safe using ( (cid:69) ) becauseif a spine t : ts is the largest under ( (cid:69) ) , the spine [ t ] must be in the set ofspines too and is optimal under the original order.Furthermore, while ( (cid:69) ) is not a total ordering, bstep is monotonic withrespect to ( (cid:69) ) : for all vs , ws :: Spine and x = ’(’ or ’)’ , we have vs (cid:69) ws ⇒ bstep x vs (cid:69) bstep x ws . That means the list of spines returned by map build · inits is sorted in ascending order, with the largest spine in the end: build [ ] (cid:69) build [ x ] (cid:69) build [ x , x ] (cid:69) ... (cid:69) build [ x ... x n ] .In summary, we have unwrap · maxBy ( size · unwrap ) · filtJust · map parseS · inits = { ( ) } unwrap · maxBy ( size · unwrap ) · map build · inits = { let max (cid:69) denote choosing maximum by ( (cid:69) ) } wrapping up and conclusions 7 head · max (cid:69) · map build · inits = { discussion above } head · last · map build · inits = { free theorem of last and last · inits = id } head · build . We can finally resume the main derivation in Section : maxBy size · map ( maxBy size · filtJust · map parse · inits ) · tails = { Section } maxBy size · map ( head · build ) · tails = head · maxBy ( size · head ) · map build · tails = { Lemma , build = foldr bstep ( Null , [ ]) } head · maxBy ( size · head ) · scanr bstep [ Null ] .We have therefore derived: lbp :: String → Tree lbp = head · maxBy ( size · head ) · scanr bstep [ Null ] .To avoid recomputing the sizes each time, we can annotate each tree byits size: letting Spine = [ (
Tree , Int ) ] , resulting in an algorithm that runs inlinear-time: lbp = fst · head · maxBy ( snd · head ) · scanr bstep [ ( Null , 0 ) ] , where bstep ’)’ ts = ( Null , 0 ) : tsbstep ’(’ [ t ] = [ ( Null , 0 ) ] bstep ’(’ (( t , m ) : ( u , n ) : ts ) = ( Fork t u , 2 + m + n ) : ts .Finally, the size-only version can be obtained by fusing size into lbp . Itturns out that we do not need to keep the actual tree, but only their sizes — Spine = [
Int ] : lbpl :: String → Int lbpl = head · maxBy head · scanr step [ ] , where step ’)’ ts = tsstep ’(’ [ t ] = [ ] step ’(’ ( m : n : ts ) = + m + n : ts .So we have derived a solution to the problem. We find it an interestingjourney because it involves two techniques: the usual approach for solvingsegment problems, and the converse-of-a-function theorem — through whichwe derived an instance of shift-reduce parsing. We hope the reader enjoyedthis journey too. acknowledgements The problem was suggested by Yi-Chia Chen.The authors would like to thank our colleagues in IIS, Academia Sinica, inparticular Hsiang-Shang ‘Josh’ Ko, Liang-Ting Chen, and Ting-Yan Lai, forvaluable discussions. Also thanks to Chung-Chieh Shan and Kim-Ee Yeohfor their advices on earlier drafts of this paper.
EFERENCES references R. S. Bird and O. de Moor.
Algebra of Programming . Prentice Hall, . ISBN - - -X.O. de Moor and J. Gibbons. Pointwise relational programming. In T. Rus,editor, Algebraic Methodology and Software Technology , number in LNCS,pages – . Springer-Verlag, .S.-C. Mu and R. S. Bird. Theory and applications of inverting functionsas folds. Science of Computer Programming (Special Issue for Mathematics ofProgram Construction) , : – , . a on introducing build Regarding proving ( ). We notice two properties: . parseS xs is either Nothing , or
Just ( build xs ) , . if parseS xs is Nothing , build xs = build xs (cid:48) for some proper prefix xs (cid:48) of xs .Let ( ⊕ ) :: Spine → Spine → Spine be any binary operator that is associa-tive, commutative, and idempotent, with identity [ Null ] , and let choose = foldr ( ⊕ ) [ Null ] . The two properties above imply that for all ys and x : ( choose · map build · inits ) ys ⊕ pickJust ( parseS ( ys ++ [ x ])) =( choose · map build · inits ) ys ⊕ build ( ys ++ [ x ]) . ( )where pickJust :: Maybe Spine → Spine extracts the spine if the input iswrapped by
Just , otherwise returns [ Null ] .Let bsteps [ y , y . . y n ] = bstep y · bstep y ... bstep y n , and stepsM [ y , y . . y n ] = stepM y < = < stepM y ... < = < stepM y n . The generalisation we can prove is: ( choose · map build · inits ) ys ⊕ ( choose · filtJust · map ( stepsM ys < = < parseS ) · inits + ) xs =( choose · map build · inits ) ys ⊕ ( choose · map ( bsteps ys · build ) · inits + ) xs , ( )where inits + returns non-empty prefixes of the input list. When ys : = [ ] ,( ) reduces to choose · filtJust · map parseS · inits = choose · map build · inits , ageneralisation of ( ).We show the inductive case: ( choose · map build · inits ) ys ⊕ ( choose · filtJust · map ( stepsM ys < = < parseS ) · inits + ) ( x : xs )= ( choose · map build · inits ) ys ⊕ pickJust ( parseS ( ys ++ [ x ])) ⊕ ( choose · filtJust · map ( stepsM ys < = < parseS ) · inits + ) xs = { by ( ) } ( choose · map build · inits ) ys ⊕ build ( ys ++ [ x ]) ⊕ ( choose · filtJust · map ( stepsM ys < = < parseS ) · inits + ) xs = { induction } ( choose · map build · inits ) ys ⊕ build ( ys ++ [ x ]) ⊕ ( choose · map ( bsteps ys · build ) · inits + ) xs = ( choose · map build · inits ) ys ⊕ ( choose · map ( bsteps ys · build ) · inits + ) ( x : xs ))