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?
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With