查询多段航班中的有效解

 

背景概述

前段时间在做机票的查询航班业务时,发现这样一种业务场景:

会员查询航班时选择的行程为:

  • 5月18日,武汉飞北京
  • 5月18日,北京飞大连
  • 5月18日,大连飞武汉

调用航信接口获得的航班列表如下:

业务上为了保证客人两趟行程之间能够及时乘机,规定两趟航班之间的间隔时间不能小于一小时,所以用户可选择的情况共四种:

从上面可以看出:

  • 北京到大连行程中的航班CA123由于无法与大连到武汉行程的任一航班共同乘坐,导致该航班为无效航班
  • 武汉到北京行程中的航班CA113本可与CA123共同乘坐,但由于CA123为无效航班,导致该航班也为无效航班

所以在多段航班查询的结果中,需要经过一些逻辑处理将无效航班从结果中去除

并且假如会员第一段航程选择CA111,则第二段航程可选CA121和CA122,如果选择CA112,则第二段航程只可选CA122,所以在不同情况下给会员展示的后续航班列表也可能会发生变化

 

问题分析

经过仔细的思考后,可以将该业务场景抽象化为以下问题:

假设存在N个连续相关问题:Q1、Q2、Q3、……、Qn,每个问题都存在个不同个数的已知解

A11、A12、A13、……、A1x

A21、A22、A23、……、A2y

……

An1、An2、An3、……、Anz

且相邻问题的解之间需要满足特定的条件才可以共存,求N个问题的解集合S

该问题的难点在于:

  • 如何找出并删除无效解
  • 如何处理不同解之间的关联关系

 

解决思路

  1. 创建数据结构类有向图(DirectionGraph),图中存在若干个节点,不同的节点之间存在指向关系,称之为边
  2. 将每一个解构造成一个节点,将前后相关问题的解创建边
  3. 删除图不满足条件的指向边
  4. 删除图中不能构造成有效解的节点
  5. 将图转换成解集合

 

代码展示

先构造节点类如下:

import java.util.ArrayList;
import java.util.List;

/**
 * 节点类
 * @param <T>
 */
public class Node<T> {
    /** 所有前驱节点 */
    private List<Node<T>> prevList = new ArrayList<>();
    /** 存储的数据 */
    private T data;
    /** 所有后继节点 */
    private List<Node<T>> nextList = new ArrayList<>();

    public Node(T data) {
        this.data = data;
    }

    /**
     * 获取数据
     * @return
     */
    public T getData() {
        return data;
    }

    /**
     * 增加前驱节点
     * @param prevNode
     */
    public void addPrevNode(Node<T> prevNode) {
        if (!prevList.contains(prevNode)) {
            prevList.add(prevNode);
        }
    }

    /**
     * 删除前驱节点
     * @param prevNode
     */
    public void deletePrevNode(Node<T> prevNode) {
        prevList.remove(prevNode);
    }

    /**
     * 增加后继节点
     * @param nextNode
     */
    public void addNextNode(Node<T> nextNode) {
        if (!nextList.contains(nextNode)) {
            nextList.add(nextNode);
        }
    }

    /**
     * 删除后继节点
     * @param nextNode
     */
    public void deleteNextNode(Node<T> nextNode) {
        nextList.remove(nextNode);
    }

    /**
     * 是否存在前驱节点
     * @return
     */
    public boolean hasPrev() {
        return prevList != null && !prevList.isEmpty();
    }

    /**
     * 是否存在后继节点
     * @return
     */
    public boolean hasNext() {
        return nextList != null && !nextList.isEmpty();
    }

    /**
     * 获取所有前驱节点
     * @return
     */
    public List<Node<T>> getPrevList() {
        return prevList;
    }

    /**
     * 获取所有后继节点
     * @return
     */
    public List<Node<T>> getNextList() {
        return nextList;
    }
}

再构造有向图类:

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * 有向图类
 * @param <T>
 */
public class DirectionGraph<T> {

    /** 所有数据节点 */
    private List<Node<T>> nodes = new ArrayList<>();

    /**
     * 深度优先搜索获取遍历集合
     * @return
     */
    public List<List<T>> dfs() {
        List<List<T>> results = new ArrayList<>();
        for (Node<T> node : nodes) {
            // 只从初始节点开始遍历
            if (!node.hasPrev()) {
                for (Node<T> next : node.getNextList()) {
                    List<T> result = new ArrayList<>();
                    result.add(node.getData());
                    dfsNode(next, result, results);
                }
            }
        }
        return results;
    }

    /**
     * 深搜的递归函数
     * @param cur
     * @param curResult
     * @param results
     */
    private void dfsNode(Node<T> cur, List<T> curResult, List<List<T>> results) {
        curResult.add(cur.getData());
        if (!cur.hasNext()) {
            List<T> result = new ArrayList<>();
            result.addAll(curResult);
            results.add(result);
        } else {
            for (Node<T> next : cur.getNextList()) {
                dfsNode(next, curResult, results);
            }
        }
        curResult.remove(cur.getData());
    }

    /**
     * 获取所有节点
     * @return
     */
    public List<T> getAll() {
        List<T> all = new ArrayList<>();
        for (Node<T> node : nodes) {
            all.add(node.getData());
        }
        return all;
    }

    /**
     * 添加节点
     * @param data
     */
    public void add(T data) {
        if (!contains(data)) {
            nodes.add(new Node<T>(data));
        }
    }

    /**
     * 删除节点
     * @param data
     */
    public void delete(T data) {
        Node<T> node = getNode(data);
        if (node == null) {
            return;
        }
        Iterator prevListItr = node.getPrevList().iterator();
        while (prevListItr.hasNext()) {
            Node<T> prevNode = (Node<T>) prevListItr.next();
            prevNode.deleteNextNode(node);
            prevListItr.remove();
        }

        Iterator nextListItr = node.getNextList().iterator();
        while (nextListItr.hasNext()) {
            Node<T> nextNode = (Node<T>) nextListItr.next();
            nextNode.deletePrevNode(node);
            nextListItr.remove();
        }
        nodes.remove(node);
    }

    /**
     * 获取节点
     * @param data
     * @return
     */
    private Node<T> getNode(T data) {
        for (Node node : nodes) {
            if (node.getData().equals(data)) {
                return node;
            }
        }
        return null;
    }

    /**
     * 是否存在节点
     * @param data
     * @return
     */
    public boolean contains(T data) {
        return getNode(data) != null;
    }

    /**
     * 增加指向关系
     * @param prev
     * @param next
     */
    public void addEdge(T prev, T next) {
        Node<T> prevNode = getNode(prev);
        if (prevNode == null) {
            add(prev);
            prevNode = getNode(prev);
        }
        Node<T> nextNode = getNode(next);
        if (nextNode == null) {
            add(next);
            nextNode = getNode(next);
        }

        if (prevNode != null && nextNode != null) {
            prevNode.addNextNode(nextNode);
            nextNode.addPrevNode(prevNode);
        }
    }

    /**
     * 删除指向关系
     * @param prev
     * @param next
     */
    public void deleteEdge(T prev, T next) {
        Node<T> prevNode = getNode(prev);
        if (prevNode == null) {
            return;
        }
        Node<T> nextNode = getNode(next);
        if (nextNode == null) {
            return;
        }

        prevNode.deleteNextNode(nextNode);
        nextNode.deletePrevNode(prevNode);
    }

    /**
     * 是否存在指向关系
     * @param prev
     * @param next
     * @return
     */
    public boolean hasEdge(T prev, T next) {
        return getNextList(prev).contains(next);
    }

    /**
     * 获取所有的后继节点
     * @param data
     * @return
     */
    public List<T> getNextList(T data) {
        Node<T> node = getNode(data);
        List<T> nextList = new ArrayList<>();
        if (node != null ) {
            for (Node<T> nextNode : node.getNextList()) {
                nextList.add(nextNode.getData());
            }
        }
        return nextList;
    }

    /**
     * 获取所有的前驱节点
     * @param data
     * @return
     */
    public List<T> getPrevList(T data) {
        Node<T> node = getNode(data);
        List<T> prevList = new ArrayList<>();
        if (node != null ) {
            for (Node<T> prevNode : node.getPrevList()) {
                prevList.add(prevNode.getData());
            }
        }
        return prevList;
    }
}

