Javascript-форум (https://javascript.ru/forum/)
-   Общие вопросы Javascript (https://javascript.ru/forum/misc/)
-   -   Случайное разбиение множества (https://javascript.ru/forum/misc/81606-sluchajjnoe-razbienie-mnozhestva.html)

vsco89 23.12.2020 01:49

Случайное разбиение множества
 
Здравствуйте, форумчане! Второй день бьюсь с одной проблемой, облазил половину форумов: как реализовать на JS случайное разбиение множества (p.s. разбиение R(A) - непустые непересекающиеся подмножества множества А, в объединении дающие А)
Пример:
A = {1,2,3,4} ->
Вариант 1: R1(A) = { {1},{2,3,4} };
Вариант 2: R2(A) = { {1,2},{3,4} };
Вариант 3: R3(A) = { {1,3},{2,4} };
Вариант 4: R4(A) = { {1,2,3},{4} };
Вариант 5: R5(A) = { {2},{1,3,4} };
Вариант 6: R6(A) = { {3},{1,2,4} }; //.....

! Перестановка элементов внутри подмножества не будет разбиением !

Нужно, чтобы написанная функция случайным образом разбивала множество, полученное как аргумент этой функции.

Спасибо, большое!

voraa 23.12.2020 07:48

<pre>
<script>
function splitSet  (set) {
    let sub1, sub2;
    do {
         sub1 = [];
         sub2 = [];
         for(let i = 0; i < set.length; i++) {
            (Math.random()<0.5? sub1 : sub2).push(set[i]);
         }
    } while (sub1.length == 0 || sub2.length == 0);
    return [sub1, sub2];
}

for (let i = 0; i < 25; i++) {
let [s1, s2] = splitSet([1,2,3,4,5,6,7])
 
document.write(`[[${s1}],[${s2}]]<br>`)
}
</script>
</pre>

рони 23.12.2020 09:34

:) :write:
<pre>
<script>
function splitSet  (set) {
  const random = a => Math.trunc(Math.random() * a),
  sort = (a, b) => a - b;
  let {length} = set = set.slice(0), ar = [], n = 1 + random(length - 1);
  for (let i = 0; i < n; i++) {
  let k = random(length - i);
  ar.push(...set.splice(k, 1))
  }
  return [set, ar.sort(sort)];
}
for (let i = 0; i < 25; i++) {
let [s1, s2] = splitSet([1,2,3,4,5,6,7])

document.write(`[[${s1}],[${s2}]]<br>`)
}
</script>
</pre>

voraa 23.12.2020 09:44

рони,
Чего то не то.
У меня получился такой результат
[[1,3],[6,7,2,5,4]]
[[7],[6,1,4,3,2,5]]
[[2,3,4,7],[6,5,1]]
[[3,5,6],[4,7,1,2]]
[[1,3,6,7],[2,4,5]]
[[4,5,6,7],[1,3,2]]
[[2],[6,7,5,3,4,1]]
[[1,2,5,7],[4,3,6]]
[[6],[4,2,1,7,3,5]]
[[2,3,4,6,7],[1,5]]
[[1,3],[4,6,7,5,2]]
[[5,7],[1,2,4,6,3]]
[[1,3,4,5,6,7],[2]]
[[2,3,6],[4,5,1,7]]
[[1,2,3,5,7],[4,6]]
[[1,6],[3,2,5,4,7]]
[[1,2],[4,6,5,7,3]]
[[7],[1,4,6,5,2,3]]
[[4,5],[7,2,6,1,3]]
[[1,3,4,5,6,7],[2]]
[[4],[1,2,3,5,7,6]]
[[4,7],[1,3,6,5,2]]
[[3,5],[6,7,4,1,2]]
[[1,5],[6,7,3,4,2]]
[[1,4,7],[6,2,3,5]]


Строки 2 и 18
Цитата:

Сообщение от vsco89
! Перестановка элементов внутри подмножества не будет разбиением !


рони 23.12.2020 10:11

voraa,
это никак не противоречит условию задания.
(+добавил сортировку)

vsco89 23.12.2020 10:35

Спасибо огромное!

voraa 23.12.2020 11:22

рони,
В таких задачах (на случайность, на вероятность) меня всегда смущает вопрос, а на сколько будет сохраняться случайность распределения и плотность вероятности.
В этой задаче существует 126 способов разбиения.
А случаев попадания 1 элемента в первое множество - 7.
Т.е вероятность такого исхода всего 7/126 = 5.5%
В твоем решении ты сразу определяешь, что в первое множество 1 элемент попадет с вероятностью 1/6 = 16.7 %
По мне такой результат нельзя назвать достаточно случайным

рони 23.12.2020 14:10

voraa,
расчёты сомнительны, не вижу условий для перекоса, вероятность одинакова для всех комбинаций,
и нет череды бесполезных циклов.

voraa 23.12.2020 15:00

Цитата:

Сообщение от рони
не вижу условий для перекоса, вероятность одинакова для всех комбинаций,

Совсем не так!
На большом количестве тестов варианты, например,
[1] [2,3,4,5,6,7] и [1,2] [3,4,5,6,7]
Должны встречаться примерно одинаковое количество раз
Теперь смотрим
<pre>
<script>
function splitSet  (set) {
  const random = a => Math.trunc(Math.random() * a);
  let {length} = set = set.slice(0), ar = [], n = 1 + random(length - 1);
  for (let i = 0; i < n; i++) {
  let k = random(length - i);
  ar.push(...set.splice(k, 1))
  }
  return [set, ar];
}
let v1 =0;
let v2=0;
for (let i = 0; i < 100_000; i++) {
let [s1, s2] = splitSet([1,2,3,4,5,6,7])
 
//document.write(`[[${s1}],[${s2}]], ${s1.length}, ${s2.length}<br>`)
if (s1.length == 1 && s1[0] ==1) v1++
if (s1.length == 2 && s1[0] ==1 && s1[1] == 2) v2++
}
document.write(`[1] - ${v1}   [1,2] - ${v2}`)

</script>
</pre>


Цитата:

Сообщение от рони
и нет череды бесполезных циклов.

Цикл. один. И тот в подавляющем числе случаев выполняет только одну итерацию
Кроме случаев что все элементы оказались в одном множестве.
Для 7 элементов - вероятность 1/64.

рони 23.12.2020 15:35

voraa,
хорошо, но я не понимаю где ошибка, ваши объяснения(#7) ,я увы, не осилил.

voraa 23.12.2020 16:02

Ошибка в самом методе.
Вот это
n = 1 + random(length - 1);
Сразу говорит, что количество случаев, когда в первом множестве 1 элемент, 2 элемента, 3 элемента - одинаковое.
Но это не так.
Всего вариантов разбиения 126. (На самом деле 63, если считать, что s1=[1,2] и s2 = [1,2] в принципе одно и тоже разбиение)
(Это комбинаторика, которую я, разумеется не помню, просто погуглил
https://ru.wikipedia.org/wiki/%D0%A7...%D0%B4%D0 %B0)
Но на самом деле вариантов выбора 1 элемента - 7
А вариантов выбора 2 элементов - 6+5+4+3+2+1 = 21

рони 23.12.2020 16:10

voraa,
спасибо подумаю.

рони 23.12.2020 16:56

voraa,
мысли вслух ... :)
<pre>
<script>
function splitSet  (set) {
    const random = a => Math.trunc(Math.random() * a),
    sort = (a, b) => a - b;
    let {length} = set = set.slice(0), ar = [], n = 1, m = 1/((length - 1)/2);
    for (let i = 1; i < length; i++) {
      Math.random() > m || n++;
    }
    for (let i = 0; i < n; i++) {
    let k = random(length - i);
    ar.push(...set.splice(k, 1))
    }
    ar.sort(sort);

    return ar[0] == 1 ? [ar, set] :[set, ar];
}
let v1 = 0;
let v2 = 0;
let v3 = 0;
let v4 = 0;
let v5 = 0
for (let i = 0; i < 63000; i++) {
let [s1, s2] = splitSet([1,2,3,4,5,6,7])

//document.write(`[[${s1}],[${s2}]], ${s1.length}, ${s2.length}<br>`)
if (s1.length == 1 && s1[0] ==1) v1++
if (s1.length == 2 && s1[0] ==1 && s1[1] == 2) v2++
if (s1.length == 3 && s1[0] ==1 && s1[1] == 2 && s1[2] == 3) v3++
if (s1.length == 4 && s1[0] ==1 && s1[1] == 2 && s1[2] == 3 && s1[3] == 4) v4++
if (s1.length == 5 && s1[0] ==1 && s1[1] == 2 && s1[2] == 3 && s1[3] == 4 && s1[4] == 5) v5++
}
document.write(`[1] - ${v1}   [1,2] - ${v2} [1,2,3] - ${v3} [1,2,3,4] - ${v4}  [1,2,3,4,5] - ${v5}`)

</script>
</pre>

рони 23.12.2020 17:31

Цитата:

Сообщение от voraa
Но на самом деле вариантов выбора 1 элемента - 7
А вариантов выбора 2 элементов - 6+5+4+3+2+1 = 21

вы не могли бы написать формулу на js, по которой можно получить 7, 21, 63?
P.S. формулу написал, но если интересно напишите свой вариант.

рони 23.12.2020 18:59

разбить массив на две части
 
:):) :)
<pre>
<script>
const randomNetto = length => {
    let ar = [],
        sum = 0;
    for (let i = 1; i <= length / 2; i++) {
        var n = 1,
            j = length;
        for (let k = i; k; k--) {
            n *= j--;
            n /= k;
        }
        sum += n;
        ar.push(sum);
    }
    ar = ar.map(a => a / sum);
    return random => {
        let len = 0;
        for (let k of ar) {
            len++;
            if (random < k) break;
        }
        return len
    }
}

