reset password
Author Message
rabbott
Posts: 1649
Posted 10:27 Oct 05, 2013 |

This book chapter is a very nice presentation of Dynamic Programming. Let's do the following.

  • Everyone read sections 6.1 and 6.2 (pp 169 - 173). These two sections present the basic idea behind dynamic programming and illustrate it with a nice example.
  • Each person (or pair of people) pick one of the other sections to present: 6.4, 6.5, 6.6, or 6.7. (All the sections are quite short.) To pick a section reply to this message saying which section you want and who will be doing that section. If a section has been selected by someone or some group, please select another section until they are all taken.
  • Each person select an exercise from the end of the chapter and write code that solves it. Let's have one exercise per person--although if you want to work in teams, do as many exercises as there are people on your team. Again, to avoid duplication reply to this message saying which exercise(s) you are doing and who will be doing it/them.

​This is not a difficult assignment.

Remember: this is a regular class; you are getting 4 units of credit for it; we will relax after the programming contest in a month. So let's put in the effort now.


The entire book is available here, either as a single pdf file or chapter-by-chapter. Perhaps we should go over some of the other chapters in the next few weeks. Let me know what you think.

Much of this material should have been covered in CS 312. Unfortunately, it's been many years (probably decades!) since I taught that course. The official syllabus for CS 312 mentions dynamic programming explicitly as one of the topics covered.

Last edited by rabbott at 20:45 Oct 09, 2013.
rabbott
Posts: 1649
Posted 10:56 Oct 06, 2013 |

Since I'm teaching CS 332f this quarter (again using Scala) I thought I'd write the code to find all the increasing subsequences of a sequence. (This is the example in section 6.2)  Here's what it looks like when executed in a Scala Worksheet. See comments below.

/** This is a helper function. It generates a new set with element at the front for each member
 *   of increasingSubsequences whose head is larger than element.
*/
 def addElement(element: Int, increasingSubsequences: Set[List[Int]]): Set[List[Int]] = {
    val result = increasingSubsequences.filter(element < _.head)
                                                               .map(element :: _)
                                                               .union(increasingSubsequences)
    if (result.exists(_.head == element)) result
    else result + List(element)
  }                                              

 def increasingSubsequences(list: List[Int]): Set[List[Int]] = {
    if (list.isEmpty) Set( )
    else addElement(list.head, increasingSubsequences(list.tail))
  }                                             
 
increasingSubsequences(List(5, 2, 8, 6, 3, 6, 9, 7)).toList.sortBy(-_.size)
                                                  //> res2: List[List[Int]] = List(List(2, 3, 6, 7), List(2, 3, 6, 9), List(2, 6, 
                                                  //| 7), List(5, 6, 9), List(2, 8, 9), List(2, 6, 9), List(2, 3, 9), List(3, 6, 7
                                                  //| ), List(2, 3, 7), List(3, 6, 9), List(5, 8, 9), List(5, 6, 7), List(3, 9), L
                                                  //| ist(2, 7), List(5, 9), List(6, 7), List(2, 9), List(5, 7), List(6, 9), List(
                                                  //| 3, 7), List(8, 9), List(9), List(7))

This is the recursive version, which works from left to right. It's also possible to write a version using an accumulator that scans from right to left. They are very similar in that they use the same add() function. The right-to-left version does exactly the same thing as the recursive version does as it unwinds the recursion. In the recursive version the returned values sets of increasing sequences take the place of the accumulator. The use of Sets for the increasing sequences avoids the duplication problem that often arises with recursion. 

The add() function adds an element to a set of increasing sequences. 

The key step is to add the element to all members of the set of increasing sequences whose head is larger than the element. It adds these new sequences (if any) to the existing set. The final step ensures that the new element has been used somewhere, either at the front of an increasing sequence or as a singleton.

The calls to toList.sortBy(0 - _.size) sorts the set of increasing sequences from longest to shortest. sortBy expects a function that maps the elements to be sorted to some ordered collection. In this case I mapped to the integers but mapped each list to the negative of its length to get the sort to come out longest to smallest.

Last edited by rabbott at 13:20 Oct 06, 2013.
rabbott
Posts: 1649
Posted 10:57 Oct 06, 2013 |

