001/*
002 * Java Genetic Algorithm Library (jenetics-8.0.0).
003 * Copyright (c) 2007-2024 Franz Wilhelmstötter
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * 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 *
017 * Author:
018 *    Franz Wilhelmstötter (franz.wilhelmstoetter@gmail.com)
019 */
020package io.jenetics.internal.math;
021
022import static java.lang.String.format;
023import static java.util.Objects.requireNonNull;
024import static io.jenetics.internal.math.Basics.isMultiplicationSave;
025
026import java.util.Arrays;
027import java.util.random.RandomGenerator;
028
029/**
030 * This class creates random subsets of size {@code k}  from a set of {@code n}
031 * elements. Implementation of the {@code RANKSB} algorithm described by
032 * <em>Albert Nijenhuis</em> and <em>Herbert Wilf</em> in <b>Combinatorial
033 * Algorithms for Computers and Calculators</b>
034 * <p>
035 *  Reference:<em>
036 *      Albert Nijenhuis, Herbert Wilf,
037 *      Combinatorial Algorithms for Computers and Calculators,
038 *      Second Edition,
039 *      Academic Press, 1978,
040 *      ISBN: 0-12-519260-6,
041 *      LC: QA164.N54.
042 *      Page: 42</em>
043 *
044 * @author <a href="mailto:franz.wilhelmstoetter@gmail.com">Franz Wilhelmstötter</a>
045 * @version 7.0
046 * @since 4.0
047 */
048public final class Subset {
049        private Subset() {}
050
051        /**
052         * Selects a random subset of size {@code k} from a set of size {@code n}.
053         *
054         * @param n the size of the set.
055         * @param k the size of the subset.
056         * @param rnd the random number generator used.
057         * @throws NullPointerException if {@code random} is {@code null}.
058         * @throws IllegalArgumentException if {@code n < k}, {@code k == 0} or if
059         *         {@code n*k} will cause an integer overflow.
060         * @return the a-set array for the given parameter. The returned subset
061         *         array is sorted in increasing order.
062         */
063        public static int[] next(final RandomGenerator rnd, final int n, final int k) {
064                requireNonNull(rnd);
065
066                final var subset = new int[k];
067                next(rnd, n, subset);
068                return subset;
069        }
070
071        private static void next(
072                final RandomGenerator rnd,
073                final int n,
074                final int[] a
075        ) {
076                requireNonNull(rnd, "Random");
077                requireNonNull(a, "Sub set array");
078
079                final int k = a.length;
080                checkSubSet(n, k);
081
082                // Early return if k == n.
083                if (k == n) {
084                        for (int i = 0; i < k; ++i) {
085                                a[i] = i;
086                        }
087                        return;
088                }
089
090                // Calculate the 'inverse' subset if k > n - k.
091                if (k > n - k) {
092                        subset0(rnd, n, n - k, a);
093                        invert(n, a);
094                } else {
095                        subset0(rnd, n, k, a);
096                }
097        }
098
099        /*
100         * "Inverts" the given subset array `a`. The first n - k elements represent
101         * the set, which must not be part of the "inverted" subset. This is done by
102         * filling the array from the back, starting with the highest possible element,
103         * which is not part of the "forbidden" subset elements. The result is a
104         * subset array, filled with elements, which is not part of the original
105         * "forbidden" subset.
106         */
107        private static void invert(
108                final int n,
109                final int[] a
110        ) {
111                final int k = a.length;
112
113                int v = n - 1;
114                int j = n - k - 1;
115                int vi;
116
117                final int[] ac = Arrays.copyOfRange(a, 0, n - k);
118                for (int i = k; --i >= 0;) {
119                        while ((vi = indexOf(ac, j, v)) != -1) {
120                                --v;
121                                j = vi;
122                        }
123
124                        a[i] = v--;
125                }
126        }
127
128        private static int indexOf(final int[] a, final int start, final int value) {
129                for (int i = start; i >= 0; --i) {
130                        if (a[i] < value) {
131                                return -1;
132                        } else if (a[i] == value) {
133                                return i;
134                        }
135                }
136
137                return -1;
138        }
139
140        // The actual implementation of the `RANKSB` algorithm.
141        private static void subset0(
142                final RandomGenerator random,
143                final int n,
144                final int k,
145                final int[] a
146        ) {
147                assert k <= a.length;
148                assert k <= n - k;
149
150                // Early return if k = 1.
151                if (k == 1) {
152                        a[0] = random.nextInt(n);
153                        return;
154                }
155
156                // (A): Initialize a[i] to "zero" point for bin Ri.
157                for (int i = 0; i < k; ++i) {
158                        a[i] = (i*n)/k;
159                }
160
161                // (B)
162                int l, x;
163                for (int c = 0; c < k; ++c) {
164                        do {
165                                // Choose random x;
166                                x = 1 + random.nextInt(n);
167
168                                // determine range Rl;
169                                l = (x*k - 1)/n;
170                        } while (a[l] >= x); // accept or reject.
171
172                        ++a[l];
173                }
174                int s = k;
175
176                // (C) Move a[i] of nonempty bins to the left.
177                int m = 0, p = 0;
178                for (int i = 0; i < k; ++i) {
179                        if (a[i] == (i*n)/k) {
180                                a[i] = 0;
181                        } else {
182                                ++p;
183                                m = a[i];
184                                a[i] = 0;
185                                a[p - 1] = m;
186                        }
187                }
188
189                // (D) Determine l, set up space for Bl.
190                for (; p > 0; --p) {
191                        l = 1 + (a[p - 1]*k - 1)/n;
192                        final int ds = a[p - 1] - ((l - 1)*n)/k;
193                        a[p - 1] = 0;
194                        a[s - 1] = l;
195                        s -= ds;
196                }
197
198                // (E) If a[l] != 0, a new bin is to be processed.
199                int r = 0, m0 = 0;
200                for (int ll = 1; ll <= k; ++ll) {
201                        l = k + 1 - ll;
202
203                        if (a[l - 1] != 0) {
204                                r = l;
205                                m0 = 1 + ((a[l - 1] - 1)*n)/k;
206                                m = (a[l - 1]*n)/k - m0 + 1;
207                        }
208
209                        // (F) Choose a random x.
210                        x = m0 + random.nextInt(m);
211                        int i = l + 1;
212
213                        // (G) Check x against previously entered elements in bin;
214                        //     increment x as it jumps over elements <= x.
215                        while (i <= r && x >= a[i - 1]) {
216                                ++x;
217                                a[i - 2] = a[i - 1];
218                                ++i;
219                        }
220
221                        a[i - 2] = x;
222                        --m;
223                }
224
225                // Convert to zero-based indexed arrays.
226                for (int i = 0; i < k; ++i) {
227                        --a[i];
228                }
229        }
230
231
232        private static void checkSubSet(final int n, final int k) {
233                if (k <= 0) {
234                        throw new IllegalArgumentException(format(
235                                "Subset size smaller or equal zero: %s", k
236                        ));
237                }
238                if (n < k) {
239                        throw new IllegalArgumentException(format(
240                                "n smaller than k: %s < %s.", n, k
241                        ));
242                }
243                if (!isMultiplicationSave(n, k)) {
244                        throw new IllegalArgumentException(format(
245                                "n*sub.length > Integer.MAX_VALUE (%s*%s = %s > %s)",
246                                n, k, (long)n*(long)k, Integer.MAX_VALUE
247                        ));
248                }
249        }
250
251}