function splitSet(set) {
    const random = a => Math.trunc(Math.random() * a),
        sort = (a, b) => a - b;
    let {
        length
    } = set = set.slice(0), ar = [], count = randomNetto(length), n = count(Math.random());
    for (let i = 0; i < n; i++) {
        let k = random(length - i);
        ar.push(...set.splice(k, 1))
    }
    ar.sort(sort);
    return ar[0] == 1 ? [ar, set] : [set, ar];
}
let v1 = 0;
let v2 = 0;
let v3 = 0;
let v4 = 0;
let v5 = 0
for (let i = 0; i < 63000; i++) {
    let [s1, s2] = splitSet([1, 2, 3, 4, 5, 6, 7])
    //document.write(`[[${s1}],[${s2}]], ${s1.length}, ${s2.length}<br>`)
    if (s1.length == 1 && s1[0] == 1) v1++
    if (s1.length == 2 && s1[0] == 1 && s1[1] == 2) v2++
    if (s1.length == 3 && s1[0] == 1 && s1[1] == 2 && s1[2] == 3) v3++
    if (s1.length == 4 && s1[0] == 1 && s1[1] == 2 && s1[2] == 3 && s1[3] == 4) v4++
    if (s1.length == 5 && s1[0] == 1 && s1[1] == 2 && s1[2] == 3 && s1[3] == 4 && s1[4] == 5) v5++
}
document.write(`[1] - ${v1}   [1,2] - ${v2} [1,2,3] - ${v3} [1,2,3,4] - ${v4}  [1,2,3,4,5] - ${v5}`)
</script>
</pre>