最后创建测试类:

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class SearchFlightTest {
    public static void main(String[] args) throws ParseException {
        // 初始化节点数据,并记录初始节点和结束节点
        DirectionGraph<Flight> graph = initialGraph();
        List<Flight> starts = new ArrayList<>();
        List<Flight> ends = new ArrayList<>();
        for (Flight prev : graph.getAll()) {
            if (graph.getPrevList(prev).isEmpty()) {
                starts.add(prev);
            }
            if (graph.getNextList(prev).isEmpty()) {
                ends.add(prev);
            }
        }

        // 删除无效的边
        deleteInvalidEdge(graph);

        // 删除无效节点
        deleteInvalidNode(graph, starts, ends);
        
        // 深度优先搜索图,获取解集合
        List<List<Flight>> results = graph.dfs();

        // 输出解集合
        for (List<Flight> result : results) {
            StringBuilder sb = new StringBuilder();
            for (Flight flight : result) {
                sb.append(flight.getFlightNo() + " " + flight.getStart() + "起飞 " + flight.getEnd() + "降落 -> ");
            }
            System.out.println(sb.substring(0, sb.length() - 4));
        }
    }

    /**
     * 初始化有向图数据
     * @return
     */
    private static DirectionGraph<Flight> initialGraph() {
        // 初始化航班数据
        List<Flight> whu2pek = new ArrayList<>();
        whu2pek.add(new Flight("CA111", "2018-05-20 06:00", "2018-05-20 08:10"));
        whu2pek.add(new Flight("CA112", "2018-05-20 09:30", "2018-05-20 11:40"));
        whu2pek.add(new Flight("CA113", "2018-05-20 15:40", "2018-05-20 17:50"));

        List<Flight> pek2dlc = new ArrayList<>();
        pek2dlc.add(new Flight("CA121", "2018-05-20 10:20", "2018-05-20 11:50"));
        pek2dlc.add(new Flight("CA122", "2018-05-20 15:40", "2018-05-20 17:00"));
        pek2dlc.add(new Flight("CA123", "2018-05-20 19:30", "2018-05-20 21:05"));

        List<Flight> dlc2whu = new ArrayList<>();
        dlc2whu.add(new Flight("CA131", "2018-05-20 16:50", "2018-05-20 19:10"));
        dlc2whu.add(new Flight("CA132", "2018-05-20 21:00", "2018-05-20 23:20"));

        DirectionGraph<Flight> graph = new DirectionGraph<>();
        // 添加节点
        for (Flight flight : whu2pek) {
            graph.add(flight);
        }
        for (Flight flight : pek2dlc) {
            graph.add(flight);
        }
        for (Flight flight : dlc2whu) {
            graph.add(flight);
        }
        // 添加边,默认前后航班均可连接
        for (Flight prev : whu2pek) {
            for (Flight next : pek2dlc) {
                graph.addEdge(prev, next);
            }
        }
        for (Flight prev : pek2dlc) {
            for (Flight next : dlc2whu) {
                graph.addEdge(prev, next);
            }
        }
        return graph;
    }

    /**
     * 删除无效的边
     * @param graph
     * @throws ParseException
     */
    private static void deleteInvalidEdge(DirectionGraph<Flight> graph) throws ParseException {
        List<List<Flight>> deletedEdges = new ArrayList<>();    // 即将被删除的边
        for (Flight prev : graph.getAll()) {
            // 遍历每个父子关系,将不满足条件的进行记录后续删除
            for (Flight next : graph.getNextList(prev)) {
                if (!canMatch(prev, next)) {
                    List<Flight> deletedEdge = new ArrayList<>();
                    deletedEdge.add(prev);
                    deletedEdge.add(next);
                    deletedEdges.add(deletedEdge);
                }
            }
        }

        // 删除不满足条件的边
        for (List<Flight> edge : deletedEdges) {
            graph.deleteEdge(edge.get(0), edge.get(1));
        }
    }

    /**
     * 判断两个节点是否能满足条件
     * @param prev
     * @param next
     * @return
     * @throws ParseException
     */
    private static boolean canMatch(Flight prev, Flight next) throws ParseException {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm");
        Date prevEnd = sdf.parse(prev.getEnd());
        Date nextStart = sdf.parse(next.getStart());
        return nextStart.getTime() - prevEnd.getTime() >= 60 * 60 * 1000;
    }

    /**
     * 删除图中无法构成有效解的节点
     * @param graph
     * @param starts
     * @param ends
     */
    private static void deleteInvalidNode(DirectionGraph<Flight> graph, List<Flight> starts, List<Flight> ends) {
        List<Flight> deletedFlights = new ArrayList<>(); // 需要被删除的节点list
        for (;;) {
            // 重新补充list
            for (Flight flight : graph.getAll()) {
                List<Flight> prevList = graph.getPrevList(flight);
                List<Flight> nextList = graph.getNextList(flight);

                if (starts.contains(flight) && !ends.contains(flight)
                        && nextList.isEmpty()) {
                    deletedFlights.add(flight);     // 初始节点如果没有子节点就需要被删除
                }
                
                if (!starts.contains(flight) && ends.contains(flight)
                        && prevList.isEmpty()) {
                    deletedFlights.add(flight);     // 结束节点如果没有父节点就需要被删除
                }
                
                if (!starts.contains(flight) && !ends.contains(flight)
                        && (prevList.isEmpty() || nextList.isEmpty())) {
                    deletedFlights.add(flight);     // 中间节点如果没有父节点或者子节点就需要被删除
                }
            }

            // 没有需要被删除的节点才终止循环
            if (deletedFlights.isEmpty()) {
                break;
            }

            // 删除节点,删除后清空list
            for (Flight flight : deletedFlights) {
                graph.delete(flight);
            }
            deletedFlights.clear();
        } 
    }
}

测试结果如下:

 

总结思考

目前上述代码还存在一些可以思考的点,比如这个测试方法没有考虑单段查询的情况,有向图和节点类无法支持多线程,无法处理环状节点问题,初始节点、结束节点、还有抽象后的问题(Qn)都并没有很好的构建在数据结构中,是否可以将图增加层级的概念,以及是否可以采用单向而非双向的节点关联关系解决问题等等,希望大家能够一起多深入思考,以便理解和学习。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值