Skip to main content

Множество всех подмножеств

Описание

Множество всех подмножеств (булеан, показательное множество) (power set) — множество, состоящее из всех подмножеств данного множества S (включая пустое множество и само множество S); обозначается P(S) или 2^S (так как оно соответствует множеству отображений из S в {0, 1}).

Например, множеством всех подмножеств множества { x, y, z } будет:

{
{}, // (также обозначается как ∅ или нулевое подмножество)
{x},
{y},
{z},
{x, y},
{x, z},
{y, z},
{x, y, z}
}

Иллюстрация элементов этого множества, упорядоченных по порядку включения:


Количество подмножеств

Если S - это конечное множество, содержащее |S| = n элементов, то количество подмножеств S равняется |P(S)| = 2^n.

Алгоритмы

Двоичный подход

Биты каждого числа двоичного представления в диапазоне от 0 до 2^n показывают, следует ли включать соответствующий элемент множества в подмножество (1 - включать, 0 - не включать). Например, для множества { 1, 2, 3 } бинарное число 0b010 означает, что в подмножество включается только второй элемент множества, т.е. 2.

-abcПодмножество
0000{}
1001{c}
2010{b}
3011{c, b}
4100{a}
5101{a, c}
6110{a, b}
7111{a, b, c}

Реализация

// algorithms/sets/power-set/bitwise.js
export default function bitwise(set) {
const subsets = []

// Количество подмножеств - `2^n`, где `n` - количество элементов в `set`.
// Это обусловлено тем, что для каждого элемента `set` мы будем решать,
// включать его в подмножество или нет (2 варианта на каждый элемент)
const numberOfCombinations = 2 ** set.length

for (let i = 0; i < numberOfCombinations; i++) {
const subset = []

for (let j = 0; j < set.length; j++) {
// Решаем, включать текущий элемента в подмножество или нет
if (i & (1 << j)) {
subset.push(set[j])
}
}

subsets.push(subset)
}

return subsets
}

Тестирование

// algorithms/sets/power-set/__tests__/bitwise.test.js
import bitwise from '../bitwise'

describe('bitwise', () => {
it('должен вычислить множества всех подмножеств с помощью бинарного подхода', () => {
expect(bitwise([1])).toEqual([[], [1]])

expect(bitwise([1, 2, 3])).toEqual([
[],
[1],
[2],
[1, 2],
[3],
[1, 3],
[2, 3],
[1, 2, 3],
])
})
})

Рекурсивный подход

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

Реализация

// algorithms/sets/power-set/backtracking.js
export default function backtracking(
set,
allSubsets = [[]],
currentSubset = [],
start = 0,
) {
// Перебираем элементы множества, которые могут быть добавлены в подмножество
// без дублирования (это обеспечивается значением `start`)
for (let i = start; i < set.length; i++) {
// Добавляем текущий элемент в подмножество
currentSubset.push(set[i])

// Текущее подмножество является валидным, запоминаем его.
// `structuredClone()` создает копию `currentSubset`.
// Это необходимо, поскольку `currentSubset` будет модифицирован
// в дальнейших рекурсивных вызовах
allSubsets.push(structuredClone(currentSubset))

// Генерируем другие подмножества для текущего подмножества.
// В качестве значения `start` передаем `i + 1` во избежание дублирования
backtracking(set, allSubsets, currentSubset, i + 1)

// Удаляем последний элемент и берем следующий
currentSubset.pop()
}

return allSubsets
}

Тестирование

// algorithms/sets/power-set/__tests__/backtracking.test.js
import backtracking from '../backtracking'

describe('backtracking', () => {
it('должен вычислить множества всех подмножеств с помощью рекурсивного подхода', () => {
expect(backtracking([1])).toEqual([[], [1]])

expect(backtracking([1, 2, 3])).toEqual([
[],
[1],
[1, 2],
[1, 2, 3],
[1, 3],
[2],
[2, 3],
[3],
])
})
})

Каскадный подход

Вероятно, это самый простой способ создания множества всех подмножеств.

Предположим, что у нас есть множество originalSet = [1, 2, 3].

Мы начинаем с пустого подмножества:

powerSets = [[]]

Добавляем первый элемент originalSet во все существующие подмножества:

[[]] ← 1 = [[], [1]]

Добавляем второй элемент:

[[], [1]] ← 2 = [[], [1], [2], [1, 2]]

Добавляем третий элемент:

[[], [1], [2], [1, 2]] ← 3 = [[], [1], [2], [1, 2], [3], [1, 3], [2, 3], [1, 2, 3]]

И так для всех элементов originalSet. На каждой итерации количество множеств удваивается, поэтому в итоге мы получаем 2^n множеств.

Реализация

// algorithms/sets/power-set/cascading.js
export default function cascading(set) {
// Начинаем с пустого множества
const sets = [[]]

for (let i = 0; i < set.length; i++) {
// Без этого мы получим бесконечный цикл,
// поскольку длина `sets` будет увеличиваться на каждой итерации
const len = sets.length
for (let j = 0; j < len; j++) {
const _set = [...sets[j], set[i]]
sets.push(_set)
}
}

return sets
}

Тестирование

// algorithms/sets/power-set/__tests__/cascading.test.js
import cascading from '../cascading'

describe('cascading', () => {
it('должен вычислить множества всех подмножеств с помощью каскадного подхода', () => {
expect(cascading([1])).toEqual([[], [1]])

expect(cascading([1, 2])).toEqual([[], [1], [2], [1, 2]])

expect(cascading([1, 2, 3])).toEqual([
[],
[1],
[2],
[1, 2],
[3],
[1, 3],
[2, 3],
[1, 2, 3],
])
})
})

Запускаем тесты:

npm run test ./algorithms/sets/power-set