Съпоставяне на подмножества от скоби към знаци

Опитвам се да създам метод на Scala, който ще вземе една родителска група от скоби, представена като низ, и след това ще съпостави всяка подгрупа от скоби с различна буква. След това трябва да ги постави в карта, която връща, така че основно извиквам следния метод по следния начин:

val s = "((2((x+3)+6)))"
val map = mapParentheses(s)

Където s може да съдържа произволен брой скоби, а върнатата карта трябва да съдържа:

"(x+3)" -> 'a'

"(a+6)" -> 'b'

"(2b)" -> 'c'

"(c)" -> 'd'

Така че другаде в моята програма мога да си припомня 'd' и да получа "(c)", което ще стане "((2b))" след това ((2(a+6))) и накрая ((2((x+3)+6))). Низът, изпратен към метода mapParentheses, никога няма да има несъответстващи скоби или допълнителни знаци извън главните родителски скоби, така че следните елементи никога няма да бъдат изпратени:

  • "(fsf)a", защото a е извън родителските скоби
  • "(a(aa))(a)" защото (a) е извън родителските скоби
  • "((a)", защото скобите не съответстват
  • ")a(" защото скобите не съответстват

Така че се чудех дали някой знае за лесен (или не лесен) начин за създаване на този метод mapParentheses.


person Seren    schedule 15.09.2012    source източник
comment
Можете ли да имате няколко скоби на едно и също ниво (напр. ((x + 1) + (y + 2)))?   -  person Travis Brown    schedule 16.09.2012
comment
@TravisBrown - Няма значение. Гарантирано е, че низовете са правилни и общото решение е толкова лесно, колкото и това, което предполага, че няма да има множество блокове на едно и също ниво.   -  person Rex Kerr    schedule 16.09.2012
comment
@RexKerr: Можете да напишете малко по-хубава версия на комбинатора на анализатора, ако знаете, че имате само един на ниво.   -  person Travis Brown    schedule 16.09.2012
comment
@TravisBrown - Добре, демонстрирал си толкова, съгласен съм.   -  person Rex Kerr    schedule 16.09.2012
comment
@TravisBrown - Да, всякакви комбинации от скоби са възможни, но общият низ ще бъде обвит в родителски набор от скоби   -  person Seren    schedule 16.09.2012


Отговори (3)


Класически проблем с рекурсивен анализ. Може да е удобно да държите различните битове. Ще добавим няколко полезни метода, които да ни помогнат по-късно.

trait Part {
  def text: String
  override def toString = text
}
class Text(val text: String) extends Part {}
class Parens(val contents: Seq[Part]) extends Part {
  val text = "(" + contents.mkString + ")"
  def mapText(m: Map[Parens, Char]) = {
    val inside = contents.collect{
      case p: Parens => m(p).toString
      case x => x.toString
    }
    "(" + inside.mkString + ")"
  }
  override def equals(a: Any) = a match {
    case p: Parens => text == p.text
    case _ => false
  }
  override def hashCode = text.hashCode
}

Сега трябва да анализирате тези неща:

def str2parens(s: String): (Parens, String) = {
  def fail = throw new Exception("Wait, you told me the input would be perfect.")
  if (s(0) != '(') fail
  def parts(s: String, found: Seq[Part] = Vector.empty): (Seq[Part], String) = {
    if (s(0)==')') (found,s)
    else if (s(0)=='(') {
      val (p,s2) = str2parens(s)
      parts(s2, found :+ p)
    }
    else {
      val (tx,s2) = s.span(c => c != '(' && c != ')')
      parts(s2, found :+ new Text(tx))
    }
  }
  val (inside, more) = parts(s.tail)
  if (more(0)!=')') fail
  (new Parens(inside), more.tail)
}

Сега сме анализирали всичко. Така че нека намерим всички битове.

def findParens(p: Parens): Set[Parens] = {
  val inside = p.contents.collect{ case q: Parens => findParens(q) }
  inside.foldLeft(Set(p)){_ | _}
}

Сега можем да изградим картата, която искате.

def mapParentheses(s: String) = {
  val (p,_) = str2parens(s)
  val pmap = findParens(p).toSeq.sortBy(_.text.length).zipWithIndex.toMap
  val p2c = pmap.mapValues(i => ('a'+i).toChar)
  p2c.map{ case(p,c) => (p.mapText(p2c), c) }.toMap
}

Доказателство, че работи:

scala> val s = "((2((x+3)+6)))"
s: java.lang.String = ((2((x+3)+6)))

scala> val map = mapParentheses(s)
map: scala.collection.immutable.Map[java.lang.String,Char] =
  Map((x+3) -> a, (a+6) -> b, (2b) -> c, (c) -> d)

