### Random 1-1 mapping

by tomazos (Deacon)
 on May 28, 2006 at 17:56 UTC Need Help??

tomazos has asked for the wisdom of the Perl Monks concerning the following question:

I'm trying to write a simple shuffling function...

```sub shuffle(\$\$\$) {
my (\$seed, \$max, \$input) = @_;

# \$max > \$input
# \$max > 0
# \$input >= 0

my \$result;

# calculate \$result

# \$result >= 0
# \$result < \$max

return \$result
}

...such that shuffle(\$K1, \$K2, \$x) == shuffle(\$K1, \$K2, \$y) if and only if \$x == \$y.

shuffle returns a semi-random number.

This could be used to iterate randomly over a read-only list in-place.

ie

```get_rand(\@\$\$) {
my (\$list, \$seed, \$input) = @_;
return \$list->[shuffle(\$seed, scalar(@\$list), \$input)];
}

my \$list = [10, 20, 30, 40, 50, 60];

for (my \$i = 0; \$i < scalar(@\$list); \$i++) {
print get_rand(\$list, 42, \$i) . ' ';
}

# might print 40 10 20 60 30 50

Any math geniuses out there care to fill in the calculate \$result part? I'm sure it has something to do with moding and hashing and stuff.

(No, this isn't a homework question. :) I actually need it to iterate randomly over a list in a resource-constrained embedded environment where I can't move the memory around - and haven't got spare memory. I knew I shouldn't have slept through my CS lectures.)

-Andrew.

Replies are listed 'Best First'.
Re: Random 1-1 mapping
by Zaxo (Archbishop) on May 28, 2006 at 18:10 UTC

Since an array has the factorial of its element count possible orders, your \$input must have that range to obtain a 1-1 relation. Are you sure that so stringent a requirement is needed?

Check out List::Util::shuffle(). It may be good enough for what you want.

You won't want to keep C++ comments in your perl, and prototypes are usually not desirable. See perltrap.

After Compline,
Zaxo

Fixed comments. Been writing in both languages and can now write in neither.

\$input is just a scalar. It has the range 0..\$max. I don't see where a factorial of the count comes in.

The idea is:

```shuffle(42, 10, 0) == 6
shuffle(42, 10, 1) == 1
shuffle(42, 10, 2) == 5
shuffle(42, 10, 3) == 8
shuffle(42, 10, 4) == 3
shuffle(42, 10, 5) == 4
shuffle(42, 10, 6) == 9
shuffle(42, 10, 7) == 2
shuffle(42, 10, 8) == 7
shuffle(42, 10, 9) == 0

?

-Andrew.

Re: Random 1-1 mapping
by QM (Parson) on May 28, 2006 at 18:56 UTC
If you want to walk the list in a random order ("without replacement" in the vernacular), then you need something to maintain state, and be aware of the size of your list (both initially and at each call).

Your claims of no spare memory make this difficult. Either you need a scheme that implicitly keeps track of which elements have already been returned, or you need to explicitly keep track of them.

On the explicit front, you can keep a record in a bit vector, with one bit for each element already used. However, to get the last element of a large list, using a PRNG will take a long time.

On the implicit front, you need an iterator thats reasonably random, yet is exactly uniformly distributed regardless of the size of your list. This makes me think of a prime modulo operation:

```#!/your/perl/here
use strict;
use warnings;

{ # closure for "static" variables
my \$mult   = 2**15-1;
my \$offset = 76543;
my \$state  = 0;

sub get_rand
{
my \$list = shift; # ref
\$state = (\$state*\$mult+\$offset)%scalar(@{\$list});
return \$list->[\$state];
}

sub init_get_rand
{
my \$list = shift;
\$state = rand(scalar(@{\$list}));
}
}

my \$list = [0..17];

for (1..2)
{
init_get_rand(\$list);
for ( 1..@{\$list} )
{
print get_rand(\$list), " ";
}
print "\n";
}

