001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.math4.genetics;
018
019import java.util.ArrayList;
020import java.util.HashSet;
021import java.util.List;
022import java.util.Set;
023
024import org.apache.commons.math4.exception.DimensionMismatchException;
025import org.apache.commons.math4.exception.MathIllegalArgumentException;
026import org.apache.commons.math4.exception.util.LocalizedFormats;
027
028/**
029 * Cycle Crossover [CX] builds offspring from <b>ordered</b> chromosomes by identifying cycles
030 * between two parent chromosomes. To form the children, the cycles are copied from the
031 * respective parents.
032 * <p>
033 * To form a cycle the following procedure is applied:
034 * <ol>
035 *   <li>start with the first gene of parent 1</li>
036 *   <li>look at the gene at the same position of parent 2</li>
037 *   <li>go to the position with the same gene in parent 1</li>
038 *   <li>add this gene index to the cycle</li>
039 *   <li>repeat the steps 2-5 until we arrive at the starting gene of this cycle</li>
040 * </ol>
041 * The indices that form a cycle are then used to form the children in alternating order, i.e.
042 * in cycle 1, the genes of parent 1 are copied to child 1, while in cycle 2 the genes of parent 1
043 * are copied to child 2, and so forth ...
044 *
045 * Example (zero-start cycle):
046 * <pre>
047 * p1 = (8 4 7 3 6 2 5 1 9 0)    X   c1 = (8 1 2 3 4 5 6 7 9 0)
048 * p2 = (0 1 2 3 4 5 6 7 8 9)    X   c2 = (0 4 7 3 6 2 5 1 8 9)
049 *
050 * cycle 1: 8 0 9
051 * cycle 2: 4 1 7 2 5 6
052 * cycle 3: 3
053 * </pre>
054 *
055 * This policy works only on {@link AbstractListChromosome}, and therefore it
056 * is parameterized by T. Moreover, the chromosomes must have same lengths.
057 *
058 * @see <a href="http://www.rubicite.com/Tutorials/GeneticAlgorithms/CrossoverOperators/CycleCrossoverOperator.aspx">
059 * Cycle Crossover Operator</a>
060 *
061 * @param <T> generic type of the {@link AbstractListChromosome}s for crossover
062 * @since 3.1
063 */
064public class CycleCrossover<T> implements CrossoverPolicy {
065
066    /** If the start index shall be chosen randomly. */
067    private final boolean randomStart;
068
069    /**
070     * Creates a new {@link CycleCrossover} policy.
071     */
072    public CycleCrossover() {
073        this(false);
074    }
075
076    /**
077     * Creates a new {@link CycleCrossover} policy using the given {@code randomStart} behavior.
078     *
079     * @param randomStart whether the start index shall be chosen randomly or be set to 0
080     */
081    public CycleCrossover(final boolean randomStart) {
082        this.randomStart = randomStart;
083    }
084
085    /**
086     * Returns whether the starting index is chosen randomly or set to zero.
087     *
088     * @return {@code true} if the starting index is chosen randomly, {@code false} otherwise
089     */
090    public boolean isRandomStart() {
091        return randomStart;
092    }
093
094    /**
095     * {@inheritDoc}
096     *
097     * @throws MathIllegalArgumentException if the chromosomes are not an instance of {@link AbstractListChromosome}
098     * @throws DimensionMismatchException if the length of the two chromosomes is different
099     */
100    @Override
101    @SuppressWarnings("unchecked")
102    public ChromosomePair crossover(final Chromosome first, final Chromosome second)
103        throws DimensionMismatchException, MathIllegalArgumentException {
104
105        if (!(first instanceof AbstractListChromosome<?> && second instanceof AbstractListChromosome<?>)) {
106            throw new MathIllegalArgumentException(LocalizedFormats.INVALID_FIXED_LENGTH_CHROMOSOME);
107        }
108        return mate((AbstractListChromosome<T>) first, (AbstractListChromosome<T>) second);
109    }
110
111    /**
112     * Helper for {@link #crossover(Chromosome, Chromosome)}. Performs the actual crossover.
113     *
114     * @param first the first chromosome
115     * @param second the second chromosome
116     * @return the pair of new chromosomes that resulted from the crossover
117     * @throws DimensionMismatchException if the length of the two chromosomes is different
118     */
119    protected ChromosomePair mate(final AbstractListChromosome<T> first, final AbstractListChromosome<T> second)
120        throws DimensionMismatchException {
121
122        final int length = first.getLength();
123        if (length != second.getLength()) {
124            throw new DimensionMismatchException(second.getLength(), length);
125        }
126
127        // array representations of the parents
128        final List<T> parent1Rep = first.getRepresentation();
129        final List<T> parent2Rep = second.getRepresentation();
130        // and of the children: do a crossover copy to simplify the later processing
131        final List<T> child1Rep = new ArrayList<>(second.getRepresentation());
132        final List<T> child2Rep = new ArrayList<>(first.getRepresentation());
133
134        // the set of all visited indices so far
135        final Set<Integer> visitedIndices = new HashSet<>(length);
136        // the indices of the current cycle
137        final List<Integer> indices = new ArrayList<>(length);
138
139        // determine the starting index
140        int idx = randomStart ? GeneticAlgorithm.getRandomGenerator().nextInt(length) : 0;
141        int cycle = 1;
142
143        while (visitedIndices.size() < length) {
144            indices.add(idx);
145
146            T item = parent2Rep.get(idx);
147            idx = parent1Rep.indexOf(item);
148
149            while (idx != indices.get(0)) {
150                // add that index to the cycle indices
151                indices.add(idx);
152                // get the item in the second parent at that index
153                item = parent2Rep.get(idx);
154                // get the index of that item in the first parent
155                idx = parent1Rep.indexOf(item);
156            }
157
158            // for even cycles: swap the child elements on the indices found in this cycle
159            if (cycle++ % 2 != 0) {
160                for (int i : indices) {
161                    T tmp = child1Rep.get(i);
162                    child1Rep.set(i, child2Rep.get(i));
163                    child2Rep.set(i, tmp);
164                }
165            }
166
167            visitedIndices.addAll(indices);
168            // find next starting index: last one + 1 until we find an unvisited index
169            idx = (indices.get(0) + 1) % length;
170            while (visitedIndices.contains(idx) && visitedIndices.size() < length) {
171                idx++;
172                if (idx >= length) {
173                    idx = 0;
174                }
175            }
176            indices.clear();
177        }
178
179        return new ChromosomePair(first.newFixedLengthChromosome(child1Rep),
180                                  second.newFixedLengthChromosome(child2Rep));
181    }
182}