voraa 23.12.2020 20:47

Цитата:

Сообщение от рони
вы не могли бы написать формулу на js, по которой можно получить 7, 21, 63?
P.S. формулу написал, но если интересно напишите свой вариант.

Математическая формула известна (вспоминаем комбинаторику :) )
Количество сочетаний из n элементов по k
(т.е сколькими способами можно выбрать k элементов из n)
C(n,k) = n!/ (k! * (n-k)!)
Сумма C(n,k) k=2...n-1 дает количество вариантов разбиения множества из n элементов на два непустых подмножества.
Отсюда получаем такой счет
<pre>
<script>
// Формирует массив из n элементов
// a[k] - количество вариантов выбора k элементов из n
// a[0] и a[n] = 1
// и считаем сумму a[i] (i=1...n-1)
const nsel = (n) => {
	let a = [1, n]; s=n
	for (let i=2; i<=n-1; i++) {
		a[i] = a[i-1]*(n-i+1)/i
		s+=a[i]
	}
	a[n] = 1;
	return [a, s]
}

for (let k=3; k<=10; k++) {
	let [a,s] = nsel(k)
	document.write(`${k} : ${a} sum = ${s}<br>`)
}
</script>
</pre>


Про лишние циклы.
Я все равно убежден, что [...x] и x.sort() (так же как и forEach, map, reduce...) это все равно циклы.
Хоть они может быть (хотя не факт) и выполняются чуть быстрее, чем явно написанный, но на производительности сказываются.