__OUTPUT__
8 9 16 11 12 1 14 15 4 17 0 7 2 3 10 5 6 13
2 3 10 5 6 13 8 9 16 11 12 1 14 15 4 17 0 7
Now, I'll be the first to admit I haven't checked on good values for \$mult, \$offset, or whether lists of certain sizes will cause you to miss values, etc. But you should be able to look up a good PRNG on your own ;)

-QM
--
Quantum Mechanics: The dreams stuff is made of

I've possibly been confusing with the use of the word iterate. The list is actually directly accessible. I can access element x cheaply and statelessly - the records are fixed sized and contiguous.

I just want to walk this list in a well-defined, reproducible, random-order.

I think multiplying by a prime and then dividing by the maximum has something to do with producing the 1-1 mapping (was it RSA or abstract algebra / group/ring theory - I'm too old for this sh*t).

-Andrew.

2) You need an offset if zero is involved.

3) Yes, multiplying by a prime mod some other prime will do what you want. There are probably several other configurations that will do the same, and some will be better than what I've given. There are numerous approaches, rules, tests, and other info that you can dig up. I'd start with Mathworld or Wikipedia.

-QM
--
Quantum Mechanics: The dreams stuff is made of

Re: Random 1-1 mapping
by BerntB (Deacon) on May 28, 2006 at 18:44 UTC
What you want is a perfect hash function, that is inexpensive in computing time?

Why not ask for something simple, like a perpetum mobile? :-)

The \$seed \$input is the place in the original list? Maybe permutation encryption algorithms? But to keep them below \$max...?

Update: If you could keep \$max to a power of 2, you could maybe XOR with a constant? (Update2: The \$seed (constant) would also have to be as many bits as \$max.)

Code example for Update (n.b. \$max must be 2^X):

```my \$max  = 8;
my \$list = [10, 20, 30, 40, 50, 60, 70, 80];

for(my \$seed = 0; \$seed < \$max; \$seed++) {
print "With seed \$seed: ";
for(my \$i = 0; \$i < @\$list; \$i++) {
my \$place = get_rand(\$i, \$seed);
print "\$place:", \$list->[\$place], "  ";
}
print "\n";
}

sub get_rand {
my(\$item, \$seed) = @_;

return \$item ^ \$seed;
}

__END__

# -------------------------------------------
# output:
With seed 0: 0:10  1:20  2:30  3:40  4:50  5:60  6:70  7:80
With seed 1: 1:20  0:10  3:40  2:30  5:60  4:50  7:80  6:70
With seed 2: 2:30  3:40  0:10  1:20  6:70  7:80  4:50  5:60
With seed 3: 3:40  2:30  1:20  0:10  7:80  6:70  5:60  4:50
With seed 4: 4:50  5:60  6:70  7:80  0:10  1:20  2:30  3:40
With seed 5: 5:60  4:50  7:80  6:70  1:20  0:10  3:40  2:30
With seed 6: 6:70  7:80  4:50  5:60  2:30  3:40  0:10  1:20
With seed 7: 7:80  6:70  5:60  4:50  3:40  2:30  1:20  0:10
Update: Your solution is cool but \$max is not guaranteed to be a power of 2. In fact, it almost certainly isn't.

I just scribbled this on a piece of paper, I think I heard something about it to do with the RSA encryption algorithm once...

```\$result = (\$prime_number * \$input + \$seed) % \$max;

Example for \$max = 10, \$prime_number = 7 and \$seed = 5..

```shuffle(5,10,0) == (7 * 0 + 5) % 10 == 5 % 10 == 5
shuffle(5,10,1) == (7 * 1 + 5) % 10 == 12 % 10 == 2
shuffle(5,10,2) == (7 * 2 + 5) % 10 == 19 % 10 == 9
shuffle(5,10,3) == (7 * 3 + 5) % 10 == 26 % 10 == 6
shuffle(5,10,4) == (7 * 4 + 5) % 10 == 33 % 10 == 3
shuffle(5,10,5) == (7 * 5 + 5) % 10 == 40 % 10 == 0
shuffle(5,10,6) == (7 * 6 + 5) % 10 == 47 % 10 == 7
shuffle(5,10,7) == (7 * 7 + 5) % 10 == 54 % 10 == 4
shuffle(5,10,8) == (7 * 8 + 5) % 10 == 61 % 10 == 1
shuffle(5,10,9) == (7 * 9 + 5) % 10 == 68 % 10 == 8

So that works for 7 and 10. Does it work for any \$max and any \$prime_number? If not what conditions will it work for? I am no good with proofs.

-Andrew.

Andrew Tomazos  |  andrew@tomazos.com  |  www.tomazos.com
A mapping of the form
```f(x) = (a*x + b) % m
is 1-to-1 (restricted to 0 <= x < m) as long as gcd(a,m)==1, since that's the only time you can find an inverse to a mod m, and invert the function.

