xmoonlight
@xmoonlight
https://sitecoder.blogspot.com

Как перебрать все элементы множества в псевдо-произвольном порядке?

Есть произвольный пронумерованный набор элементов.
Например: 1,2,3,4,5
Как получить формулу для перебора всех элементов строго по одному разу (как-бы рандомно) и (по-возможности) с разными последовательностями с помощью управляющего коэффициента.

Например:
f(1) = {3,5,2,1,4}
f(2) = {5,3,1,4,2}
и т.д.

UPD: брут или стэк (достаём до опустошения) - не нужны!
Нужна именно формула (или где почитать), меняющая позиции элементов в зависимости от поданного на вход коэффициента.

UPD2: нашёл только лекцию по лексографическим перестановкам, но это совсем не то, что мне требуется.

PS: подобный вопрос - здесь.
  • Вопрос задан
  • 546 просмотров
Решения вопроса 3
sergiks
@sergiks Куратор тега Алгоритмы
♬♬
Когда длина набора кратна степеням двойки можно перемешивать биты, адресующие элемент.
В рамках одного порядка перемешивания битов адреса, все множество элементов однозначно («биективно») отображается в такое же по длине «перемешанное» множество.

Вот пример кода:
const src = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; // массив элементов
const key = [2, 1, 0]; // номера битов для перемешивания адреса

const mapIndex = (n, key) => key.reduce((p,c,i) => p | ((1 << c & n) >> c) << i, 0);
const biject = (arr, key) => arr.map((e,i) => arr[mapIndex(i, key)]);

console.log( JSON.stringify(biject(src, key)));
// ["a","e","c","g","b","f","d","h"] 

console.log( JSON.stringify(biject(src, [1,2,0])));
// ["a","e","b","f","c","g","d","h"]


Disclaimer. Этот способ не исчерпывает все возможные перестановки, ограничиваясь их небольшой частью. Первый и последний всегда остаются на своих местах (все "0" как ни перемешивай..)

Upd. отличный способ подсказали на SO. Когда длина массива N, нужно взять любое простое число больше N. Оно гарантированно будет взаимнопростым с любым из чисел меньше.
Новый адрес элемента получается из старого: адрес умножить на простое и взять остаток от деления на длину.
{
const src = 'abcdefgh'.split('');
const N = src.length; // 8
const Prime = 37;  // Prime > N
const mapIndex = (i, N, Prime) => (i * Prime) % N;
//for(let i  = 0; i< 8; i++) console.log(i, mapIndex(i, N, Prime))
const shuffle = (str, Prime) => str.split('').map((e,i,a) => a[mapIndex(i, a.length, Prime)]).join('');

console.log( shuffle('abcdefgh', 17));
console.log( shuffle('abcdefgh', 19));
console.log( shuffle('abcdefgh', 23));
console.log( shuffle('abcdefgh', 29));

abcdefgh  // не перемешалось
adgbehcf
ahgfedcb
afchebgd

Для некоторых простых чисел почему-то не происходит перемешивания: для 17, 37 при длине массива 8.
Ответ написан
longclaps
@longclaps
function shuffle(str, seed) {
    let arr = str.split(''), j;
    for (let i = 0; i < arr.length; i++) {
        // так называемый минимальный стандартный генератор случайных чисел
        // https://ru.wikipedia.org/wiki/Линейный_конгруэнтный_метод
        // аккуратно, с нуля он не стартует )
        j = (seed = seed * 16807 % 2147483647) % arr.length;
        [arr[i], arr[j]] = [arr[j], arr[i]];
    }
    return arr.join('');
}

console.log(shuffle('abcdefgh', 42));
Ответ написан
@Sumor
Небольшой код для перемешивания с помощью порождающего элемента.
Математическое обоснование основано на теории групп.
Кратко:
В конечном поле, мощностью равному простому числу p есть мультипликативная группа, которая включает в себя все элементы кроме нуля.
В этой мультипликативной группе обязательно есть порождающий элемент.
Что такое порождающий элемент? Это элемент (a), который можно последовательно возводить в степень и получить все элементы мультипликативной группы, то есть все элементы нашего поля, кроме нуля. Причём единица получится всегда последней. a^(p-1) = 1 (mod p)
На том, что степени порождающего элемента проходят все элементы ровно по одному разу и построен предложенный алгоритм генерации перестановок. Он не годится для реальных дел, но в некоторых научных или ненаучных целях его можно использовать.
Нужно взять простое число большее как минимум на 2 максимальной длины строки.
0 в результате умножения не получается, а 1 будем считать признаком завершения подсчётов, элементы больше длины строки + 2 будем пропускать.
Для поиска порождающих элементов нужно разложить число (p-1) на множители. Если элемент a является порождающим, то возведение его в степени полученных множителей не должно равняться 1 по модулю p.
Как только один порождающий элемент найден, то порождающими будут его степени, взаимно простые с (p-1).

Для примера взял число 19. (19-1)=18=2*3*3
Проверяем число 2: 2^2 = 4≢1(mod p) 2^3 = 8≢1(mod p) - значит 2 - порождающий элемент.
Числа 1, 5, 7, 11, 13, 17 - взаимно простые с 18, поэтому 2^1, 2^5, 2^7, 2^11, 2^13, 2^17 - порождающие элементы
Это числа 2, 3, 10, 13, 14, 15. В данном случае у нас получится 6 вариантов перемешивания.
И теперь код:
public static void Main()
  {
    var str = "abcdefghijklmnop";
    Console.WriteLine(str);
    Console.WriteLine(string.Concat(Shuffle(str)));
  }
  private static Random Rnd = new Random((int)DateTime.Now.Ticks);
  public static IEnumerable<char> Shuffle(string str)
  {
    var prime = 19;	
    var makers = new [] {2, 3, 10, 13, 14, 15};
    var maker = makers[Rnd.Next(makers.Length)];
    if(prime < str.Length + 2) throw new Exception("Слишком длинная строка");
    var current = maker;
    while(current != 1)
    {		
      if(current < str.Length + 2)
        yield return str[current - 2];
      current *= maker;
      current %= prime;			
    }
  }
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
mayton2019
@mayton2019
Bigdata Engineer
Это называется генерация перестановок.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы