Dados los n
números en una matriz (no se puede suponer que son enteros), me gustaría calcular el producto de todos los subconjuntos de tamaño n-1
.
Puede hacerlo multiplicando todos los números y luego dividiéndolos por turnos, siempre que ninguno de los números sea cero. Sin embargo, ¿qué tan rápido puedes hacer esto sin división?
Si no permite la división, ¿cuál es el número mínimo de operaciones aritméticas (por ejemplo, multiplicación y suma) necesarias para calcular el producto de todos los subconjuntos de tamaño n-1?
Claramente puedes hacerlo en (n-1)*n
multiplicaciones.
Para aclarar, la salida son n
productos diferentes y las únicas operaciones aparte de leer y escribir en la memoria permitidas son la multiplicación, la suma y la resta.
Ejemplo
Si la entrada tiene tres números 2,3,5
, entonces la salida es tres números 15 = 3*5
, 10 = 2*5
y 6 = 2*3
.
Criterio ganador
Las respuestas deben dar una fórmula exacta para la cantidad de operaciones aritméticas que usará su código en términos de n
. Para simplificar la vida, simplemente conectaré n = 1000
tu fórmula para juzgar su puntaje. Cuanto más bajo, mejor.
Si es demasiado difícil producir una fórmula exacta para su código, puede ejecutarla n = 1000
y contar las operaciones aritméticas en el código. Sin embargo, una fórmula exacta sería la mejor.
Debe agregar su puntaje n=1000
a su respuesta para una comparación fácil.
fuente
+
en índices ? Si este es el caso, ¿también cuenta la indexación de matrices? (dado que, después de todo, es el azúcar sintáctico para sumar y desreferenciar).(n-1)*n
multiplicaciones ¿ Quieres decir(n-2)*n
, verdad?Respuestas:
Python, 3 (n-2) operaciones, puntaje = 2994
Las matrices
left
yright
contienen los productos acumulados de la matriz desde la izquierda y desde la derecha, respectivamente.EDITAR: Prueba de que 3 (n-2) es el número óptimo de operaciones necesarias para n> = 2, si solo usamos la multiplicación.
Lo haremos por inducción; según el algoritmo anterior, solo tenemos que demostrar que para n> = 2, 3 (n-2) es un límite inferior en el número de multiplicaciones necesarias.
Para n = 2, necesitamos al menos 0 = 3 (2-2) multiplicaciones, por lo que el resultado es trivial.
Supongamos que n> 2, y supongamos que para n - 1 elementos, necesitamos al menos 3 (n-3) multiplicaciones. Considere una solución para n elementos con k multiplicaciones. Ahora, eliminamos el último de estos elementos como si fuera 1, y simplificamos todas las multiplicaciones directamente por él. También eliminamos la multiplicación que conduce al producto de todos los demás elementos, ya que ese no es necesario, ya que nunca se puede utilizar como un valor intermedio para obtener el producto de n-2 de los otros elementos, ya que la división no está permitida. Esto nos deja con l multiplicaciones, y una solución para n - 1 elementos.
Por hipótesis de inducción, tenemos l> = 3 (n-3).
Ahora, echemos un vistazo a cuántas multiplicaciones se eliminaron. Uno de ellos fue el que condujo al producto de todos los elementos, excepto el último. Además, el último elemento se usó directamente en al menos dos multiplicaciones: si se usó en una sola, se usó al multiplicar por un resultado intermedio que consiste en algún producto de los otros elementos; digamos, sin pérdida de generalidad, este resultado intermedio incluye el primer elemento en el producto. Entonces, no hay forma de obtener el producto de todos los elementos, excepto el primero, ya que cada producto que contiene el último elemento es el último o contiene el primer elemento.
Por lo tanto, tenemos k> = l + 3> = 3 (n-2), lo que demuestra el teorema reivindicado.
fuente
f l = zipWith (*) (scanl (*) 1 l) (scanr (*) 1 $ tail l)
.Haskell , puntuación 2994
Pruébalo en línea!
Digamos que nos dan la lista
[a,b,c,d,e,f,g,h]
. Primero lo agrupamos en pares[[a,b],[c,d],[e,f],[g,h]]
. Luego, recurrimos a la listapairs
de tamaño medio de sus productos para obtenersubresults
Si tomamos el primer elemento
(c*d)*(e*f)*(g*h)
y lo multiplicamos porb
ya
respectivamente, obtenemos el producto de todo peroa
y todo menosb
. Al hacer esto para cada par y resultado recursivo con ese par perdido, obtenemos el resultado final. El caso de longitud impar se maneja especialmente haciendo pasar el elemento impar sin emparejar al paso recursivo, y el producto de los elementos restantes devueltos es el producto sin él.El número de multiplicaciones
t(n)
esn/2
para el producto de emparejamiento,t(n/2)
para la llamada recursiva y otron
para los productos con elementos individuales. Esto dat(n) = 1.5 * n + t(n/2)
por extrañon
. El uso de recuentos más precisos para lan
multiplicación impar e ignorando1
para el caso base da puntaje2997
paran=1000
.fuente
products_but_one'
podría evitar eso al devolver algo de la longitud correcta.1
multiplicación gratuita. Creo que el relleno 1 no afectó las cosas, pero limpié mi algoritmo para no usarlas.float
.Haskell , puntaje 9974
Pruébalo en línea!
Una estrategia de divide y vencerás, que recuerda mucho al tipo de fusión. No hace ninguna indexación.
La función
partition
divide la lista en mitades tan iguales como sea posible al colocar elementos alternos en lados opuestos de la partición. Fusionamos recursivamente los resultados(p,r)
para cada una de las mitades, conr
la lista de productos con uno perdido yp
el producto general.Para la salida de la lista completa, el elemento que falta debe estar en una de las mitades. El producto que falta ese elemento es un producto que falta para la mitad en el que está, multiplicado por el producto completo para la otra mitad. Entonces, multiplicamos cada producto con uno perdido por el producto completo de la otra mitad y hacemos una lista de los resultados, como
map(*p1)r2 ++ map(*p2)r1)
. Esto toman
multiplicaciones, donden
es la longitud. También tenemos que hacer un nuevo producto completop1*p2
para uso futuro, que cuesta 1 multiplicación más.Esto le da a la recursión general para el número de operaciones
t(n)
conn
aún:t(n) = n + 1 + 2 * t(n/2)
. El impar es similar, pero una de las sublistas es1
más grande. Al hacer la recursión, obtenemosn*(log_2(n) + 1)
multiplicaciones, aunque la distinción par / impar afecta ese valor exacto. Los valores hastat(3)
se mejoran al no multiplicar por1
definiendo una variante(%)
de los(*)
accesos directos a los casos_*1
o1*_
.Esto da
9975
multiplicaciones paran=1000
. Creo que la pereza de Haskell significa que no se calcula el producto general no utilizado en la capa externa9974
; si me equivoco, podría omitirlo explícitamente.fuente
n = 1000
y contar las operaciones aritméticas en el código.9974
y no9975
multiplicacionesn = 1000
(en el caso de calcular el producto general en la capa externa). ¿Incluiste un1
en la entrada que usaste para probarlo?trace
a partirDebug.Trace
de un cajón de sastre| trace "call!" False = undefined
guardia, creo. Pero esto se usaunsafePerformIO
debajo del capó, por lo que no es realmente una gran mejora.Haskell , puntuación 2994
Pruébalo en línea!
Cómo funciona
Esta es una versión limpia del algoritmo de xnor que trata el caso extraño de una manera más directa (editar: parece que xnor lo ha limpiado de la misma manera):
[a, b, c, d, e, f, g] ↦
[a, bc, de, fg] ↦
[(bc) (de) (fg), a (de) (fg), a (bc) ( fg), a (bc) (de)] por recursión ↦
[(bc) (de) (fg), a (de) (fg) c, a (de) (fg) b, a (bc) (fg) e, a (bc) (fg) d, a (bc) (de) g, a (bc) (de) f]
[a, b, c, d, e, f, g, h] ↦
[ab, cd, ef, gh] ↦
[(cd) (ef) (gh), (ab) (ef) (gh), ( ab) (cd) (gh), (ab) (cd) (ef)] por recursión ↦
[(cd) (ef) (gh) b, (cd) (ef) (gh) a, (ab) (ef ) (gh) d, (ab) (ef) (gh) c, (ab) (cd) (gh) f, (ab) (cd) (gh) e, (ab) (cd) (ef) h, (ab) (cd) (ef) g].
fuente
O (n log n) operaciones, puntaje = 9974
Funciona con un árbol binario.
Pitón
Esto también requiere operaciones de suma de listas y algo de aritmética en números que no son los valores de entrada; No estoy seguro si eso cuenta. La
mul
función está ahí para guardar n operaciones para el caso base, para evitar desperdiciarlas multiplicando por 1. En cualquier caso, se trata de operaciones O (n log n). La fórmula exacta es, si solamente contando operaciones aritméticas sobre números de entrada, conj = floor(log_2(n))
:j * (2^(j + 1) - n) + (j + 1) * (2 * n - 2^(j + 1)) - 2
.¡Gracias a @xnor por guardar una operación con la idea de no calcular el producto externo!
La última parte es generar los productos en el orden del término faltante.
fuente
n = 1000
y contar las operaciones aritméticas en el código.p[i] = p[i + i] * p[i + i+1]
no se cuentan log2 n + n
operaciones (que es O (nlogn) por ciertop[i] = p[i + i] * p[i + i + 1]
deben ser guardadas por la optimización de multiplicación. Sin embargo, podría haber contado uno demasiado.O ((n-2) * n) = O (n 2 ): solución trivial
Esta es solo la solución trivial que multiplica cada uno de los subconjuntos:
Pitón
Tenga en cuenta que esto también requiere
n
operaciones de adición de lista; No estoy seguro si eso cuenta. Si eso no está permitido,product(array[:index] + array[index + 1:])
puede reemplazarse porproduct(array[:index]) * product(array[index + 1:])
, lo que cambia la fórmula aO((n-1)*n)
.fuente
product
funciónO(n)
? uno para cada elemento de la matriz (aunque esto se puede cambiar fácilmente aO(n-1)
)Python, 7540
Una estrategia de fusión tripartita. Creo que puedo hacerlo incluso mejor que esto, con una fusión aún grande. Es O (n log n).
EDITAR: se corrigió un error.
La función relevante es
missing_products
, que proporciona el producto general y todos los que tienen un elemento faltante.fuente
tri_merge
? También puede reemplazar el2 * split_size + ...
intri_partition
consplit_size + split_size + ...
.dc, puntaje 2994
Supongo que la comparación de enteros
z1=
(que termina la recursión cuando alcanzamos el último valor) es libre. Esto es equivalente a los gustos deforeach
en otros idiomas.Manifestaciones
Una demostración con entradas grandes y pequeñas:
fuente
C ++, puntaje: 5990, O ([2NlogN] / 3)
Esta implementación utiliza una tabla de búsqueda de árbol binario. Mi primera implementación fue O (NlogN), pero una optimización de último minuto, que busca el producto de todos los elementos de la matriz menos un par, + 2 multiplicaciones salvaron el día. Creo que esto aún podría optimizarse un poco más, tal vez otro 16% ...
He dejado algunos rastros de depuración, solo porque es más fácil eliminarlos que reescribirlos :)
[Editar] la complejidad real se mide en O ([2NlogN] / 3) para 100. En realidad, es un poco peor que O (NlogN) para conjuntos pequeños, pero tiende a O ([NlogN] / 2) a medida que la matriz crece O muy grande (0.57.NlogN) para un conjunto de 1 millón de elementos.
Estoy agregando el algoritmo de @nore, para completar. Es realmente agradable y es el más rápido.
fuente