Please sign up for the section you are going to present and the problem you are going to code.


Here is a question about the knapsack problem (p 181). You want to solve:

K(w) = maximum value achievable with a knapsack of capacity w

Can we express this in terms of smaller subproblems? Well, if the optimal solution to K(w)
includes item i, then removing this item from the knapsack leaves an optimal solution to
K(w  - wi). In other words, K(w) is simply K(w - wi) + vi, for some i. We don't know which i,
so we need to try all possibilities.

My question to you is why is this true? Why is it that "if the optimal solution to K(w) includes item i, then removing this item from the knapsack leaves an optimal solution to K(w  - wi)."
 

Last edited by rabbott at 17:07 Oct 06, 2013.
rabbott
Posts: 1649
Posted 17:39 Oct 06, 2013 |

Problem 6.17 is very close to one of the first problems we do in CS 332f. The archive, which we don't do in 332f, eliminates redundant calls.

var archive = Map[(Int, List[Int]), Boolean]( ) 

/**
   *   Is it possible to make change for money using coins of certain denominations?
   */
def canMakeChange(money: Int, denominations: List[Int]): Boolean =
    // If this (money, denominations) pair is in the archive, return its value
    if (archive.contains((money, denominations))) archive((money, denominations))
    else {
      val answer =
         if (money == 0) true
         else if (denominations.isEmpty) false
         else if (denominations.head > money) canMakeChange(money, denominations.tail)
         else canMakeChange(money - denominations.head, denominations) ||
                canMakeChange(money, denominations.tail) 
      archive += (money, denominations) -> answer
      answer
    }                               //> canMakeChange: (money: Int, denominations: List[Int])Boolean

  // For testing reset the archive to empty each time.
  archive = Map[(Int, List[Int]), Boolean]()
  canMakeChange(12, List(10, 15))                 //> res0: Boolean = false
  archive = Map[(Int, List[Int]), Boolean]()
  canMakeChange(25, List(10, 15))                 //> res1: Boolean = true
  archive = Map[(Int, List[Int]), Boolean]()
  canMakeChange(13, List(2, 4, 10, 15))           //> res2: Boolean = false
  archive = Map[(Int, List[Int]), Boolean]()
  canMakeChange(16, List(2, 4, 10, 15))           //> res3: Boolean = true

 

Last edited by rabbott at 18:06 Oct 08, 2013.
rabbott
Posts: 1649
Posted 17:52 Oct 06, 2013 |

Problem 6.18 is a simple variation of 6.17. It is shown without the archive, which can be added as above.

/** May use a denomination only once */
def canMakeChangeOneUse(money: Int, denominations: List[Int]): Boolean = {
    if (money == 0) true
    else if (denominations.isEmpty) false
    else if (denominations.head > money) canMakeChangeOneUse(money, denominations.tail)
    else canMakeChangeOneUse(money - denominations.head, denominations.tail) ||
            canMakeChangeOneUse(money, denominations.tail)
  }                                               //> canMakeChangeOneUse: (money: Int, denominations: List[Int])Boolean

  canMakeChangeOneUse(16, List(1, 5, 10, 20))     //> res4: Boolean = true
  canMakeChangeOneUse(31, List(1, 5, 10, 20))     //> res5: Boolean = true
  canMakeChangeOneUse(40, List(1, 5, 10, 20))     //> res6: Boolean = false

Last edited by rabbott at 19:31 Oct 06, 2013.
rabbott
Posts: 1649
Posted 18:00 Oct 06, 2013 |

Problem 6.19 is the same as 6.17 but there is a limit on the number of coins that can be used. It is shown without the archive, which can be added as above.

  /**
    * Is it possible to use a limited number of coins of certain denominations make change for money?
    */

  def canMakeChangeLimitCoins(coinLimit: Int, money: Int, denominations: List[Int]): Boolean = {
     if (money == 0) true
     else if (denominations.isEmpty || coinLimit == 0) false
     else if (denominations.head > money) canMakeChangeLimitCoins(coinLimit, money, denominations.tail)
     else canMakeChangeLimitCoins(coinLimit-1, money - denominations.head, denominations) ||
            canMakeChangeLimitCoins(coinLimit, money, denominations.tail)
  }        //> canMakeChangeLimitCoins: (coinLimit: Int, money: Int, denominations: List[Int])Boolean

  canMakeChangeLimitCoins(6, 55, List(5, 10))     //> res7: Boolean = true
  canMakeChangeLimitCoins(6, 65, List(5, 10))     //> res8: Boolean = false

 
