Algoritmo para generar todas las combinaciones de lista de conjuntos con hasta un elemento por conjunto

Para una lista dada de conjuntos donde los elementos de los conjuntos no comparten ningún elemento entre los conjuntos, quiero calcular todas las combinaciones posibles donde una combinación puede tener hasta un elemento por conjunto. Por ejemplo:

{{a}, {b1, b2}}

Produciría:

{ Ø, {a}, {b1}, {b2}, {a, b1}, {a, b2}}

Supuse que para cualquier juego múltiple dado con norte conjuntos donde conjunto norte tiene k norte elementos hay:

( k 1 + 1 ) × ( k 2 + 1 ) + + ( k norte + 1 )
posibles combinaciones. Mi enfoque algorítmico hasta ahora me ha llevado a codificar todas las combinaciones posibles manteniendo norte registros donde cada registro puede contar desde 0 a k norte + 1 . Para el ejemplo anterior, esto se vería así:

{0, 0} - Ø
{0, 1} - b1
{0, 2} - b2
{1, 0} - a
{1, 1} - a, b1
{1, 2} - a, b2

Luego uso esos números para asignarlos a los números en los conjuntos. Esto funciona, pero para realizar un seguimiento de qué número debe fluir y cuáles deben restablecerse, tengo que revisar cada número en el registro.

¿Existe una forma más eficiente, por ejemplo, codificar todos los estados diferentes en un solo número? Intenté hacer esto con un número binario e intenté determinar el elemento del conjunto con operaciones bit a bit, pero me encontré con un problema en el que este ejemplo no funciona:

0|00 - Ø
0|01 - b1
0|10 - b2
1|00 - a      <-- the consecutive number here would be 011
1|01 - a, b1
1|10 - a, b2
¿Por qué necesitas que los números binarios sean consecutivos? Solo cámbialo a
0 00 0 01 { b 1 } 0 10 { b 2 } 0 11 inválido 1 00 { a } 1 01 { a , b 1 } 1 10 { a , b 2 } 1 11 inválido
@SlipEternal Entonces podría omitir ciertas iteraciones, pero mi pregunta sería, ¿cómo sé qué números omitir y, por lo tanto, también cuántos pasos más necesito? Parece que el primer registro es cada 4 pasos, lo que tiene sentido dado que necesito dos bits para codificar todos los estados. ¿Precalcularía las combinaciones no válidas?
Suponga que tiene este algoritmo que codifica y decodifica una combinación particular. Preguntas: ¿Cuál sería la entrada al algoritmo de decodificación? ¿Solo la cadena de bytes codificada? O también estarías pasando norte ? Los diversos k i ? ¿Todos los elementos de los conjuntos de componentes? ¿Qué pasa con el algoritmo de codificación? ¿Qué información se considera una entrada?
Mi enfoque actual usa una lista de números como entrada para la decodificación donde cada número representa la cantidad de elementos en un conjunto dado, de esa manera puedo seleccionar directamente el elemento que quiero incluir en la combinación. Para la codificación solo cuento de 0 a ( k 1 + 1 ) × × ( k 2 + 1 )
Parece que su entrada es solo una lista de conjuntos, no un conjunto múltiple . ¿Podemos suponer que los conjuntos dados no tienen elementos en común?
@Karl Tienes razón, no pensé en eso, sí, los elementos de los conjuntos no tienen ningún elemento en común.
¿Te importa en absoluto el orden en que se enumeran los conjuntos? También es posible que desee migrar esto a cs.stackexchange.com
@DanielV el orden no importa en absoluto. Buen punto, ni siquiera pensé en eso.
FWIW, esto es equivalente a enumerar las hojas de un árbol, donde la raíz tiene | a | + 1 bordes que salen de ella representando { norte i yo , a 0 , a 1 , } . Y cada hijo de la raíz tiene | b | + 1 bordes que salen de ella representando { norte i yo , b 0 , b 1 , } . Etc.
Entonces, ¿otra forma podría ser construir un árbol repasando cada elemento del conjunto e insertarlos para cada nodo y luego atravesar el árbol?
@Mahoni No, no, en realidad no construyas el árbol. Sería enorme. No necesitas construir un árbol para atravesarlo. Pero puede usar el algoritmo transversal de árbol básico para enumerar los conjuntos: suba el árbol hasta que pueda ir a la derecha, vaya a la derecha una vez y luego descienda al niño más a la izquierda.