рони 23.12.2020 20:57

voraa,
спасибо!:thanks:

рони 23.12.2020 22:38

:write:
<pre>
<script>
const randomNetto = length => {
    let n = length, ar = [n], sum = n, k = n;
    for (let i = 2; i < length; i++) {
            n *= --k;
            n /= i;
            sum += n;
            ar.push(sum);
            }
    ar = ar.map(a => a / sum);
    return random => {
        let len = 0;
        for (let k of ar) {
            len++;
            if (random < k) break;
        }
        return len
    }
}

function splitSet(set) {
    const random = a => Math.trunc(Math.random() * a),
        sort = (a, b) => a - b;
    let {length} = set = set.slice(0),
    ar = [],
    count = randomNetto(length),
    n = count(Math.random());
    for (let i = 0; i < n; i++) {
        let k = random(length - i);
        ar.push(...set.splice(k, 1))
    }
    ar.sort(sort);
    return ar[0] == 1 ? [ar, set] : [set, ar];
}
let obj = {};
for (let i = 0; i < 63000; i++) {
    let [s1, s2] = splitSet([1, 2, 3, 4, 5, 6, 7])
    obj[[s1]] = (obj[[s1]] >> 0) + 1;
    obj[[s2]] = (obj[[s2]] >> 0) + 1;
}

document.write(JSON.stringify(obj, "", " "))
obj = {};
for (let i = 0; i < 15500; i++) {
    let [s1, s2] = splitSet([1, 2, 3, 4, 5])
    obj[[s1]] = (obj[[s1]] >> 0) + 1;
    obj[[s2]] = (obj[[s2]] >> 0) + 1;
}

document.write(JSON.stringify(obj, "", " "))
</script>
</pre>

voraa 23.12.2020 23:18

Ну завершающий тест :)
<pre>
<script>
let t0, t1;
let nloop=0;
function splitSet  (set) {
    let sub1, sub2;
    do {
        nloop++;
         sub1 = [];
         sub2 = [];
         for(let i = 0; i < set.length; i++) {
            (Math.random()<0.5? sub1 : sub2).push(set[i]);
         }
    } while (sub1.length == 0 || sub2.length == 0);
    return [sub1, sub2];
}
let obj = {};
t0 = performance.now()
for (let i = 0; i < 100_000; i++) {
    let [s1, s2] = splitSet([1, 2, 3, 4, 5, 6, 7])
    obj[[s1]] = (obj[[s1]] >> 0) + 1;
    obj[[s2]] = (obj[[s2]] >> 0) + 1;
}
t1 = performance.now()

document.write(JSON.stringify(obj, "", " "))
document.write(`<br>Time ${t1-t0}    nloop  ${nloop}`)

document.write('<br><br>')


