Поиск пути Java в общих деревьях — Часть 2/4: алгоритмы

  • Поиск путей Java в общих деревьях. Часть 1/4: классы моделей
  • Поиск путей Java в общих деревьях — Часть 2/4: алгоритмы (этот пост)
  • Поиск пути Java в общих деревьях — часть 3/4: демонстрация
  • Поиск пути Java в общих деревьях — часть 4/4: модульные тесты

В этом посте я представлю алгоритмы поиска пути:

com.github.coderodde.pathfinding.BidirectionalBreadthFirstSearchPathfinder.java:

package com.github.coderodde.pathfinding;

import com.github.coderodde.pathfinding.WeightedTree.WeightedTreeNode;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * This class implements a bidirectional breadth-first search. It alternates 
 * two search frontiers meeting somewhere in between.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Nov 6, 2022)
 * @since 1.6 (Nov 6, 2022)
 */
public final class BidirectionalBreadthFirstSearchPathfinder 
        implements Pathfinder {

    @Override
    public WeightedPath search(WeightedTree tree, int sourceNodeId, int targetNodeId) {

        Objects.requireNonNull(tree, "The tree is null.");
        checkTerminalNodes(tree, sourceNodeId, targetNodeId);

        WeightedTree.WeightedTreeNode sourceNode = 
                tree.getWeightedTreeNode(sourceNodeId);

        WeightedTree.WeightedTreeNode targetNode = 
                tree.getWeightedTreeNode(targetNodeId);

        if (sourceNode.equals(targetNode)) {
            // We need to deal with this special case separately since the 
            // algorithm logic cannot handle it:
            return new WeightedPath(tree, Arrays.asList(sourceNode));
        }   

        Deque<WeightedTreeNode> queueForward  = new ArrayDeque<>();
        Deque<WeightedTreeNode> queueBackward = new ArrayDeque<>();

        Map<WeightedTreeNode, WeightedTreeNode> parentMapForward = 
                new HashMap<>();

        Map<WeightedTreeNode, WeightedTreeNode> parentMapBackward = 
                new HashMap<>();

        Map<WeightedTreeNode, Integer> distanceMapForward = 
                new HashMap<>();

        Map<WeightedTreeNode, Integer> distanceMapBackward = 
                new HashMap<>();

        int bestCost = Integer.MAX_VALUE;
        WeightedTreeNode touchNode = null;

        queueForward.addLast(sourceNode);
        queueBackward.addLast(targetNode);

        parentMapForward.put(sourceNode, null);
        parentMapBackward.put(targetNode, null);

        distanceMapForward.put(sourceNode, 0);
        distanceMapBackward.put(targetNode, 0);

        while (!queueForward.isEmpty() && !queueBackward.isEmpty()) {
            int distanceForward = 
                    distanceMapForward.get(queueForward.getFirst());

            int distanceBackward = 
                    distanceMapBackward.get(queueBackward.getFirst());

            if (touchNode != null
                    && bestCost < distanceForward + distanceBackward) {
                return tracebackPath(tree,
                                     touchNode, 
                                     parentMapForward, 
                                     parentMapBackward);
            }

            int forwardSearchAreaSize = queueForward.size() 
                                      + parentMapForward.size();

            int backwardSearchAreaSize = queueBackward.size() 
                                       + parentMapBackward.size();

            // Trivial load balancing:
            if (forwardSearchAreaSize < backwardSearchAreaSize) {
                WeightedTreeNode currentNode = queueForward.removeFirst();

                if (distanceMapBackward.containsKey(currentNode) 
                        && bestCost > distanceForward + distanceBackward) {

                    bestCost = distanceForward + distanceBackward;
                    touchNode = currentNode;
                }

                for (WeightedTreeNode neighbor : currentNode.getNeighbors()) {
                    if (!parentMapForward.containsKey(neighbor)) {
                        parentMapForward.put(neighbor, currentNode);
                        distanceMapForward.put(neighbor, 
                                               distanceMapForward.get(
                                                       currentNode) + 1);

                        queueForward.addLast(neighbor);
                    }
                }
            } else {
                WeightedTreeNode currentNode = queueBackward.removeFirst();

                if (distanceMapForward.containsKey(currentNode) 
                        && bestCost > distanceForward + distanceBackward) {

                    bestCost = distanceForward + distanceBackward;
                    touchNode = currentNode;
                }

                for (WeightedTreeNode neighbor : currentNode.getNeighbors()) {
                    if (!parentMapBackward.containsKey(neighbor)) {
                        parentMapBackward.put(neighbor, currentNode);
                        distanceMapBackward.put(neighbor, 
                                                distanceMapBackward.get(
                                                        currentNode) + 1);

                        queueBackward.addLast(neighbor);
                    }
                }
            }
        }

        throw new PathNotFoundException(sourceNodeId, targetNodeId);
    }
}