Respuestas (2)

Sí, hay una manera más eficiente. Dejar

norte = ( k 1 + 1 ) × × ( k norte + 1 )
sea ​​el número de posibles selecciones. dado un entero A { 0 , 1 , , norte 1 } , puede producir directamente la selección de la siguiente manera. usaré A % metro para indicar el resto de A dividido por metro , y A / / metro para denotar A / metro . Eso es, A / / metro es A / metro redondeado hacia abajo.

  • Dejar s 1 = A % ( k 1 + 1 ) . Tenga en cuenta que hay k 1 + 1 posibilidades de s 1 , entonces s 1 define nuestra elección para el primer conjunto.

  • Dejar s 2 = ( A / / ( k 1 + 1 ) ) % ( k 2 + 1 ) . Del mismo modo, hay ( k 2 + 1 ) valores posibles para s 2 , entonces s 2 determina nuestra elección del segundo conjunto.

  • Dejar s 3 = ( A / / [ ( k 1 + 1 ) ( k 2 + 1 ) ] ) % ( k 3 + 1 ) . s 3 determina la elección del tercer set.

Etcétera. En general, para calcular s j de A , para cada j { 1 , , norte } , calculas el producto ( k 1 + 1 ) ( k 2 + 1 ) ( k j 1 + 1 ) (cuando j = 1 , este es un producto vacío, igual a 1 ), luego calcule la división redondeada hacia abajo de A dividido por ese producto, y finalmente establecer s j ser el resultado de esa división módulo ( k j + 1 ) .

Esto significa que A te da una lista ( s 1 , s 2 , , s norte ) que determina su selección por completo. Cada A { 0 , 1 , , norte 1 } da una selección única, no se saltan números.

"Eficiente" es una palabra complicada. ¿"Eficiente" en qué sentido? ¿Consumo de espacio? Entonces sí, este algoritmo es el más eficiente. Sin embargo, si se trata de complejidad computacional, esto puede no ser más eficiente. Si el espacio es barato y la potencia computacional es costosa, el algoritmo que sugirió el OP está en camino de ser más eficiente.
La cantidad de operaciones requeridas aumenta con cada elemento, eso es cierto, pero también estaba pensando que el producto acumulado podría memorizarse en cada paso.
Esto es hermoso, acabo de intentarlo. La complejidad del tiempo se limita a algo como: ( k 1 + 1 ) × × ( k norte + 1 ) × norte . Mi enfoque fue tratar de hacer uso del espacio, pero creo que perdí la complejidad del tiempo nuevamente cuando trato de determinar qué números tengo que incrementar y fluir, termino teniendo que iterar los registros. Supongo que es un poco más eficiente porque no tengo que repetirlos todos, pero en general me encanta la simplicidad que produce.
Además, ¿hay un nombre para este tipo de enfoque? Estaba tratando de detectar ese patrón cuando miraba los intervalos en los que aumentan los números y se seleccionan diferentes elementos de cada conjunto, pero me preguntaba si esto tiene un nombre.
@Mahoni El mejor nombre que se me ocurre es el sistema de numeración radix mixto . En tu publicación, intentamos binario, donde cada posición es una potencia de 2 . Mi método es un sistema numérico donde hay un 1 's lugar, un ( k 1 + 1 ) lugar, un ( k 1 + 1 ) ( k 2 + 1 ) lugar, etc

Esta es una buena tarea para un algoritmo recursivo: Para cada elemento X del primer conjunto, iterar recursivamente a través de las combinaciones de los conjuntos restantes, anteponiendo X a cada uno

También hay una función de biblioteca de Python itertools.productque hace la mayor parte del trabajo:

import itertools
mysets = [[None, "a"], [None, "b1", "b2"]]
for combi in itertools.product(*mysets):
    print(combi)

salidas:

(None, None)
(None, 'b1')
(None, 'b2')
('a', None)
('a', 'b1')
('a', 'b2')
Vi eso, curioso de cómo implementan eso github.com/python/cpython/blob/main/Modules/…
Supongo que el enfoque recursivo es el enfoque de fuerza bruta que toma más tiempo en términos de complejidad de tiempo.
No, el enfoque recursivo es óptimo (lineal en el tamaño de salida).
Generar un producto cartesiano solo es costoso desde el punto de vista computacional debido a la gran cantidad de resultados; un algoritmo razonable de "fuerza bruta" no está haciendo ningún trabajo innecesario.