const randomNetto = length => {
    let n = length, ar = [n], sum = n, k = n;
    for (let i = 2; i < length; i++) {
            n *= --k;
            n /= i;
            sum += n;
            ar.push(sum);
            }
    ar = ar.map(a => a / sum);
    return random => {
        let len = 0;
        for (let k of ar) {
            len++;
            if (random < k) break;
        }
        return len
    }
}
 
function splitSet1(set) {
    const random = a => Math.trunc(Math.random() * a),
        sort = (a, b) => a - b;
    let {length} = set = set.slice(0),
    ar = [],
    count = randomNetto(length),
    n = count(Math.random());
    for (let i = 0; i < n; i++) {
        let k = random(length - i);
        ar.push(...set.splice(k, 1))
    }
    ar.sort(sort);
    return ar[0] == 1 ? [ar, set] : [set, ar];
}
obj = {};
t0 = performance.now()
for (let i = 0; i < 100_000; i++) {
    let [s1, s2] = splitSet1([1, 2, 3, 4, 5, 6, 7])
    obj[[s1]] = (obj[[s1]] >> 0) + 1;
    obj[[s2]] = (obj[[s2]] >> 0) + 1;
}
t1 = performance.now()

document.write(JSON.stringify(obj, "", " "))
document.write(`<br>Time ${t1-t0}`)

</script>
</pre>


Примерно 1500 "лишних" циклов. Как я и писал (пост #9) вероятность 1/64 = 1.56%

рони 23.12.2020 23:52

voraa,
:lol:

Alexandroppolus 25.12.2020 12:29

В посте #2 этого топика максимально простое и очевидное решение. Я правильно понимаю, что далее дискуссия ведется по поводу строки 11, из-за которой это решение теоретически может работать бесконечно долго?

рони 25.12.2020 14:08

Alexandroppolus,
дискуссии не было, полностью согласен что #2 простое решение и чем больше начальный массив, тем меньше "лишних" циклов вероятность, мне было интересно решить это как-то иначе.

voraa 25.12.2020 14:39

Я покрутил алгоритм из поста #2 со множеством из 20 элементов 1 000 000 раз.
Получил 2 лишних цикла.
Как и положено по теории, с вероятностью 1/2^20 получаем последовательность из 20 случайных чисел, каждое из которых < 0.5 или >=0.5
Годный датчик случайных чисел

Alexandroppolus 25.12.2020 14:53

чисто ради прикола докрутил #2, чтобы не было вероятности лишнего цикла

function splitSet(set) {
    const subs = [[], []];
    for(let i = 0; i < set.length; i++) {
        subs[Math.round(Math.random())].push(set[i]);
    }
    if (!subs[0].length || !subs[1].length) {
        const srcIndex = subs[0].length ? 0 : 1;
        const index = Math.floor(Math.random() * set.length);
        subs[1 - srcIndex] = subs[srcIndex].splice(index, 1);
    }
    return subs;
}

for (let i = 0; i < 25; i++) {
    let [s1, s2] = splitSet([1,2,3,4,5,6,7]);
    console.log(s1, s2);
}


суть: если в одном из массивов пусто, из другого перекидываем случайно выбранный элемент. Это сохраняет равные шансы для каждого из элементов попасть в любой массив.

Можно было бы перекидывать, допустим, первый элемент. Но тогда немного уменьшились бы его шансы оказаться вместе со вторым, хотя по отдельности и у первого, и у второго одинаковые шансы быть в любом из массивов. Этот "парадокс" я пока не осмыслил.

Alexandroppolus 25.12.2020 17:11

Цитата:

Сообщение от Alexandroppolus
если в одном из массивов пусто, из другого перекидываем случайно выбранный элемент. Это сохраняет равные шансы для каждого из элементов попасть в любой массив.

сейчас подумал - а ведь этот вариант не совсем правильный, он немного увеличивает шансы для разбиений, где отдельно один элемент.
Да, задачка хитрая)


Часовой пояс GMT +3, время: 03:25.