Last edited by rabbott at 19:33 Oct 06, 2013.
rabbott
Posts: 1649
Posted 22:10 Oct 06, 2013 |

Problem 6.22 looks like the same problem as 6.18.

rabbott
Posts: 1649
Posted 23:10 Oct 06, 2013 |

Some thoughts on problem 6.13.

A sequence where Player_1 should not be greedy. 1, 9, 100, 10. If Player_1 is greedy and takes the 10, Player_2 takes the 100 and wins. But if Player_1 takes the 1, Player_2 must take either the 9 or the 10 making the 100 available for Player_1. I believe that Player_1 can always ensure at least a draw. For example, with 1, 5, 6, 2. Both Players end with a total of 7. 

Initial processing. Think of the cards as an array [1 .. n]. Maintain two indices (left, right) to indicate the left-most and right-most unselected cards. Initially left = 1 and right = n. Any pair of indices with left < right and (left + right) mod 2 = 1 — i.e., (left + right) odd — represents a possible state of the game in terms of cards remaining. The goal of the initial computation is to build an n x n table for all pairs of valid indices. The value at table(left, right) is the move Player_1 should make along with the amount (which may be 0) by which Player_1 beats Player_2 if Player_1 makes that move and both play optimally from there on. (The n x n table will be only 1/4 filled.)

Given a state (left, right) there are 4 possible moves for Player_1 followed by Player_2. LL, LR, RL, RR. (LL means that Player_1 selected the card on the left as did Player_2. LR means that Player_1 selected the card on the left; Player_2 selected the card on the right.  Etc.

To compute the value in position (left, right) consider the four possible move sequences. Each move sequence yields a score based on the two cards taken and the value in the cell arrived at after those two moves. The value in any cell is the maximum of those four possible move values. The table can be computed either recursively top-down (but without repetition) or sequentially bottom up starting with states in which left = right - 1. Filling in the table is O(n2).

Once the table is computed, Player_1 simply consults it for each move.

 

Last edited by rabbott at 10:06 Oct 07, 2013.
rabbott
Posts: 1649
Posted 14:29 Oct 07, 2013 |

Here's code for problem 6.13.

 def game6_13(cards: List[Int]): Array[Array[Option[(Int, Char)]]] = {

   var moveTable = ofDim[Option[(Int, Char)]](cards.size, cards.size)
   for (i <- 0 until cards.size)
     moveTable(i) = Array.fill[Option[(Int, Char)]](cards.size)(None)

    def getBestMove(left: Int, right: Int): (Int, Char) = {
      if (moveTable(left)(right).isDefined) moveTable(left)(right).get
      else {
        val forPlayer1 = (left + right) % 2 == 1
        val bestMove = {
          if (left == right) (-cards(left), 'X') // No choice. Player_2 takes last card
          else {
            val scores: List[(Int, Char)] = List('L', 'R').map(
              _ match {
                case 'L' => ((if (forPlayer1) cards(left) else -cards(left)) + getBestMove(left + 1, right)._1, 'L')
                case 'R' => ((if (forPlayer1) cards(right) else -cards(right)) + getBestMove(left, right - 1)._1, 'R')
              }) //end map
            if (scores(0)._1 == scores(1)._1) (scores(0)._1, 'X')
            else if (forPlayer1) { if (scores(0)._1 > scores(1)._1) scores(0) else scores(1) }
              else if (scores(0)._1 < scores(1)._1) scores(0) else scores(1)
          } // end else
        } // end bestMove = 
        moveTable(left)(right) = Option(bestMove)
        println("Player_" + (if (forPlayer1) "1 " else "2 ") + left + " " + right + " " + moveTable(left)(right))
        bestMove
      } // end else
    } // end getBestMove

    for (left <- cards.size - 2 to 0 by -1) 
      for (right <- left +1 until cards.size) 
         getBestMove(left, right)

    moveTable
  } // end game6_13

 

Here's a call to it from a worksheet. Intermediate results are printed during the computation. For example,

Player_2 0 2 Some((-92,R))

means that it's Player_2's turn with the indices at 0 and 2. (For a given pair of indices it's always either Player_1's turn or Player_2's turn.) His best move is to take the R (right) card (which is 100). The final score will be -92 (from Player_1's perspective).

  def playGame6_13(cards: List[Int]): Array[Array[Option[(Int, Char)]]] = {
    var moveTable = game6_13(cards)
    for (i <- 0 until cards.size) {
      for (j <- 0 until cards.size)
        printf("%12s", moveTable(i)(j).getOrElse(""))
      println(" ")
    } //
    moveTable
  }                                               //> playGame6_13: (cards: List[Int])Array[Array[Option[(Int, Char)]]]
  playGame6_13(List(1, 9, 100, 10))              
                                                  //> Player_2 3 3 Some((-10,X))
                                                  //| Player_2 2 2 Some((-100,X))
                                                  //| Player_1 2 3 Some((90,L))
                                                  //| Player_2 1 1 Some((-9,X))
                                                  //| Player_1 1 2 Some((91,R))
                                                  //| Player_2 1 3 Some((81,X))
                                                  //| Player_2 0 0 Some((-1,X))
                                                  //| Player_1 0 1 Some((8,R))
                                                  //| Player_2 0 2 Some((-92,R))
                                                  //| Player_1 0 3 Some((82,L))
                                                  //|   (-1,X)       (8,R)     (-92,R)      (82,L) 
                                                  //|                   (-9,X)      (91,R)     (81,X) 
                                                  //|                               (-100,X)      (90,L) 
                                                  //|                                                 (-10,X) 
                                                  //| res0: Array[Array[Option[(Int, Char)]]] = Array(Array(Some((-1,X)), Some((8,
                                                  //| R)), Some((-92,R)), Some((82,L))), Array(None, Some((-9,X)), Some((91,R)), S
                                                  //| ome((81,X))), Array(None, None, Some((-100,X)), Some((90,L))), Array(None, N
                                                  //| one, None, Some((-10,X))))

  playGame6_13(List(1, 5, 6, 2))                  
                                                  //> Player_2 3 3 Some((-2,X))
                                                  //| Player_2 2 2 Some((-6,X))
                                                  //| Player_1 2 3 Some((4,L))
                                                  //| Player_2 1 1 Some((-5,X))
                                                  //| Player_1 1 2 Some((1,R))
                                                  //| Player_2 1 3 Some((-1,X))
                                                  //| Player_2 0 0 Some((-1,X))
                                                  //| Player_1 0 1 Some((4,R))
                                                  //| Player_2 0 2 Some((-2,R))
                                                  //| Player_1 0 3 Some((0,X))
                                                  //|   (-1,X)       (4,R)      (-2,R)       (0,X) 
                                                  //|                   (-5,X)       (1,R)      (-1,X) 
                                                  //|                                  (-6,X)        (4,L) 
                                                  //|                                                  (-2,X) 
                                                  //| res1: Array[Array[Option[(Int, Char)]]] = Array(Array(Some((-1,X)), Some((4,
                                                  //| R)), Some((-2,R)), Some((0,X))), Array(None, Some((-5,X)), Some((1,R)), Some
                                                  //| ((-1,X))), Array(None, None, Some((-6,X)), Some((4,L))), Array(None, None, N
                                                  //| one, Some((-2,X))))

 

Last edited by rabbott at 17:41 Oct 08, 2013.
rabbott
Posts: 1649
Posted 00:17 Oct 10, 2013 |

Updated Left-Right game code.

Last edited by rabbott at 13:15 Oct 11, 2013.
bpham2
Posts: 6
Posted 17:55 Oct 10, 2013 |

I will present 6.7 and problem 6.2

Eric Liao
Posts: 158
Posted 18:31 Oct 10, 2013 |

Michael and I will be doing section 6.4.

 

Exercise 6.20 for me

Exercise 6.26 for Michael

dyoung14
Posts: 4
Posted 19:09 Oct 10, 2013 |

I will cover 6.6 and problem 6.16

dedward
Posts: 22
Posted 23:28 Oct 10, 2013 |

I will cover 6.5 and problem 6.14.

rabbott
Posts: 1649
Posted 00:58 Oct 11, 2013 |

Here is some code for Bugs Integrated (placing chips on a plate).

Last edited by rabbott at 13:16 Oct 11, 2013.
yxia4
Posts: 3
Posted 11:20 Oct 11, 2013 |

I will cover exercise 6.7. 

rabbott
Posts: 1649
Posted 10:04 Oct 12, 2013 |

I liked Larry's problem of finding the longest palindromic subsequence so much that I thought I'd write it in Scala.  Here's the whole thing in a worksheet.

  def longestPalindrome(string: String): Set[String] = {
       var archive = Map[(Int, Int), Set[String]]()

         def longestPalindrome(i: Int, j: Int): Set[String] = {
               val result =
                    archive.getOrElse((i, j), {
                         if (i > j) Set("")
                         else if (i == j) Set(string(i) +: "")
                         else if (string(i) == string(j)) longestPalindrome(i + 1, j - 1).map(s => (string(i) +: s) :+ string(j))
                         else {
                                val leftSet = longestPalindrome(i, j - 1)
                                val rightSet = longestPalindrome(i + 1, j)
                                if (leftSet.head.length > rightSet.head.length) leftSet
                                else if (leftSet.head.length < rightSet.head.length) rightSet
                                // Is there a better way to deal with this case?
                                else leftSet ++ rightSet
                         }
                  archive += (i, j) -> result
                  result
                }
         }
         // There must be a better way to sort a set.
         scala.collection.immutable.TreeSet[String]() ++ longestPalindrome(0, string.length() - 1)
  }                                               //> longestPalindrome: (string: String)Set[String]
  
  
  longestPalindrome("ACGTGTCAAAATCG").mkString(", ")
                                                  //> res0: String = CTAAAATC, GCAAAACG, GTAAAATG

Last edited by rabbott at 20:23 Oct 13, 2013.
rabbott
Posts: 1649
Posted 20:20 Oct 13, 2013 |

Here is some generic Dynamic Programming code. 

package generalDP

trait Solution {
  override def toString: String
}

trait Problem {
  var archive = Map[Problem, (Problem, Solution)]()

  def baseSolution: Option[(Problem, Solution)]
  def subProblems: Set[Problem]
  def selectBest(problemsAndSolutions: Set[(Problem, Solution)]): (Problem, Solution)

  def solve: (Problem, Solution) = {
    archive.getOrElse(this, {
      baseSolution.getOrElse({
        val subproblems: Set[Problem] = subProblems
        val problemsWithSolutions: Set[(Problem, Solution)] = subproblems.map(_.solve)
        val problemWithSolution: (Problem, Solution) = selectBest(problemsWithSolutions)
        archive += this -> problemWithSolution
        problemWithSolution
      })
    })
  }

}

Here's how it could be used for the longestPalindrome problem. Unfortunately, the code for longestPalindrome is longer when using the generic framework than it was stand-alone!

import scala.collection.immutable.TreeSet

class LPSolution(palindromes: Set[String]) extends Solution { 
  def paliLength: Int = palindromes.head.length
  def palis: Set[String] = palindromes
  override def toString: String = "\n" + (TreeSet[String]() ++ palindromes).mkString("\n") + "\n"
}

class LPProblem(string: String, left: Int, right: Int) extends Problem {

  def this(string: String) = this(string, 0, string.length - 1)

  def baseSolution: Option[(LPProblem, LPSolution)] = {

    if (left > right) Some((this, new LPSolution(Set(""))))
    else if (left == right) Some((this, new LPSolution(Set(string(left) +: ""))))
    else None

  }

  def isBookended: Boolean = string(left) == string(right)

  def subProblems: Set[Problem] = {
    if (isBookended) 
      Set(new LPProblem(string, left + 1, right - 1))
    else Set(new LPProblem(string, left, right - 1), new LPProblem(string, left + 1, right))
  }

  def selectBest(problemsAndSolutions: Set[(Problem, Solution)]): (Problem, Solution) = {
    val (_, bookendSolution) = problemsAndSolutions.head.asInstanceOf[(LPProblem, LPSolution)]
    if (isBookended) (this, new LPSolution(bookendSolution.palis.map(string(left) +: _ :+ string(right))))
    else {
      val leftSet = bookendSolution
      val (_, rightSet): (LPProblem, LPSolution) = problemsAndSolutions.tail.head.asInstanceOf[(LPProblem, LPSolution)]
      if (leftSet.paliLength > rightSet.paliLength) (this, leftSet)
      else if (leftSet.paliLength < rightSet.paliLength) (this, rightSet)
      else (this, new LPSolution(leftSet.palis ++ rightSet.palis))
    }
  }

}

Last edited by rabbott at 20:22 Oct 13, 2013.
rabbott
Posts: 1649
Posted 23:07 Oct 14, 2013 |

Here is the palindrome problem from the bottom up.  It first finds all pairs of matching letters -- as well as each individual letter. Then it inserts these into each other until no more insertions are possible. 

/**
 *  @param string is the original string
 */
case class PairwisePaliSolver(string: String) {

  /**
   *  This is the class Pali of palindromes. It is a tree of Palindromes
   *  with the same left and right characters. children aset of internal Palindromes.
   *  @param left and right are the indices of the leftmost and righmost character.
   *  @param children are embedded palindromes. Only the largest children are kept.
   *  Therefore they all have the same length.
   *
   */
  case class Pali(left: Int, right: Int, children: Set[Pali]) {

    def this(left: Int, right: Int) = this(left, right, Set[Pali]())

    // The length of the children. Since only the largest are kept, 
    // the children all have the same length
    val childLength = if (children.isEmpty) 0 else children.head.length

    // The number of characters in this palidrome.
    val length: Int = (if (isASingleChar) 1 else 2) + childLength

    // The span of the string covered.
    def width = right - left + 1

    /**
     * Add potentialChild to the set of children if it compatible with this Palindrome.
     * It it is larger than the others, discard the others and keep only it.
     * It if is the same size add it to the set of children.
     * If it is smaller than the children don't add it.
     */
    def addChild(potentialChild: Pali): (Boolean, Pali) = {
      if (potentialChild.left <= left ||
          potentialChild.right >= right ||
          potentialChild.length < childLength) (false, this)
      else {
        val newChildren =
                 if (potentialChild.length > childLength) Set(potentialChild)
                 else children + potentialChild
        (true, Pali(left, right, newChildren))
      }
    }

    /**
     *  Is this Palindrome a single character?
     */
    private def isASingleChar = left == right

    /**
     * Generate a pretty-print nested and intended version of all the Palis
     * that this one represents.
     */
    override def toString: String = toString("\n")

    /**
     * Generate a pretty-print nested and intended version of all the Palis
     * that this one represents.
     * @param indent is "\n <some spaces> "
     */
    def toString(indent: String): String = {
      if (children.isEmpty)
        indent +
          (if (length == 1) string(left).toString
          else string(left) + (if (width == 2) "" else "-") + string(right))
      else {
        val childStrings = children.map(_.toString(indent + "  ")).mkString(", ")
        indent + string(left) + childStrings + indent + string(right)
      }
    }

    /**
     * Generate a Sequence of Strings of the palindromes this Pali represents.
     */
    def toString2: Seq[String] = {
      if (children.isEmpty)
        Seq({
          if (length == 1) string(left).toString
          else string(left) + (if (width == 2) "" else "-") + string(right)
        })
      else {
        val childStrings = children.map(_.toString2).toList.flatten
        childStrings.map((s: String) => string(left) + s + string(right))
      }
    }
  } // End of class Pali

  /**
   * Add childPali as a new child (using addChild) to each of the Pali's in palis.
   * For each potential parent, return (Boolean, Pali) where Boolean is true or false
   * depending on whether the child was added to the parent. Pali is either the
   * parent with the new child or the original Pali -- without the new child.
   */
  private def addChildToParents(childPali: Pali, palis: Seq[Pali]): (Boolean, Seq[Pali]) = {
    if (palis.isEmpty) (false, Seq())
    else {
      val insertions = palis.map(_.addChild(childPali))
      val (acceptedList, paliList) = insertions.unzip
      val wasInsertedSomewhere = acceptedList.exists(x => x)
      (wasInsertedSomewhere, paliList)
    }
  }

  /**
   * Return a Seq of Pali objects. Each stores pointers to
   * left and right characters that are the same. The children set is empty.
   * Later these will be embedded inside each other to build complete palindromes.
   * To test findPairs, remove the private and call
   *
   * > PairwisePaliSolver(<some string>)).findPairs
   */
  private def findPairs: Seq[Pali] = {
    for {
      left: Int <- 0 to string.length - 1;
      right: Int <- left to string.length - 1;
      if (string(left) == string(right))
    } yield new Pali(left, right)
  }

  /**
   * This is the method that does all the work. It takes the pairs generated
   * by findPairs and inserts one into another until no more insertions are
   * possible. At that point, all the palindromes will have been discovered.
   * The Palis in the returned Seq cannot be inserted into each other.
   *
   */
  private def pairsToPalis(palis: Seq[Pali]): Seq[Pali] = {
    if (palis.isEmpty) Seq()
    else {
      /* Sort the palis by width. Since each child must have a smaller width 
       * than its parent, no Pali can be inserted into another Pali that
       * is closer to the front of the list. This simplifies the insertion algorithm. 
       */
      val sortedPalis = palis.sortWith((a: Pali, b: Pali) => a.width < b.width)
      /*
       * Add the first Pali to each of the rest of the Palis if possible. If it 
       * can be added to any Pali, don't keep it in the list. If not keep it.
       * result will the same list of Palis as sortedPalis.tail except that some
       * of them may have sortedPalis.head inserted as a child.
       */
      val (wasInserted, result) = addChildToParents(sortedPalis.head, sortedPalis.tail)
      /* 
       * Call this method recursively on the rest of sortedPalis. newSeq will have
       * all the palis in sortedPalis.tail inserted into every other pali in 
       * sortedPalis.tailto the extent possible. The final question is whether to
       * keep sortedPalis.head. If it has been inserted somewhere, don't keep it.
       * If not, keep it as the head of the new Seq.
       */
      val newSeq = pairsToPalis(result)
      // Return the result
      if (wasInserted) newSeq else sortedPalis.head +: newSeq
    }
  }

  /**
   *  The main control.
   */
  def solve: Seq[Pali] = {
    val pairs: Seq[Pali] = findPairs // Find the initial pairs
    val palis = pairsToPalis(pairs) // Build the maximal Palis
    val maxLength = palis.map(_.length).max // maxLength is the length of the longest Pali
    palis.filter(_.length == maxLength) // Keep only Palis of that length
  }

}

rabbott
Posts: 1649
Posted 23:10 Oct 14, 2013 |

Here an example of running it.  The output is presented in two different ways.

  val p = PairwisePaliSolver("ammcdrcdbdca")      
                                                  //> p  : longestPalindrome.PairwisePaliSolver = PairwisePaliSolver(ammcdrcdbdca)
                                                  //| 
  val solution = p.solve                 //> solution  : Seq[longestPalindrome.worksheet.p.Pali] = List(
                                                  //| a
                                                  //|   c
                                                  //|     d
                                                  //|       b
                                                  //|     d
                                                  //|   c, 
                                                  //|   c
                                                  //|     d
                                                  //|       b
                                                  //|     d, 
                                                  //|     d
                                                  //|       r, 
                                                  //|       c
                                                  //|     d, 
                                                  //|     d
                                                  //|       r, 
                                                  //|       c, 
                                                  //|       d, 
                                                  //|       b
                                                  //|     d
                                                  //|   c
                                                  //| a)
  solution.map(_.toString2).flatten //> res0: Seq[String] = List(acdbdca, acdbdca, acdrdca, acdcdca, acdrdca, acdcdc
                                                  //| a, acdddca, acdbdca)