Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert Branch and Bound loop to use Java Stream API

I have a simple Branch and Bound algorithm that works on a variant of the Traveling Salesman problem and I thought it would be fun to try and convert it to use the Java 8 Stream API. I'm having a difficult time figuring out how to do it without relying on side effects, however.

Initial Code

int bound = Integer.MAX_VALUE;
List<Location> bestPath = null;

while(!queue.isEmpty()) {
    Node curr = queue.poll();
    //bound exceeds best, bail
    if (curr.getBound() >= bound) { 
        return bestPath;
    }
    //have a complete path, save it
    if(curr.getPath().size() == locations.size()) {
        bestPath = curr.getPath();
        bound = curr.getBound();
        continue;
    }
    //incomplete path - add all possible next steps
    Set<Location> unvisited = new HashSet<>(locations);
    unvisited.removeAll(curr.getPath());
    for (Location l : unvisited) {
        List<Location> newPath = new ArrayList<>(curr.getPath());
        newPath.add(l);
        Node newNode = new Node(newPath, getBoundForPath(newPath));
        if (newNode.getBound() <= bound){
            queue.add(newNode);
        }
    }
}

I took a first shot at converting it to the Stream API and came up with the following:

Java 8 Version

Consumer<Node> nodeConsumer = node -> {
    if(node.getPath().size() == locations.size() ) {
        bestPath = node.getPath();
        bound = node.getBound();
    } else {
        locations.stream()
            .filter(l -> !node.getPath().contains(l))
            .map(l -> {
                List<Location> newPath = new ArrayList<>(node.getPath());
                newPath.add(s);
                return new Node(newPath, getBoundForPath(newPath));
            })
            .filter(newNode -> newNode.getBound() <= bound)
            .forEach(queue::add);
    }
};

Stream.generate(() -> queue.poll())
    .peek(nodeConsumer)
    .filter(s -> s.getBound() > bound)
    .findFirst();

return bestPath;

The main problem is that the nodeConsumer has to reference bestPath and bound, which are not final variables. I could make them final AtomicReference variables to work around this, but I feel like this sort of violates the spirit of the stream API. Can anyone help me distill the initial algorithm into a more idiomatic implementation?

like image 475
Alex Pritchard Avatar asked Sep 30 '15 00:09

Alex Pritchard


1 Answers

I wonder if using reduce is the way to go about this, as it allows you to track values without the need for external variables.

Something like the following (I've had to guess at a few of the details of your above code, but hopefully I'm on the right track).

    final BiFunction<Entry<Integer, List<Location>>, Node, Entry<Integer, List<Location>>> accumulator
        = (identity, node) -> {
            if (node.getPath().size() == locations.size() ) {
                return new SimpleEntry<>(node.getBound(), node.getPath());
            } else {
                locations.stream()
                    .filter(l -> !node.getPath().contains(l))
                    .map(l -> {
                        List<Location> newPath = new ArrayList<>(node.getPath());
                        newPath.add(l);
                        return new Node(newPath, getBoundForPath(newPath));
                    })
                    .filter(newNode -> newNode.getBound() <= identity.getKey())
                    .forEach(queue::add);
                return identity;
            }
        };

    final BinaryOperator<Entry<Integer, List<Location>>> combiner
        = (left, right) -> left.getKey() < right.getKey() ? left : right;

    final Entry<Integer, List<Location>> identity
        = new SimpleEntry<>(Integer.MAX_VALUE, null);

    final List<Location> bestValue = Stream.generate(queue::poll)
        .reduce(identity, accumulator, combiner)
        .getValue();

Alternatively, you could look at using Seq in jOOλ (a sequential extension to Streams), and use foldLeft instead.

like image 56
lukens Avatar answered Nov 09 '22 16:11

lukens