/* * Copyright 2015 Lei CHEN ([email protected]) * * 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.raistlic.common.permutation; import org.raistlic.common.precondition.Precondition; import java.math.BigInteger; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; import java.util.List; /** * Utility class for easy query of all possible combinations of "picking m elements from an n-size * collection". * * It basically provides the functionalities of : * 1 - enquiry the number of combination count of combination(m, n) * 2 - given an ordinal number i, fetch the i-th combination sequence as a read-only list. * 3 - convenient for-each iteration of all the combinations * * It is NOT thread safe. * * It re-uses one array to fetch the combination, so if the user want to keep the i-th combination * result, make a copy. */ public class Combination<E> implements Iterable<List<E>> { /** * A callback interface that implements the combination algorithm. */ public interface Algorithm { /** * Returns the maximum size of collection that the algorithm implementation supports, it is * {@link Integer#MAX_VALUE} by default. * * @return the maximum size of collection that the algorithm implementation supports. */ default int getMaxSupportedSize() { return Integer.MAX_VALUE; } /** * Returns the number of all possible combinations, for picking {@code numberToPick} elements * from a collection of size {@code numberOfElements}. * * @param numberOfElements the collection size, must be no less than {@code 0} . * @param numberToPick the number of elements to pick, must be no less than {@code 0} and no more * than {@code numberOfElements} . * @return the number of all possible combinations. * * @throws org.raistlic.common.precondition.InvalidParameterException when {@code numberOfElements} * or {@code numberToPick} is invalid. */ BigInteger getCombinationCount(int numberOfElements, int numberToPick); /** * Fills the {@code target} array with selected elements in the {@code source} array, as the * result of the {@code ordinal}-th combination result. * * @param source the source array, cannot be {@code null} * @param target the target array, cannot be {@code null}, and it's size cannot be bigger than * {@code source} . * @param ordinal the index of the combination result to pick, must be no less than {@code 0} and * must be less than {@code getCombinationCount(source.length, target.length)} . * * @throws org.raistlic.common.precondition.InvalidParameterException when any of the parameters * is invalid. */ void fetchCombination(Object[] source, Object[] target, BigInteger ordinal); } static final Algorithm DEFAULT_ALGORITHM = DefaultAlgorithm.INSTANCE; /** * Factory method that exports a combination instance with the specified {@code elements} collection * and {@code numberToPick} , using the default algorithm implementation. * * @param elements the elements collection to pick combination from, cannot be {@code null}. * @param numberToPick the number of elements to pick, must be no less than {@code 0} and no more * than {@code elements} size. * @param <E> the element type. * @return the combination instance. * * @throws org.raistlic.common.precondition.InvalidParameterException when any of the parameters * is invalid. */ public static <E> Combination<E> of(Collection<E> elements, int numberToPick) { return of(elements, numberToPick, DEFAULT_ALGORITHM); } /** * Factory method that exports a combination instance with the specified {@code elements} collection * and {@code numberToPick} , using the {@code algorithm} callback provided, or the default algorithm * if the {@code algorithm} parameter passed in is {@code null}. * * @param elements the elements collection to pick combination from, cannot be {@code null}. * @param numberToPick the number of elements to pick, must be no less than {@code 0} and no more * than {@code elements} size. * @param algorithm the algorithm as a callback for picking combinations, or {@code null} if using * the default algorithm. * @param <E> the element type. * @return the combination instance. * * @throws org.raistlic.common.precondition.InvalidParameterException when any of the parameters * is invalid. */ public static <E> Combination<E> of(Collection<E> elements, int numberToPick, Algorithm algorithm) { if (algorithm == null) { algorithm = DEFAULT_ALGORITHM; } Precondition.param(elements).isNotNull("'elements' cannot be null."); Precondition.param(elements.size()).lessThanOrEqualTo( algorithm.getMaxSupportedSize(), "'elements' size is too big and not supported by the algorithm: " + elements.size() + " / " + algorithm.getMaxSupportedSize() ); Precondition.param(numberToPick) .greaterThanOrEqualTo(0, "Invalid 'numberToPick': " + numberToPick) .lessThanOrEqualTo(elements.size(), "Invalid 'numberToPick': " + numberToPick); // defensive copy before checking collection size and numberToPick: @SuppressWarnings("unchecked") E[] arr = (E[])elements.toArray(); return new Combination<>(arr, numberToPick, algorithm); } /* * An internal copy of the elements collection to pick combinations from */ private E[] elements; /* * A buffer that is reused every time to store the picked combination result */ private E[] result; /* * The algorithm to do the combination pick */ private Algorithm algorithm; /* * A cache that stores the number of combinations * C(elements.length, result.length), to avoid frequent repeat calculations. */ private BigInteger count; /* * Wraps result as an unmodifiable list. */ private List<E> resultList; @SuppressWarnings("unchecked") private Combination(E[] elements, int numberToPick, Algorithm algorithm) { // no defensive copy here because it's already done in the factory method. this.elements = elements; this.result = (E[])new Object[numberToPick]; this.algorithm = algorithm; this.count = algorithm.getCombinationCount(elements.length, numberToPick); this.resultList = Collections.unmodifiableList(Arrays.asList(result)); } /** * Returns the number of all possible combinations. * * @return the number of all possible combinations. */ public BigInteger getCombinationCount() { return count; } /** * Returns the {@code ordinal}-th combination result, as a {@link List} . * * @param ordinal the index of the combination result to return, cannot be {@code null} and must * be no less than {@code 0} and must less than {@link #getCombinationCount()}. * @return the {@code ordinal}-th combination result. * * @throws org.raistlic.common.precondition.InvalidParameterException when {@code ordinal} is * {@code null} or out of the valid range. */ public List<E> getCombination(BigInteger ordinal) { Precondition.param(ordinal) .isNotNull("ordinal number cannot be null.") .greaterThanOrEqualTo(BigInteger.ZERO, "ordinal number out of range: " + ordinal) .lessThan(getCombinationCount(), "ordinal number out of range: " + ordinal + " / " + getCombinationCount()); algorithm.fetchCombination(elements, result, ordinal); return resultList; } /** * {@inheritDoc} * * <p> * The iterator returned here is a read-only iterator, and does not support * {@link java.util.Iterator#remove()} operation, calling the method will * cause an {@link java.lang.UnsupportedOperationException}. */ @Override public Iterator<List<E>> iterator() { return this.new OrdinalIterator(); } private class OrdinalIterator implements Iterator<List<E>> { private BigInteger ordinal; private OrdinalIterator() { ordinal = BigInteger.ZERO; } @Override public boolean hasNext() { return ordinal.compareTo(getCombinationCount()) < 0; } @Override public List<E> next() { List<E> result = getCombination(ordinal); ordinal = ordinal.add(BigInteger.ONE); return result; } @Override public void remove() { throw new UnsupportedOperationException( "Cannot remove from a read-only iterator."); } } private enum DefaultAlgorithm implements Algorithm { INSTANCE; @Override public int getMaxSupportedSize() { return Integer.MAX_VALUE; } @Override public BigInteger getCombinationCount(int numberOfElements, int numberToPick) { // no parameter validation because this is only called in the package. if( numberToPick == 0 || numberToPick == numberOfElements) { return BigInteger.ONE; } else { BigInteger pAll = Factorial.of(numberOfElements); BigInteger pPick = Factorial.of(numberToPick); BigInteger pRest = Factorial.of(numberOfElements - numberToPick); return pAll.divide(pPick.multiply(pRest)); } } @Override public void fetchCombination(Object[] source, Object[] target, BigInteger ordinal) { // no parameter validation because this is only called in the package. for(int i=0, si=0; i<target.length; i++, si++) { if( ordinal.compareTo(BigInteger.ZERO) > 0 ) { BigInteger cLeft = getCombinationCount(source.length - si - 1, target.length - i - 1); while( ordinal.compareTo(cLeft) >= 0 ) { si++; ordinal = ordinal.subtract(cLeft); if( ordinal.compareTo(BigInteger.ZERO) == 0 ) { break; } cLeft = getCombinationCount(source.length - si - 1, target.length - i - 1); } } target[i] = source[si]; } } } }