com.github.coderodde.pathfinding.BidirectionalIterativeDeepeningDepthFirstSearchPathfinder.java:

package com.github.coderodde.pathfinding;

import com.github.coderodde.pathfinding.WeightedTree.WeightedTreeNode;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Deque;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;

/**
 * This class implements a bidirectional iterative-deepening depth-first search
 * (IDDFS for short). It alternates two search frontiers meeting somewhere in 
 * between.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Nov 6, 2022)
 * @since 1.6 (Nov 6, 2022)
 */
public final class BidirectionalIterativeDeepeningDepthFirstSearchPathfinder 
implements Pathfinder {

    private final int maximumDepth;

    public BidirectionalIterativeDeepeningDepthFirstSearchPathfinder(
            int maximumDepth) {

        this.maximumDepth = checkMaximumDepth(maximumDepth);
    }

    public BidirectionalIterativeDeepeningDepthFirstSearchPathfinder() {
        this(Integer.MAX_VALUE);
    }

    @Override
    public WeightedPath search(WeightedTree tree, 
                               int sourceNodeId,
                               int targetNodeId) {

        Objects.requireNonNull(tree, "The tree is null.");
        checkTerminalNodes(tree, sourceNodeId, targetNodeId);

        WeightedTreeNode sourceNode = tree.getWeightedTreeNode(sourceNodeId);
        WeightedTreeNode targetNode = tree.getWeightedTreeNode(targetNodeId);

        List<WeightedTreeNode> path = searchImpl(tree, 
                                                 sourceNode, 
                                                 targetNode);
        return new WeightedPath(tree, path);
    }

    private void depthLimitedSearchForwards(WeightedTreeNode node, 
                                            int depth,
                                            Set<WeightedTreeNode> frontier,
                                            Set<WeightedTreeNode> visited) {
        if (depth == 0) {
            frontier.add(node);
            return;
        }

        for (WeightedTreeNode neighbor : node.getNeighbors()) {
            if (visited.contains(neighbor)) {
                continue;
            }

            visited.add(neighbor);

            depthLimitedSearchForwards(neighbor, 
                                       depth - 1, 
                                       frontier, 
                                       visited);
        }
    }

    private WeightedTreeNode
        depthLimitedSearchBackward(
                WeightedTreeNode node, 
                int depth,
                Set<WeightedTreeNode> frontier,
                Set<WeightedTreeNode> visited,
                Deque<WeightedTreeNode> backwardSearchStack) {
        backwardSearchStack.addFirst(node);

        if (depth == 0) {
            if (frontier.contains(node)) {
                return node;
            }

            backwardSearchStack.removeFirst();
            return null;
        }

        for (WeightedTreeNode neighbor : node.getNeighbors()) {
            if (visited.contains(neighbor)) {
                continue;
            }

            visited.add(neighbor);

            WeightedTreeNode meetingNode = 
                    depthLimitedSearchBackward(
                            neighbor,
                            depth - 1,
                            frontier,
                            visited,
                            backwardSearchStack);

            if (meetingNode != null) {
                return meetingNode;
            }
        }

        backwardSearchStack.removeFirst();
        return null;
    }

    private List<WeightedTreeNode> searchImpl(WeightedTree tree,
                                              WeightedTreeNode sourceNode, 
                                              WeightedTreeNode targetNode) {

        if (sourceNode.equals(targetNode)) {
            return Arrays.asList(sourceNode);
        }

        int totalForwardDepth = maximumDepth / 2;
        int totalBackwardDepth = maximumDepth - totalForwardDepth;

        int currentForwardDepth = 0;
        int currentBackwardDepth = 0;

        boolean incrementForwardSearchDepth = true;

        Deque<WeightedTreeNode> backwardSearchStack = new ArrayDeque<>();
        Set<WeightedTreeNode> frontier = new HashSet<>();
        Set<WeightedTreeNode> visited = new HashSet<>();

        while (currentForwardDepth + currentBackwardDepth <= maximumDepth) {
            visited.clear();
            visited.add(sourceNode);

            depthLimitedSearchForwards(sourceNode, 
                                       currentForwardDepth, 
                                       frontier,
                                       visited);

            for (int depth = currentBackwardDepth;
                    depth <= Math.min(currentBackwardDepth + 1, 
                                      totalBackwardDepth);
                    depth++) {

                visited.clear();
                visited.add(targetNode);

                WeightedTreeNode meetingNode = 
                        depthLimitedSearchBackward(targetNode,
                                                   depth,
                                                   frontier, 
                                                   visited,
                                                   backwardSearchStack);
                if (meetingNode != null) {
                    return buildPath(tree,
                                     meetingNode,
                                     sourceNode, 
                                     backwardSearchStack);
                }
            }

            frontier.clear();
            backwardSearchStack.clear();

            if (incrementForwardSearchDepth) {
                incrementForwardSearchDepth = false;
                currentForwardDepth++;
            } else {
                incrementForwardSearchDepth = true;
                currentBackwardDepth++;
            }
        }

        throw new PathNotFoundException(sourceNode.getId(), targetNode.getId());
    }

    private List<WeightedTreeNode>
         buildPath(WeightedTree tree,
                   WeightedTreeNode meetingNode, 
                   WeightedTreeNode sourceNode,
                   Deque<WeightedTreeNode> backwardSearchStack) {

        List<WeightedTreeNode> path = new ArrayList<>();
        List<WeightedTreeNode> prefixPath = 
                searchImpl(tree,
                           sourceNode, 
                           meetingNode);

        path.addAll(prefixPath);
        path.remove(path.size() - 1);
        path.addAll(backwardSearchStack);
        return path;
    }

    private int checkMaximumDepth(int depth) {
        if (depth < 0) {
            throw new IllegalArgumentException(
                    "The depth is negative: "
                            + depth
                            + ". Must be at least 0.");
        }

        return depth;
    }
}