I would suggest splitting your "seed" value into the a & b coefficients in the following way:

• a = largest number less than seed satisfying gcd(a,m)=1
• b = seed - a
```sub gcd { \$_[1] ? gcd(\$_[1], \$_[0] % \$_[1]) : \$_[0] }

sub shuffle {
my (\$seed, \$max, \$i) = @_;

my \$ca = \$seed;
\$ca-- until gcd(\$ca, \$max) == 1;
my \$cb = \$seed - \$ca;

(\$ca * \$i + \$cb) % \$max;
}

for my \$seed (1 .. 10) {
my @result = map shuffle(\$seed, 15, \$_), 0 .. 14;
print "seed=\$seed ==> @result\n";
}
But take this approach for what it's worth -- The only kinds of mapping you'll get by this process are simple linear (affine) mappings, which may not "look random enough":
```seed=1 ==> 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
seed=2 ==> 0 2 4 6 8 10 12 14 1 3 5 7 9 11 13
seed=3 ==> 1 3 5 7 9 11 13 0 2 4 6 8 10 12 14
seed=4 ==> 0 4 8 12 1 5 9 13 2 6 10 14 3 7 11
seed=5 ==> 1 5 9 13 2 6 10 14 3 7 11 0 4 8 12
seed=6 ==> 2 6 10 14 3 7 11 0 4 8 12 1 5 9 13
seed=7 ==> 0 7 14 6 13 5 12 4 11 3 10 2 9 1 8
seed=8 ==> 0 8 1 9 2 10 3 11 4 12 5 13 6 14 7
seed=9 ==> 1 9 2 10 3 11 4 12 5 13 6 14 7 0 8
seed=10 ==> 2 10 3 11 4 12 5 13 6 14 7 0 8 1 9
For more "unpredictable" orders, you could have more tools at your disposal if \$max is always a prime. Then you take one of the linear sequences above and use it as a sequence of powers of a generator element for the field mod \$max.

Re: Random 1-1 mapping
by TedPride (Priest) on May 28, 2006 at 23:33 UTC
As stated above, you're good to go as long as max % prime != 0:
```use strict;
use warnings;

my (\$seed, \$prime, \$max, \$input, \$result);

\$seed = 8;
for \$prime (2,3,5,7,11,13,17,19,23) {
for \$max (20..30) {
next if \$max % \$prime == 0;
my %hash;
for \$input (0..(\$max-1)) {
\$result = (\$prime * \$input + \$seed) % \$max;
\$hash{\$result}++;
}
print "\$prime \$max : ";
print join '', values %hash, "\n";
}
}
All you need to guarantee this is a sufficiently large prime so that it's always going to be larger than sqrt(max) (but not equal to max itself).
```use strict;
use warnings;

my (@arr, \$seed, \$max, \$prime, \$input, \$result);

@arr = 1..10;
\$seed = 5;

\$max = \$#arr+1;
\$prime = 65521;

for \$input (0..(\$max-1)) {
\$result = (\$prime * \$input + \$seed) % \$max;
print \$arr[\$result], " ";
}
The hard part though, since I assume you're doing this in assembly or a low level language, will be making sure that none of the basic mathematical operations go out of bounds.

Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://552199]
Approved by Zaxo
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others pondering the Monastery: (3)
As of 2023-09-29 01:07 GMT
Sections?
Information?
Find Nodes?
Leftovers?
Voting Booth?

No recent polls found

Notices?