【问题标题】:Functional/Stream programming for the graph problem "Reconstruct Itinerary"图问题“Reconstruct Itinerary”的函数式/流式编程
【发布时间】:2021-02-22 20:50:11
【问题描述】:

我正在尝试使用函数式方法在 Scala 中解决重构行程问题 (https://leetcode.com/problems/reconstruct-itinerary/)。 Java 解决方案有效,但 Scala 无效。我发现的一个原因是哈希图正在更新,并且每次迭代都有最新的哈希图(即使从递归中弹出),这很奇怪。

这是Java中的解决方案:

import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.PriorityQueue;

public class Solution1 {

private void dfg(Map<String, PriorityQueue<String>> adj, LinkedList<String> result, String vertex){
  PriorityQueue<String> pq = adj.get(vertex);
  while (pq!=null && !pq.isEmpty()){
    System.out.println("Before :"+adj.get(vertex));
    String v = pq.poll();
    System.out.println("After :"+ adj.get(vertex));
    dfg(adj,result,v);
  }
  result.addFirst(vertex);
}

public List<String> findItinerary(List<List<String>> tickets){
  Map<String,PriorityQueue<String>> adj = new HashMap<>();
  for(List<String> ticket:  tickets){
    adj.putIfAbsent(ticket.get(0),new PriorityQueue<>());
    adj.get(ticket.get(0)).add(ticket.get(1));
  }
  LinkedList<String> result = new LinkedList<>();
  dfg(adj,result,"JFK");
  //not reverse yet
  return result;
}

public static void main(String[] args){

  List<List<String>> tickets = new ArrayList<>();
  List t1= new ArrayList();
  t1.add("JFK");
  t1.add("SFO");
  tickets.add(t1);
  List t2= new ArrayList();
  t2.add("JFK");
  t2.add("ATL");
  tickets.add(t2);
  List t3= new ArrayList();
  t3.add("SFO");
  t3.add("ATL");
  tickets.add(t3);
  List t4= new ArrayList();
  t4.add("ATL");
  t4.add("JFK");
  tickets.add(t4);
  List t5= new ArrayList();
  t5.add("ATL");
  t5.add("SFO");
  tickets.add(t5);


  System.out.println();
  Solution1 s1 = new Solution1();
  List<String> finalRes  = s1.findItinerary(tickets);

  for(String model : finalRes) {
    System.out.print(model + " ");
  }
}

}

这是我在 Scala 中不起作用的解决方案:

package graph

class Itinerary {

}

case class Step(g: Map[String,List[String]],sort: List[String]=List())
object Solution {
  def main(arr: Array[String]) = {
    val tickets = List(List("JFK","SFO"),List("JFK","ATL"),List("SFO","ATL"),List("ATL","JFK"),List("ATL","SFO"))
    println(findItinerary(tickets))
  }
  def findItinerary(tickets: List[List[String]]): List[String] = {
    val g = tickets.foldLeft(Map[String,List[String]]())((m,t)=>{
      val key=t(0)
      val value= t(1)
      m + (key->(m.getOrElse(key,Nil) :+ value).sorted)
    })
    println(g)
    // g.keys.foldLeft(Step())((s,n)=> dfs(n,g,s)).sort.toList
    dfs("JFK",Step(g)).sort.toList
  }

  def dfs(vertex: String,step: Step): Step = {
    println("Input vertex " + vertex)
    println("Input map "+ step.g)
    val updatedStep= step.g.getOrElse(vertex,Nil).foldLeft(step) ((s,n)=>{
      //println("Processing "+n+" of vertex "+vertex)
      //delete link
      val newG = step.g + (vertex->step.g.getOrElse(vertex,Nil).filter(v=>v!=n))
      // println(newG)
      dfs(n,step.copy(g=newG))
    })
    println("adding vertex to result "+vertex)
    updatedStep.copy(sort = updatedStep.sort:+vertex)
  }
}

【问题讨论】:

    标签: algorithm scala recursion graph functional-programming


    【解决方案1】:

    Scala 有时被认为是“更好”的 Java,但这确实非常有限。如果您能够进入 FP 思维模式,并研究标准库,您会发现这是一个全新的世界。

    def findItinerary(tickets: List[List[String]]): List[String] = {
      def loop(from : String
              ,jump : Map[String,List[String]]
              ,acc  : List[String]) : List[String] = jump.get(from) match {
        case None => if (jump.isEmpty) from::acc else Nil
        case Some(next::Nil) => loop(next, jump - from, from::acc)
        case Some(nLst) =>
          nLst.view.map{ next =>
            loop(next, jump+(from->(nLst diff next::Nil)), from::acc)
          }.find(_.lengthIs > 0).getOrElse(Nil)
      }
      loop("JFK"
          ,tickets.groupMap(_(0))(_(1)).map(kv => kv._1 -> kv._2.sorted)
          ,Nil).reverse
    }
    

    【讨论】:

    • 恢复了对函数式编程的信心。这是一个多么漂亮的代码。非常感谢你们。这很有帮助。
    • 所以我们基本上做 DFS 并删除该节点之间的链接但是当我们弹出 [["JFK","SFO"],["JFK","ATL"],[ "SFO","ATL"],["ATL","JFK"],["ATL","SFO"]] 去它的另一个邻居时它如何开始不同的路径?我正在尝试使用您的算法实现leetcode.com/problems/all-paths-from-source-to-target,但对您的算法现在如何工作感到困惑
    • 另外,如果你不介意我问这个如何工作 case None => if (jump.isEmpty) from::acc else Nil and .find(_.lengthIs > 0).getOrElse(无)
    • case None =&gt; if (jump.isEmpty) from::acc else Nil - 这里没有“下一个”。 如果jump 地图为空,则表示行程已完成。返回将我们带到这里的路径。 否则我们在某个地方转错了方向。返回一个空路径,以便调用者知道这是一个死胡同。
    • .find(_.lengthIs &gt; 0).getOrElse(Nil) - 从已完成路径的惰性列表中返回第一个成功的路径(非空)。如果没有成功,则返回一个空路径,以便调用者知道这是一个死胡同。
    【解决方案2】:

    老实说,我没有查看您的代码来找出问题所在。但是,我被这个问题抓住了,决定试一试;这是代码:
    (希望我的代码对您有所帮助)

    type Airport = String // Refined 3 upper case letters.
    
    final case class AirlineTiket(from: Airport, to: Airport)
    
    object ReconstructItinerary {
      // I am using cats NonEmptyList to improve type safety, but you can easily remove it from the code.
      private final case class State(
          currentAirport: Airport,
          availableDestinations: Map[Airport, NonEmptyList[Airport]],
          solution: List[Airport]
      )
      
      def apply(tickets: List[AirlineTiket])(start: Airport): Option[List[Airport]] = {
        @annotation.tailrec
        def loop(currentState: State, checkpoints: List[State]): Option[List[Airport]] = {
          if (currentState.availableDestinations.isEmpty) {
            // We used all the tickets, so we can return this solution.
            Some((currentState.currentAirport :: currentState.solution).reverse)
          } else {
            val State(currentAirport, availableDestinations, solution) = currentState
            
            availableDestinations.get(currentAirport) match {
              case None =>
                // We got into nowhere, lets see if we can return to a previous state...
                checkpoints match {
                  case checkpoint :: remaining =>
                    // If we can return from there
                    loop(currentState = checkpoint, checkpoints = remaining)
                  
                  case Nil =>
                    // If we can't, then we can say that there is no solution.
                    None
                }
              
              case Some(NonEmptyList(destination, Nil)) =>
                // If from the current airport we can only travel to one destination, we will just follow that.
                loop(
                  currentState = State(
                    currentAirport = destination,
                    availableDestinations - currentAirport,
                    currentAirport :: solution
                  ),
                  checkpoints
                )
              
              case Some(NonEmptyList(destination, destinations @ head :: tail)) =>
               // If we can travel to more than one destination, we are going to try all in order.
               val newCheckpoints = destinations.map { altDestination =>
                 val newDestinations = NonEmptyList(head = destination, tail = destinations.filterNot(_ == altDestination))
                 State(
                   currentAirport = altDestination,
                   availableDestinations.updated(key = currentAirport, value = newDestinations),
                   currentAirport :: solution
                 )
               }
               
               loop(
                 currentState = State(
                   currentAirport = destination,
                   availableDestinations.updated(key = currentAirport, value = NonEmptyList(head, tail)),
                   currentAirport :: solution
                 ),
                 newCheckpoints ::: checkpoints
               )
            }
          }
        }
        
        val availableDestinations = tickets.groupByNel(_.from).view.mapValues(_.map(_.to).sorted).toMap 
        loop(
          currentState = State(
            currentAirport = start,
            availableDestinations,
            solution = List.empty
          ),
          checkpoints = List.empty
        )
      }
    }
    

    可以看到运行here的代码

    【讨论】:

    • 这段代码绝对有帮助和解释性。这很棒。我想知道为什么不可变对象和函数式编程会让 Java 代码如此庞大?
    • @DhavalKolapkar 主要是因为 Java 代码在很多方面都不安全。例如,它不是堆栈安全的,它不表明它是否没有找到解决方案,它可能会抛出一个NullPointerException,因为它相信 Map 有一个值;而且我不太确定它在所有情况下都能产生正确的结果。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-04-22
    • 1970-01-01
    • 1970-01-01
    • 2010-09-06
    • 2021-12-23
    相关资源
    最近更新 更多