com.github.coderodde.pathfinding.BreadthFirstSearchPathfinder.java:

package com.github.coderodde.pathfinding;

import com.github.coderodde.pathfinding.WeightedTree.WeightedTreeNode;
import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * This class implements the breadth-first search.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Nov 5, 2022)
 * @since 1.6 (Nov 5, 2022)
 */
public final class BreadthFirstSearchPathfinder implements Pathfinder {

    @Override
    public WeightedPath search(WeightedTree tree, 
                               int sourceNodeId,
                               int targetNodeId) {

        Objects.requireNonNull(tree, "The tree is null.");
        checkTerminalNodes(tree, sourceNodeId, targetNodeId);

        WeightedTreeNode sourceNode = tree.getWeightedTreeNode(sourceNodeId);
        WeightedTreeNode targetNode = tree.getWeightedTreeNode(targetNodeId);

        Deque<WeightedTreeNode> deque = new ArrayDeque<>();
        Map<WeightedTreeNode, WeightedTreeNode> parentMap = new HashMap<>();

        deque.add(sourceNode);
        parentMap.put(sourceNode, null);

        while (!deque.isEmpty()) {
            WeightedTreeNode currentNode = deque.removeFirst();

            if (currentNode.equals(targetNode)) {
                return tracebackPath(tree, targetNode, parentMap);
            }

            for (WeightedTreeNode neighborNode : 
                    tree.getNeighbors(currentNode.getId())) {
                if (parentMap.containsKey(neighborNode)) {
                    continue;
                }

                parentMap.put(neighborNode, currentNode);
                deque.addLast(neighborNode);
            }
        }

        throw new PathNotFoundException(sourceNodeId, targetNodeId);
    }
}

com.github.coderodde.pathfinding.DepthFirstSearchPathfinder.java:

package com.github.coderodde.pathfinding;

import com.github.coderodde.pathfinding.WeightedTree.WeightedTreeNode;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * This class implements a depth-first search. 
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Nov 6, 2022)
 * @since 1.6 (Nov 6, 2022)
 */
public class DepthFirstSearchPathfinder implements Pathfinder {

    @Override
    public WeightedPath search(WeightedTree tree,
                               int sourceNodeId, 
                               int targetNodeId) {

        Objects.requireNonNull(tree, "The tree is null.");
        checkTerminalNodes(tree, sourceNodeId, targetNodeId);

        WeightedTreeNode sourceNode = tree.getWeightedTreeNode(sourceNodeId);
        WeightedTreeNode targetNode = tree.getWeightedTreeNode(targetNodeId);

        Map<WeightedTreeNode, WeightedTreeNode> parentMap = new HashMap<>();

        parentMap.put(sourceNode, null);

        if (sourceNode.equals(targetNode)) {
            // Handle the trvial case separately, since it is not covered by
            // algorithm logic:
            return tracebackPath(tree, targetNode, parentMap);
        }

        for (WeightedTreeNode neighbor : sourceNode.getNeighbors()) {
            WeightedPath path = searchImpl(tree, 
                                           targetNode,
                                           sourceNode,
                                           neighbor,
                                           parentMap);

            if (path != null) {
                return path;    
            }
        }

        throw new PathNotFoundException(sourceNodeId, targetNodeId);
    }

