org.jamesframework.core.search.neigh.subset.adv.DisjointMultiSwapNeighbourhood Maven / Gradle / Ivy
Show all versions of james-core Show documentation
// Copyright 2014 Herman De Beukelaer
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package org.jamesframework.core.search.neigh.subset.adv;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import org.jamesframework.core.problems.solutions.SubsetSolution;
import org.jamesframework.core.search.algo.exh.SubsetSolutionIterator;
import org.jamesframework.core.search.neigh.Move;
import org.jamesframework.core.search.neigh.Neighbourhood;
import org.jamesframework.core.search.neigh.subset.SingleSwapNeighbourhood;
import org.jamesframework.core.util.SetUtilities;
/**
*
* A subset neighbourhood that generates moves performing a fixed number of simultaneous swaps of selected and unselected
* IDs. When applying moves generated by this neighbourhood to a given subset solution, the set of selected IDs will always
* remain of the same size. Therefore, this neighbourhood is only suited for fixed size subset selection problems. If
* desired, a set of fixed IDs can be provided which are not allowed to be swapped.
*
*
* Note that the number of possible moves quickly becomes very large when the size of the full set and/or selected
* subset increase. For example, generating all combinations of 2 simultaneous swaps already yields
* \[
* \frac{s(s-1)}{2} \times \frac{(n-s)(n-s-1)}{2}
* \]
* possibilities, where \(n\) is the size of the full set and \(s\) is the desired subset size. When selecting e.g.
* 30 out of 100 items, this value already exceeds one million. Because of the large number of possible moves,
* this extended neighbourhood should be used with care, especially in combination with searches that generate
* all moves in every step. Furthermore, searches that generate random moves may have few chances to find an
* improvement in case of a huge amount of possible neighbours.
*
*
* This neighbourhood is thread-safe: it can be safely used to concurrently generate moves in different searches
* running in separate threads.
*
*
* @author Herman De Beukelaer
*/
public class DisjointMultiSwapNeighbourhood implements Neighbourhood {
// number of simultaneous swaps
private final int numSwaps;
// set of fixed IDs (may be null or empty if no IDs are fixed)
private final Set fixedIDs;
/**
* Creates a multi swap neighbourhood without fixed IDs, indicating the number of (simultaneous)
* swaps performed by any generated move. If numSwaps
is 1, this neighbourhood
* generates exactly the same moves as the {@link SingleSwapNeighbourhood} so in such case
* it is advised to use the latter neighbourhood which has been optimized for this specific
* scenario.
*
* @param numSwaps number of swaps performed by any generated move (> 0)
* @throws IllegalArgumentException if maxSwaps
is not strictly positive
*/
public DisjointMultiSwapNeighbourhood(int numSwaps){
this(numSwaps, null);
}
/**
* Creates a multi swap neighbourhood with a given set of fixed IDs which are not allowed to be swapped.
* None of the generated moves will add nor remove any of these fixed IDs. All generated moves will swap
* exactly numSwaps
pairs of IDs. If numSwaps
is 1, this neighbourhood generates
* exactly the same moves as the {@link SingleSwapNeighbourhood} so in such case it is advised
* to use the latter neighbourhood which has been optimized for this specific scenario.
*
* @param numSwaps number of swaps performed by any generated move (> 0)
* @param fixedIDs set of fixed IDs which are not allowed to be swapped
* @throws IllegalArgumentException if numSwaps
is not strictly positive
*/
public DisjointMultiSwapNeighbourhood(int numSwaps, Set fixedIDs){
if(numSwaps <= 0){
throw new IllegalArgumentException("The number of swaps should be strictly positive.");
}
this.numSwaps = numSwaps;
this.fixedIDs = fixedIDs;
}
/**
* Generates a move for the given subset solution that removes a random subset of \(k\) IDs from the current
* selection and replaces them with an equally large random subset of the currently unselected IDs, where
* \(k\) is the number of swaps specified at construction. Possible fixed IDs are not considered to be swapped.
* If no swaps can be performed, null
is returned.
*
* @param solution solution for which a random multi swap move is generated
* @return random multi swap move, null
if no swaps can be performed
*/
@Override
public Move getRandomMove(SubsetSolution solution) {
// get set of candidate IDs for deletion and addition
Set deleteCandidates = solution.getSelectedIDs();
Set addCandidates = solution.getUnselectedIDs();
// remove fixed IDs, if any, from candidates
if(fixedIDs != null && !fixedIDs.isEmpty()){
deleteCandidates = new HashSet<>(deleteCandidates);
addCandidates = new HashSet<>(addCandidates);
deleteCandidates.removeAll(fixedIDs);
addCandidates.removeAll(fixedIDs);
}
// return null if no swaps are possible
if(!canSwap(addCandidates, deleteCandidates)){
// impossible to perform a swap
return null;
}
// use thread local random for better concurrent performance
Random rg = ThreadLocalRandom.current();
// pick random IDs to remove from selection
Set del = SetUtilities.getRandomSubset(deleteCandidates, numSwaps, rg);
// pick random IDs to add to selection
Set add = SetUtilities.getRandomSubset(addCandidates, numSwaps, rg);
// create and return move
return new GeneralSubsetMove(add, del);
}
/**
*
* Generates the set of all possible moves that perform exactly \(k\) swaps, where \(k\) is the desired number
* of swaps specified at construction. Possible fixed IDs are not considered to be swapped. If \(m < k\)
* IDs are currently selected or unselected (excluding any fixed IDs), no swaps can be performed and the
* returned set will be empty.
*
* @param solution solution for which all possible multi swap moves are generated
* @return set of all multi swap moves, may be empty
*/
@Override
public Set> getAllMoves(SubsetSolution solution) {
// create empty set to store generated moves
Set> moves = new HashSet<>();
// get set of candidate IDs for deletion and addition
Set deleteCandidates = solution.getSelectedIDs();
Set addCandidates = solution.getUnselectedIDs();
// remove fixed IDs, if any, from candidates
if(fixedIDs != null && !fixedIDs.isEmpty()){
deleteCandidates = new HashSet<>(deleteCandidates);
addCandidates = new HashSet<>(addCandidates);
deleteCandidates.removeAll(fixedIDs);
addCandidates.removeAll(fixedIDs);
}
// possible to perform desired number of swaps?
if(!canSwap(addCandidates, deleteCandidates)){
// impossible: return empty set
return moves;
}
// create all moves performing numSwaps swaps
SubsetSolutionIterator itDel, itAdd;
Set del, add;
itDel = new SubsetSolutionIterator(deleteCandidates, numSwaps);
while(itDel.hasNext()){
del = itDel.next().getSelectedIDs();
itAdd = new SubsetSolutionIterator(addCandidates, numSwaps);
while(itAdd.hasNext()){
add = itAdd.next().getSelectedIDs();
// create and add move
moves.add(new GeneralSubsetMove(add, del));
}
}
// return all moves
return moves;
}
/**
* Checks whether it is possible to perform the desired number of swaps, as specified at construction.
*
* @param addCandidates candidate IDs to be added to the selection
* @param deleteCandidates candidate IDs to be removed from the selection
* @return true
if the desired number of swaps can be performed
*/
private boolean canSwap(Set addCandidates, Set deleteCandidates){
return Math.min(addCandidates.size(), deleteCandidates.size()) >= numSwaps;
}
}