Unique Random Iterator
On quite some occasions I have been needing to iterate a range in a random order, without visiting the same position twice.
1. Naive Attempt
I think that the usual approach involves creating an array with all positions, and then shuffling it. It ensures that each position is only iterated once, and you can easily rely on your standard library which should provide you with a good shuffle.
// initialize values in range - O(n) memory and complexity int range = 10000; std::vector v; v.reserve(range); for (auto i = range; i >= 0; --i) { v.push_back(i); } // shuffle all positions - at least O(n) complexity std::shuffle(v.begin(), v.end(), g); // example usage for (auto &position : v) { if (is_suitable(position)) { return position; } }
However, this involves unnecessarily allocating a lot of memory for big ranges, especially if you expect to only need a small portion of the range.
2. Prime Addition
Iterating over a range in a random fashion can be done adding an arbitrary prime bigger than the array size to the current position and using modulo on the result. This is a known trick, however finding a good prime is not easy.
A is the array S is the size of the array Generate a random prime number (P), such that P > S Q = P % S for i = 1 to S process A[Q] Q = (Q + P) % S
3. LCG Based Random Iterator
I have been implementing a LCG wrapped as an interface to iterate over a specified integer range. We make use of its unique properties respecting the Hull-Dobell Theorem (Hull, T. E. and Dobell, A. R., 1962) to find a suitable value for our modulo value. Note that due to its nature, the generator will alternate between odd and even numbers. If we can accept the inferior results of a LCG, we can reduce the complexity and completely avoid the allocation of an array.
#include <cstdint> inline constexpr uint64_t next_pow2_64(uint64_t i) { --i; i |= i >> 1; i |= i >> 2; i |= i >> 4; i |= i >> 8; i |= i >> 16; i |= i >> 32; return ++i; } /* * LCG based random iterator implementation * Requirements (Hull-Dobell Theorem): * 1) mod and incr are relatively prime * 2) mult-1 is divisible by all prime factors of range * 3) mult-1 is divisible by 4 if mod is divisible by 4 * We round up to the next power of two for mod to guarantee 2) and 3). * Outputs bigger than range (<50%) are discarded. */ class UniqueRandomIterator { private: uint64_t state, count; const uint64_t mult, incr, mod_mask, range, offset; public: inline UniqueRandomIterator( uint64_t min, uint64_t max, uint64_t seed = 0xC789F4F722444853, uint64_t stream = 0x5084B57B66D07A23, uint64_t mult = 0xC360248E09A02DF5 ) : mult(mult), incr((stream << 1) | 1) , range(max - min + 1) , offset(min), count(0) , mod_mask(next_pow2_64(max - min + 1) - 1) { state = seed % range; } inline uint64_t next() { do { state = (state * mult + incr) & mod_mask; } while (state >= range); if (++count > range) [[unlikely]] { count = 1; } return state + offset; } inline uint64_t current() const { return state + offset; } inline uint64_t left() const { return range - count; } inline bool next_available() const { return left() > 0; } };
We have been avoiding modulo by using a power of two for our LCG modulo value, which allows us to use a bit-mask instead of an expensive division. Luckily this fits well with our requirements. By using a base offset, we support arbitrary integer ranges. Values that fall out of our power of two modulo range are discarded, and are less than 50% even in the worst case by definition.
3.1. Example Usage
You only need to specify your range, and an optional seed as third parameter. In this example we just print out our positions line by line.
#include <cstdio> int main() { UniqueRandomIterator it(1, 10); while (it.next_available()) { printf("%i\n", it.next()); } return 0; }
3.2. Example Output
While the approach shines for huge ranges that are not processed in whole, for showcase I have set the range from one to ten.
5 9 3 2 4 7 6 1 8 10
3.3. Generated Assembly
This is using x86-64 GCC 13.2 using -O3
. You can see that the resulting
assembly is very compact and basic.
.LC0: .string "%i\n" main: push r13 mov r13d, 10 push r12 movabs r12, -4368451446083932683 push rbp movabs rbp, -6842820550568381369 push rbx mov ebx, 9 sub rsp, 8 .L2: imul rbx, r12 add rbx, rbp and ebx, 15 cmp rbx, 9 ja .L2 xor eax, eax lea rsi, [rbx+1] mov edi, OFFSET FLAT:.LC0 call printf sub r13, 1 jne .L2 add rsp, 8 xor eax, eax pop rbx pop rbp pop r12 pop r13 ret
4. Extension for 2D Ranges
By wrapping our iterator, we can split the range into a two dimensional area. This is useful, if for example, you need to iterate over a set of cells or tiles in a random fashion.
struct UniqueRandomIterator2DPos { uint64_t x, y; }; class UniqueRandomIterator2D : public UniqueRandomIterator { private: const uint64_t offset_x; const uint64_t size_x; public: UniqueRandomIterator2D( uint64_t x1, uint64_t x2, uint64_t y1, uint64_t y2, uint64_t seed, uint64_t stream = 0x5084B57B66D07A23 ) : UniqueRandomIterator( (x2 - x1 + 1) * y1, (x2 - x1 + 1) * (y2 + 1) - 1, seed, stream) , offset_x(x1), size_x(x2 - x1 + 1) {} auto to_pos(const uint64_t state) const { return UniqueRandomIterator2DPos { .x = (state % size_x) + offset_x, .y = (state / size_x), }; } auto current_pos() const { return to_pos(current()); } auto next_pos() { return to_pos(next()); } };
Figure 1: Visualization
5. Extension for 3D Ranges
For completion, the same can be done for three dimensions. Feel free to write your own n-dimensional template implementation.
struct UniqueRandomIterator3DPos { uint64_t x, y, z; }; class UniqueRandomIterator3D : public UniqueRandomIterator { private: const uint64_t offset_x, offset_y; const uint64_t size_x, size_y; public: UniqueRandomIterator3D( uint64_t x1, uint64_t x2, uint64_t y1, uint64_t y2, uint64_t z1, uint64_t z2, uint64_t seed, uint64_t stream = 0x5084B57B66D07A23 ) : UniqueRandomIterator( (x2 - x1 + 1) * (y2 - y1 + 1) * z1, (x2 - x1 + 1) * (y2 - y1 + 1) * (z2 + 1) - 1, seed, stream) , offset_x(x1), offset_y(y1) , size_x(x2 - x1 + 1), size_y(y2 - y1 + 1) {} auto to_pos(uint64_t state) const { return UniqueRandomIterator3DPos { .x = ((state % size_x) + offset_x), .y = ((state / size_x) % size_y) + offset_y, .z = ((state / size_x) / size_y), }; } auto current_pos() const { return to_pos(current()); } auto next_pos() { return to_pos(next()); } };