Как поддерживать неизменяемый список, когда вы воздействуете на объект, связанный друг с другом в этом списке

Я пытаюсь запрограммировать быстрый алгоритм без допорядной сортировки (NDS) Deb, используемый в NSGA2, неизменным способом, используя Scala.

enter image description here

Но проблема кажется более сложной, чем я думаю, поэтому я упрощаю здесь проблему создания MWE.

Представьте себе совокупность Seq[A], и каждый элемент A будет decoratedA со списком, который содержит указатели на другие элементы популяции Seq[A].

Функция evalA(a:decoratedA) принимает список linkedA, который содержит и уменьшает значение каждого из них.

Далее я возьму список подмножеств decoratedAPopulation популяции A и вызовет evalA для каждого. У меня проблема, потому что между каждой итерацией по элементу в этом списке подмножеств decoratedAPopulation мне нужно обновить мою популяцию A новым decoratedA и обновленным linkedA содержать...

Более проблематично, каждый элемент популяции нуждается в обновлении "linkedA", чтобы заменить связанный элемент, если он изменится...

enter image description here

Hum, как вы можете видеть, кажется сложным поддерживать синхронизированный синхронизируемый список таким образом. Я предлагаю другое решение внизу, которое, вероятно, нуждается в рекурсии, чтобы вернуть после каждого evalA новую группу с замененным элементом.

enter image description here

Как я могу сделать это правильно неизменным способом?

Легко закодировать в изменчивом виде, но я не могу найти хороший способ сделать это непреложным способом, у вас есть путь или идея сделать это?

object test extends App{

  case class A(value:Int) {def decrement()= new A(value - 1)}

  case class decoratedA(oneAdecorated:A, listOfLinkedA:Seq[A])

  // We start algorithm loop with A element with value = 0
  val population = Seq(new A(0), new A(0), new A(8), new A(1))

  val decoratedApopulation = Seq(new decoratedA(population(1),Seq(population(2),population(3))),
                                 new decoratedA(population(2),Seq(population(1),population(3))))
  def evalA(a:decoratedA) = {
    val newListOfLinked = a.listOfLinkedA.map{e => e.decrement()
    new decoratedA(a.oneAdecorated,newListOfLinked)}
  }

  def run()= {
    //decoratedApopulation.map{
    // ?
    //}
  }
} 

Обновление 1:

О вводе/выводе исходного алгоритма.

Первая часть алгоритма Deb (Шаг 1 до Шаг 3) анализирует список Individual и вычисляет для каждого A: (a) кол-во подсчетов, число of A, которые доминируют во мне (атрибут value A) (b) список A я доминирует (listOfLinkedA).

Таким образом, он возвращает полностью заполненную совокупность decoratedA, а для входа в Шаг 4 (моя проблема) я беру первый не доминируемый фронт, ср. подмножество элементов decoratedA с A значением = 0.

Моя проблема начинается здесь, со списком decoratedA с A значением = 0; и я просматриваю следующий фронт в этом списке, вычисляя каждый listOfLinkedA каждого из этих A

На каждой итерации между шагами с 4 по 6 нужно вычислить новый B список подмножеств decoratedA с A значением = 0. Для каждого, я уменьшаю сначала атрибут count доминирования каждого элемента в listOfLinkedA, затем я фильтрую, чтобы получить элемент равным 0. A конец шага 6, B сохраняется в списке List[Seq[DecoratedA]], затем я перезапускаю до шага 4 с B и вычисляю новый C и т.д..

Что-то вроде этого в моем коде, я вызываю explore() для каждого элемента B, а Q равен концу нового подмножества decoratedA с value (пригодность здесь) = 0:

case class PopulationElement(popElement:Seq[Double]){
  implicit def poptodouble():Seq[Double] = {
    popElement
  }
}

class SolutionElement(values: PopulationElement, fitness:Double, dominates: Seq[SolutionElement])  {

  def decrement()= if (fitness == 0) this else new SolutionElement(values,fitness - 1, dominates)

  def explore(Q:Seq[SolutionElement]):(SolutionElement, Seq[SolutionElement])={
    // return all dominates elements with fitness - 1
    val newSolutionSet = dominates.map{_.decrement()}
    val filteredSolution:Seq[SolutionElement] = newSolutionSet.filter{s => s.fitness == 0.0}.diff{Q}
    filteredSolution

  }
}

A конец алгоритма, у меня есть окончательный список seq decoratedA List[Seq[DecoratedA]], который содержит все мои фронты, вычисленные.

Обновление 2

Образец значения, извлеченный из этого примера. Я беру только фронт парето (красный) и следующий {f, h, l} следующий фронт с преобладанием count = 1.

enter image description here

case class p(x: Int, y: Int)

val a = A(p(3.5, 1.0),0) 
val b = A(p(3.0, 1.5),0) 
val c = A(p(2.0, 2.0),0) 
val d = A(p(1.0, 3.0),0) 
val e = A(p(0.5, 4.0),0) 
val f = A(p(0.5, 4.5),1) 
val h = A(p(1.5, 3.5),1) 
val l = A(p(4.5, 1.0),1) 

case class A(XY:p, value:Int) {def decrement()= new A(XY, value - 1)}

case class ARoot(node:A, children:Seq[A])

val population = Seq(
  ARoot(a,Seq(f,h,l),
  ARoot(b,Seq(f,h,l)),
  ARoot(c,Seq(f,h,l)),   
  ARoot(d,Seq(f,h,l)),
  ARoot(e,Seq(f,h,l)),
  ARoot(f,Nil),
  ARoot(h,Nil),
  ARoot(l,Nil))

Возврат алгоритма List(List(a,b,c,d,e), List(f,h,l))

Обновление 3

Через 2 часа и некоторые проблемы с сопоставлением шаблонов (Ahum...) я возвращаюсь с полным примером, который автоматически вычисляет счетчик с преобладанием и дети каждого ARoot.

Но у меня такая же проблема, вычисление списка моих детей не совсем правильно, потому что каждый элемент A, возможно, является общим членом другого дочернего списка ARoot, поэтому мне нужно подумать о вашем ответе, чтобы изменить его:/В это время я только вычисляет список детей Seq[p], и мне нужен список Seq[A]

  case class p(x: Double, y: Double){
  def toSeq():Seq[Double] = Seq(x,y)
}

case class A(XY:p, dominatedCounter:Int) {def decrement()= new A(XY, dominatedCounter - 1)}

case class ARoot(node:A, children:Seq[A])
case class ARootRaw(node:A, children:Seq[p])

object test_stackoverflow extends App {

  val a = new p(3.5, 1.0)
  val b = new p(3.0, 1.5)
  val c = new p(2.0, 2.0)
  val d = new p(1.0, 3.0)
  val e = new p(0.5, 4.0)
  val f = new p(0.5, 4.5)
  val g = new p(1.5, 4.5)
  val h = new p(1.5, 3.5)
  val i = new p(2.0, 3.5)
  val j = new p(2.5, 3.0)
  val k = new p(3.5, 2.0)
  val l = new p(4.5, 1.0)
  val m = new p(4.5, 2.5)
  val n = new p(4.0, 4.0)
  val o = new p(3.0, 4.0)
  val p = new p(5.0, 4.5)

  def isStriclyDominated(p1: p, p2: p): Boolean = {
    (p1.toSeq zip p2.toSeq).exists { case (g1, g2) => g1 < g2 }
  }

  def sortedByRank(population: Seq[p]) = {

    def paretoRanking(values: Set[p]) = {
      //comment from @dk14: I suppose order of values isn't matter here, otherwise use SortedSet

      values.map { v1 =>
        val t = (values - v1).filter(isStriclyDominated(v1, _)).toSeq
        val a = new A(v1, values.size - t.size - 1)
        val root = new ARootRaw(a, t)
        println("Root value ", root)
        root
      }
    }

    val listOfARootRaw = paretoRanking(population.toSet)
    //From @dk14: Here is convertion from Seq[p] to Seq[A]
    val dominations: Map[p, Int] = listOfARootRaw.map(a => a.node.XY -> a.node.dominatedCounter) //From @dk14: It a map with dominatedCounter for each point
    val listOfARoot = listOfARootRaw.map(raw =>  ARoot(raw.node, raw.children.map(p => A(p, dominations.getOrElse(p, 0)))))

    listOfARoot.groupBy(_.node.dominatedCounter)

  }

  //Get the first front, a subset of ARoot, and start the step 4
  println(sortedByRank(Seq(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p)).head)

}

Ответ 1

Говоря о вашей проблеме с отличительными фронтами (после обновления 2):

val (left,right) = population.partition(_.node.value == 0)
List(left, right.map(_.copy(node = node.copy(value = node.value - 1))))

Не нужно ничего мутировать здесь. copy скопирует все, кроме полей, которые вы указали с новыми значениями. Говоря о коде, новая копия будет связана с одним и тем же списком детей, но с новым value = value - 1.

P.S. У меня такое чувство, что вы действительно можете сделать что-то вроде этого:

case class A(id: String, level: Int)
val a = A("a", 1)
val b = A("b", 2)
val c = A("c", 2)
val d = A("d", 3)

clusterize(List(a,b,c,d)) === List(List(a), List(b,c), List(d))

Это просто реализовать:

def clusterize(list: List[A]) = 
   list.groupBy(_.level).toList.sortBy(_._1).map(_._2)

Тест:

scala> clusterize(List(A("a", 1), A("b", 2), A("c", 2), A("d", 3)))
res2: List[List[A]] = List(List(A(a,1)), List(A(b,2), A(c,2)), List(A(d,3)))

P.S.2. Пожалуйста, рассмотрите лучшие соглашения об именах, например здесь.


Говоря о "мутирующих" элементах в некоторой сложной структуре:

Идея "непреложного мутирования" некоторой общей (между частями структуры) значения заключается в том, чтобы отделить вашу "мутацию" от структуры. Или просто говоря, разделите и убедите:

  • заранее рассчитать изменения
  • применить их

Код:

 case class A(v: Int)
 case class AA(a: A, seq: Seq[A]) //decoratedA

 def update(input: Seq[AA]) = {
    //shows how to decrement each value wherever it is:
    val stats = input.map(_.a).groupBy(identity).mapValues(_.size) //domination count for each A
    def upd(a: A) = A(a.v - stats.getOrElse(a, 0)) //apply decrement
    input.map(aa => aa.copy(aa = aa.seq.map(upd))) //traverse and "update" original structure
 }

Итак, я ввел новую структуру Map[A, Int], которая показывает, как изменить исходную. Этот подход основан на очень упрощенной версии Концепция Applicative Functor. В общем случае он должен быть Map[A, A => A] или даже Map[K, A => B] или даже Map[K, Zipper[A] => B] как прикладной функтор (input <*> map). * Застежка-молния (см. 1, 2) на самом деле может дать вам информацию о текущем элементе контекст.

Примечания:

  • Я предположил, что A с одинаковым значением одинаковы; это поведение по умолчанию для classessess, иначе вам нужно предоставить дополнительные id (или переопределить hashCode/equals).

  • Если вам нужно больше уровней - например, AA(AA(AA(...)))) - просто сделайте stats и upd рекурсивным, если вес diecrement зависит от уровня вложенности - просто добавьте уровень вложенности в качестве параметра в свою рекурсивную функцию.

  • Если декремент зависит от родительского node (например, только декремент A(3) ', который принадлежит A(3)) - добавьте родительский элемент node (s) как часть ключа stats и проанализируйте его во время upd.

  • Если существует некоторая зависимость между вычислением статистики (насколько уменьшится), скажем input(1) из input(0) - вы должны использовать foldLeft с частичной статистикой в ​​качестве аккумулятора: val stats = input.foldLeft(Map[A, Int]())((partialStats, elem) => partialStats ++ analize(partialStats, elem))

Btw, здесь требуется O(N) (линейная память и использование процессора)

Пример:

scala> val population = Seq(A(3), A(6), A(8), A(3))
population: Seq[A] = List(A(3), A(6), A(8), A(3))

scala> val input = Seq(AA(population(1),Seq(population(2),population(3))), AA(population(2),Seq(population(1),population(3))))
input: Seq[AA] = List(AA(A(6),List(A(8), A(3))), AA(A(8),List(A(6), A(3))))

scala> update(input)
res34: Seq[AA] = List(AA(A(5),List(A(7), A(3))), AA(A(7),List(A(5), A(3))))