    private WeightedPath searchImpl(WeightedTree tree,
                                    WeightedTreeNode targetNode,
                                    WeightedTreeNode parentNode,
                                    WeightedTreeNode node, 
                                    Map<WeightedTreeNode, 
                                        WeightedTreeNode> parentMap) {
        parentMap.put(node, parentNode);

        if (node.equals(targetNode)) {
            return tracebackPath(tree, targetNode, parentMap);
        }

        for (WeightedTreeNode neighbor : node.getNeighbors()) {
            if (parentMap.containsKey(neighbor)) {
                continue;
            }

            WeightedPath path = searchImpl(tree,
                                           targetNode,
                                           node, 
                                           neighbor, 
                                           parentMap);

            if (path != null) {
                return path;
            }
        }

        return null;
    }
}

com.github.coderodde.pathfinding.IterativeDeepeningDepthFirstSearchPathfinder.java:

package com.github.coderodde.pathfinding;

import com.github.coderodde.pathfinding.WeightedTree.WeightedTreeNode;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * This class implements a bidirectional breadth-first search. It alternates 
 * two search frontiers meeting somewhere in between.
 * 
 * @author Rodion "rodde" Efremov
 * @version 1.6 (Nov 6, 2022)
 * @since 1.6 (Nov 6, 2022)
 */
public final class IterativeDeepeningDepthFirstSearchPathfinder 
        implements Pathfinder {

    private final int maximumDepth;

    public IterativeDeepeningDepthFirstSearchPathfinder(int maximumDepth) {
        this.maximumDepth = checkMaximumDepth(maximumDepth);
    }

    public IterativeDeepeningDepthFirstSearchPathfinder() {
        this(Integer.MAX_VALUE);
    }

    @Override
    public WeightedPath search(WeightedTree tree, 
                               int sourceNodeId,
                               int targetNodeId) {

        Objects.requireNonNull(tree, "The tree is null.");
        checkTerminalNodes(tree, sourceNodeId, targetNodeId);

        WeightedTreeNode sourceNode = tree.getWeightedTreeNode(sourceNodeId);
        WeightedTreeNode targetNode = tree.getWeightedTreeNode(targetNodeId);

        Map<WeightedTreeNode, WeightedTreeNode> parentMap = new HashMap<>();

        if (sourceNode.equals(targetNode)) {
            parentMap.put(sourceNode, null);
            return tracebackPath(tree, targetNode, parentMap);
        }

        for (int depth = 0; depth <= maximumDepth; depth++) {
            parentMap.clear();
            parentMap.put(sourceNode, null);

            WeightedTreeNode found = depthLimitedSearch(sourceNode, 
                                                        targetNode,
                                                        depth, 
                                                        parentMap);

            if (found != null) {
                return tracebackPath(tree, targetNode, parentMap);
            }
        }

        throw new PathNotFoundException(sourceNodeId, targetNodeId);
    }

    private WeightedTreeNode 
        depthLimitedSearch(WeightedTreeNode node,
                           WeightedTreeNode targetNode, 
                           int depth, 
                           Map<WeightedTreeNode, 
                               WeightedTreeNode> parentMap) {

        if (depth == 0) {
            return node.equals(targetNode) ? targetNode : null;
        }

        for (WeightedTreeNode neighbor : node.getNeighbors()) {
            if (parentMap.containsKey(neighbor)) {
                continue;
            }

            parentMap.put(neighbor, node);

            WeightedTreeNode found = depthLimitedSearch(neighbor,
                                                        targetNode, 
                                                        depth - 1,
                                                        parentMap);
            if (found != null) {
                return found;
            }
        }

        return null;
    }

    private int checkMaximumDepth(int depth) {
        if (depth < 0) {
            throw new IllegalArgumentException(
                    "The depth is negative: "
                            + depth
                            + ". Must be at least 0.");
        }

        return depth;
    }
}

com.github.coderodde.pathfinding.PathNotFoundException.java:

package com.github.coderodde.pathfinding;

public final class PathNotFoundException extends RuntimeException {

    public PathNotFoundException(int sourceId, int targetId) {
        super("Path not found from " + sourceId + " to " + targetId + ".");
    }
}

Запрос критики

Как всегда, пожалуйста, расскажите мне все, что приходит на ум.

0

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *