1 /**
2  * ae.utils.math.combinatorics
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <ae@cy.md>
12  */
13 
14 module ae.utils.math.combinatorics;
15 
16 /// https://en.wikipedia.org/wiki/Binomial_coefficient
17 /// This implementation is non-recursive but overflows easily
18 R binomialCoefficient(T, R=T)(T n, T k)
19 {
20 	if (n < k)
21 		return R(0);
22 	R result = 1;
23 	foreach (i; n - k + 1 .. n + 1)
24 		result *= i;
25 	foreach (i; 1 .. k + 1)
26 		result /= i;
27 	return result;
28 }
29 
30 unittest
31 {
32 	assert(binomialCoefficient(3067L, 3) == 4803581405);
33 }
34 
35 unittest
36 {
37 	import std.bigint : BigInt;
38 	assert(binomialCoefficient!(int, BigInt)(3067, 3) == 4803581405);
39 }
40 
41 /// https://en.wikipedia.org/wiki/Multiset#Counting_multisets
42 R multisetCoefficient(T, R=T)(T n, T k)
43 {
44 	return binomialCoefficient!(T, R)(n + k - 1, k);
45 }
46 
47 unittest
48 {
49 	assert(multisetCoefficient(3067L, 3) == 4812987894);
50 }
51 
52 /// Precalculated binomial coefficient table
53 struct BinomialCoefficientTable(T, T maxN, T maxK)
54 {
55 	static assert(maxN >= 1);
56 	static assert(maxK >= 1);
57 
58 	T[maxK + 1][maxN + 1] table;
59 
60 	static typeof(this) generate()
61 	{
62 		typeof(this) result;
63 		result.table[0][0] = 1;
64 		foreach (size_t n; 1 .. maxN + 1)
65 		{
66 			result.table[n][0] = 1;
67 			foreach (size_t k; 1 .. maxK + 1)
68 				result.table[n][k] = cast(T)(result.table[n-1][k-1] + result.table[n-1][k]);
69 		}
70 		return result;
71 	}
72 
73 	T binomialCoefficient(size_t n, size_t k) const
74 	{
75 		return table[n][k];
76 	}
77 
78 	T multisetCoefficient(size_t n, size_t k) const
79 	{
80 		return binomialCoefficient(n + k - 1, k);
81 	}
82 }
83 
84 unittest
85 {
86 	assert(BinomialCoefficientTable!(ulong, 5000, 3).generate().binomialCoefficient(3067, 3) == 4803581405);
87 }
88 
89 /// Combinatorial number system encoder/decoder
90 /// https://en.wikipedia.org/wiki/Combinatorial_number_system
91 struct CNS(
92 	/// Type for packed representation
93 	P,
94 	/// Type for one position in unpacked representation
95 	U,
96 	/// Number of positions in unpacked representation
97 	size_t N,
98 	/// Cardinality (maximum value plus one) of one position in unpacked representation
99 	U unpackedCard,
100 	/// Produce lexicographic ordering?
101 	bool lexicographic,
102 	/// Are repetitions representable? (multiset support)
103 	bool multiset,
104 	/// Binomial coefficient calculator implementation
105 	/// (You can supply a precalculated BinomialCoefficientTable here)
106 	alias binomialCalculator = ae.utils.math.combinatorics,
107 )
108 {
109 static:
110 	/// Cardinality (maximum value plus one) of the packed representation
111 	static if (multiset)
112 		enum P packedCard = multisetCoefficient(unpackedCard, N);
113 	else
114 		enum P packedCard = binomialCoefficient(unpackedCard, N);
115 	alias Index = P;
116 
117 	private P summand(U value, Index i)
118 	{
119 		static if (lexicographic)
120 		{
121 			value = cast(U)(unpackedCard-1 - value);
122 			i = cast(Index)(N-1 - i);
123 		}
124 		static if (multiset)
125 			value += i;
126 		return binomialCalculator.binomialCoefficient(value, i + 1);
127 	}
128 
129 	P pack(U[N] values)
130 	{
131 		P packed = 0;
132 		foreach (Index i, value; values)
133 		{
134 			static if (!multiset)
135 				assert(i == 0 || value > values[i-1]);
136 			else
137 				assert(i == 0 || value >= values[i-1]);
138 
139 			packed += summand(value, i);
140 		}
141 
142 		static if (lexicographic)
143 			packed = packedCard-1 - packed;
144 		return packed;
145 	}
146 
147 	U[N] unpack(P packed) @nogc
148 	{
149 		import std.algorithm.iteration : map;
150 		import std.range : iota, retro, assumeSorted, SearchPolicy;
151 
152 		import ae.utils.meta : I;
153 
154 		static if (lexicographic)
155 			packed = packedCard-1 - packed;
156 
157 		// We make i a compile-time parameter here to avoid
158 		// allocating a closure for std.algorithm.map
159 		void unpackOne(Index i)(ref U r) @nogc
160 		{
161 			static if (lexicographic)
162 				enum lastValue = 0;
163 			else
164 				enum lastValue = unpackedCard - 1;
165 
166 			bool checkValue(U value, U nextValue)
167 			{
168 				if (value == lastValue || summand(nextValue, i) > packed)
169 				{
170 					r = value;
171 					packed -= summand(value, i);
172 					return true;
173 				}
174 				return false;
175 			}
176 
177 			static if (lexicographic)
178 			{
179 				r = unpackedCard
180 					.iota
181 					.map!(value => summand(value, i))
182 					.retro
183 					.assumeSorted
184 					.lowerBound!(SearchPolicy.binarySearch)(packed + 1)
185 					.length
186 					.I!(l => cast(U)(unpackedCard - l));
187 				packed -= summand(r, i);
188 			}
189 			else
190 			{
191 				r = unpackedCard
192 					.iota
193 					.map!(value => summand(value, i))
194 					.assumeSorted
195 					.lowerBound!(SearchPolicy.binarySearch)(packed + 1)
196 					.length
197 					.I!(l => cast(U)(l - 1));
198 				packed -= summand(r, i);
199 			}
200 		}
201 
202 		U[N] values;
203 		static if (lexicographic)
204 			static foreach         (Index i; 0 .. N)
205 				unpackOne!i(values[i]);
206 		else
207 			static foreach_reverse (Index i; 0 .. N)
208 				unpackOne!i(values[i]);
209 
210 		return values;
211 	}
212 }
213 
214 static if (__VERSION__ >= 2_096)
215 unittest
216 {
217 	enum N = 3;
218 	enum cardinality = 10;
219 
220 	static foreach (lexicographic; [false, true])
221 	static foreach (multiset; [false, true])
222 	static foreach (precalculate; [false, true])
223 	{{
224 		static if (precalculate)
225 			static immutable calculator = BinomialCoefficientTable!(uint, cardinality + 1, N).generate();
226 		else
227 			alias calculator = ae.utils.math.combinatorics;
228 		alias testCNS = CNS!(uint, ubyte, N, cardinality, lexicographic, multiset, calculator);
229 
230 		uint counter;
231 		foreach (ubyte a; 0 .. cardinality)
232 		foreach (ubyte b; cast(ubyte)(a + (multiset ? 0 : 1)) .. cardinality)
233 		foreach (ubyte c; cast(ubyte)(b + (multiset ? 0 : 1)) .. cardinality)
234 		{
235 			ubyte[N] input = [a, b, c];
236 			auto packed = testCNS.pack(input);
237 			assert(packed < testCNS.packedCard);
238 			static if (lexicographic)
239 				assert(counter == packed, "Packed is wrong");
240 			auto unpacked = testCNS.unpack(packed);
241 			assert(input == unpacked, "Unpacked is wrong");
242 			counter++;
243 		}
244 	}}
245 }