Ще го оставя като упражнение на читателя да разбере как работи, с намек, че рекурсията е наистина мощен начин за анализиране на рекурсивни структури.

person Rex Kerr    schedule 15.09.2012
comment
Защо да страдате от цялото това if (x(0)=='(')-иране, когато имате комбинатори за анализатори в стандартната библиотека? - person Travis Brown; 16.09.2012
comment
@TravisBrown - Тъй като когнитивното бреме от добавянето на библиотека за анализиране, когато имате почти тривиална задача, не си струва, ако искате да разберете как всъщност работи решението (а не че работи). - person Rex Kerr; 16.09.2012
comment
Съвсем честно, въпреки че мисля, че подходът на комбинаторния анализатор всъщност прави структурата на проблема много по-ясна, дори в доста прост случай като този. - person Travis Brown; 16.09.2012
comment
@TravisBrown - Това със сигурност прави някои части по-ясни, но има доста магия за разбиране. След като разберете както повърхностното ниво, така и вътрешната магия, съгласен съм, че това е по-добър начин да изразите логическата структура на проблема. - person Rex Kerr; 16.09.2012

Можете да направите това доста лесно с комбинаторите за анализатори на Scala. Първо за импортирането и някои прости структури от данни:

import scala.collection.mutable.Queue
import scala.util.parsing.combinator._

sealed trait Block {
  def text: String
}

case class Stuff(text: String) extends Block

case class Paren(m: List[(String, Char)]) extends Block {
  val text = m.head._2.toString
  def toMap = m.map { case (k, v) => "(" + k + ")" -> v }.toMap
}

Т.е., блокът представлява подниз на входа, който е или нещо без скоби, или скоби.

Сега за самия анализатор:

class ParenParser(fresh: Queue[Char]) extends RegexParsers {
  val stuff: Parser[Stuff] = "[^\\(\\)]+".r ^^ (Stuff(_))

  def paren: Parser[Paren] = ("(" ~> insides <~ ")") ^^ {
    case (s, m) => Paren((s -> fresh.dequeue) :: m)
  }

  def insides: Parser[(String, List[(String, Char)])] =
    rep1(paren | stuff) ^^ { blocks =>
      val s = blocks.flatMap(_.text)(collection.breakOut)
      val m = blocks.collect {
        case Paren(n) => n
      }.foldLeft(List.empty[(String, Char)])(_ ++ _)
      (s, m)
    }

  def parse(input: String) = this.parseAll(paren, input).get.toMap
}

Използването на get в последния ред до голяма степен не е идеално, но е оправдано от вашето твърдение, че можем да очакваме добре оформен вход.

Сега можем да създадем нов анализатор и да предадем променяща се опашка с някои свежи променливи:

val parser = new ParenParser(Queue('a', 'b', 'c', 'd', 'e', 'f'))

А сега опитайте своя тестов низ:

scala> println(parser parse "((2((x+3)+6)))")
Map((c) -> d, (2b) -> c, (a+6) -> b, (x+3) -> a)

По желание. По-интересно упражнение (оставено на читателя) би било да се прекара някакво състояние през анализатора, за да се избегне променливата опашка.

person Travis Brown    schedule 15.09.2012
comment
Не би ли било по-добре да вземете Iterator[Char] (или => Char), за да можете да генерирате безкраен източник, ако искате? - person Rex Kerr; 16.09.2012

def parse(s: String, 
  c: Char = 'a', out: Map[Char, String] = Map() ): Option[Map[Char, String]] =
  """\([^\(\)]*\)""".r.findFirstIn(s) match {
    case Some(m) => parse(s.replace(m, c.toString), (c + 1).toChar , out + (c -> m))
    case None if s.length == 1 => Some(out)
    case _ => None
  }

Това извежда Option, съдържащо Map, ако анализира, което е по-добре, отколкото да хвърли изключение, ако не го направи. Подозирам, че наистина искахте карта от Char до String, така че това извежда това. c и out са параметри по подразбиране, така че не е необходимо да ги въвеждате сами. Регулярният израз просто означава "всякакъв брой знаци, които не са скоби, затворени в скоби" (знаците за родители трябва да бъдат екранирани с "\"). findFirstIn намира първото съвпадение и връща Option[String], което можем да съпоставим по шаблон, заменяйки този низ със съответния знак.

val s = "((2((x+3)+6)))"
parse(s)  //Some(Map(a -> (x+3), b -> (a+6), c -> (2b), d -> (c)))
parse("(a(aa))(a)") //None
person Luigi Plinge    schedule 17.09.2012