wells vale tipo referencia porque peso nivel mundial moneda mas fuerte fargo economia dolar cuba cuanto cambio arrays algorithm dynamic-programming space-complexity

arrays - vale - Cambio de moneda con memoria limitada para números de hasta mil millones



tipo de cambio euro dolar wells fargo (2)

Me enfrenté a este problema en un entrenamiento. Es decir, hemos dado N valores diferentes ( N<= 100 ). Llamemos a esta matriz A[N] , para esta matriz A estamos seguros de que tenemos 1 en la matriz y A[i] ≤ 10 9 . En segundo lugar, hemos dado el número S donde S ≤ 10 9 .

Ahora tenemos que resolver el problema de la moneda clásica con estos valores. En realidad, necesitamos encontrar el número mínimo de elementos que sumarán exactamente S Cada elemento de A se puede usar un número infinito de veces.

  • Tiempo límite: 1 seg.

  • Límite de memoria: 256 MB

Ejemplo:

S = 1000, N = 10 A[] = {1,12,123,4,5,678,7,8,9,10}. The result is 10. 1000 = 678 + 123 + 123 + 12 + 12 + 12 + 12 + 12 + 12 + 4

Lo que he intentado

Traté de resolver esto con la técnica clásica de problemas de la programación dinámica de monedas, pero usa demasiada memoria y da un límite de memoria excedido.

No puedo averiguar qué debemos mantener sobre esos valores. Gracias por adelantado.

Aquí están los casos de prueba en pareja que no se pueden resolver con el problema clásico de la moneda dp.

S = 1000000000 N = 100 1 373241370 973754081 826685384 491500595 765099032 823328348 462385937 251930295 819055757 641895809 106173894 898709067 513260292 548326059 741996520 959257789 328409680 411542100 329874568 352458265 609729300 389721366 313699758 383922849 104342783 224127933 99215674 37629322 230018005 33875545 767937253 763298440 781853694 420819727 794366283 178777428 881069368 595934934 321543015 27436140 280556657 851680043 318369090 364177373 431592761 487380596 428235724 134037293 372264778 267891476 218390453 550035096 220099490 71718497 860530411 175542466 548997466 884701071 774620807 118472853 432325205 795739616 266609698 242622150 433332316 150791955 691702017 803277687 323953978 521256141 174108096 412366100 813501388 642963957 415051728 740653706 68239387 982329783 619220557 861659596 303476058 85512863 72420422 645130771 228736228 367259743 400311288 105258339 628254036 495010223 40223395 110232856 856929227 25543992 957121494 359385967 533951841 449476607 134830774 OUTPUT FOR THIS TEST CASE: 5 S = 999865497 N = 7 1 267062069 637323855 219276511 404376890 528753603 199747292 OUTPUT FOR THIS TEST CASE: 1129042 S = 1000000000 N = 40 1 12 123 4 5 678 7 8 9 10 400 25 23 1000 67 98 33 46 79 896 11 112 1223 412 532 6781 17 18 19 170 1400 925 723 11000 607 983 313 486 739 896 OUTPUT FOR THIS TEST CASE: 90910


( NOTA: Actualizado y editado para mayor claridad. Análisis de complejidad agregado al final. )

Bien, aquí está mi solución, incluidas mis correcciones a los problemas de rendimiento encontrados por @PeterdeRivaz. He probado esto con todos los casos de prueba proporcionados en la pregunta y los comentarios y termina todo en menos de un segundo (bueno, 1.5s en un caso), usando principalmente solo la memoria para el caché de resultados parciales (supongo unos 16MB).

En lugar de utilizar la solución tradicional de DP (que es demasiado lenta y requiere demasiada memoria), utilizo una búsqueda combinatoria de Depth-First, Greedy-First con poda con los mejores resultados actuales. Me sorprendió (muy) que esto funcione tan bien como lo hace, pero aún sospecho que se podrían construir conjuntos de pruebas que tomarían una cantidad de tiempo exponencial en el peor de los casos.

Primero, hay una función maestra que es lo único que el código de llamada necesita llamar. Maneja toda la configuración e inicialización y llama a todo lo demás. (todo el código es C #)

// Find the min# of coins for a specified sum int CountChange(int targetSum, int[] coins) { // init the cache for (partial) memoization PrevResultCache = new PartialResult[1048576]; // make sure the coins are sorted lowest to highest Array.Sort(coins); int curBest = targetSum; int result = CountChange_r(targetSum, coins, coins.GetLength(0)-1, 0, ref curBest); return result; }

Debido al problema de los casos de prueba planteados por @PeterdeRivaz, también he agregado un caché de resultados parciales para manejar cuando hay grandes números en N [] que están muy juntos.

Aquí está el código para el caché:

// implement a very simple cache for previous results of remainder counts struct PartialResult { public int PartialSum; public int CoinVal; public int RemainingCount; } PartialResult[] PrevResultCache; // checks the partial count cache for already calculated results int PrevAddlCount(int currSum, int currCoinVal) { int cacheAddr = currSum & 1048575; // AND with (2^20-1) to get only the first 20 bits PartialResult prev = PrevResultCache[cacheAddr]; // use it, as long as it''s actually the same partial sum // and the coin value is at least as large as the current coin if ((prev.PartialSum == currSum) && (prev.CoinVal >= currCoinVal)) { return prev.RemainingCount; } // otherwise flag as empty return 0; } // add or overwrite a new value to the cache void AddPartialCount(int currSum, int currCoinVal, int remainingCount) { int cacheAddr = currSum & 1048575; // AND with (2^20-1) to get only the first 20 bits PartialResult prev = PrevResultCache[cacheAddr]; // only add if the Sum is different or the result is better if ((prev.PartialSum != currSum) || (prev.CoinVal <= currCoinVal) || (prev.RemainingCount == 0) || (prev.RemainingCount >= remainingCount) ) { prev.PartialSum = currSum; prev.CoinVal = currCoinVal; prev.RemainingCount = remainingCount; PrevResultCache[cacheAddr] = prev; } }

Y aquí está el código para la función recursiva que realiza el conteo real:

/* * Find the minimum number of coins required totaling to a specifuc sum * using a list of coin denominations passed. * * Memory Requirements: O(N) where N is the number of coin denominations * (primarily for the stack) * * CPU requirements: O(Sqrt(S)*N) where S is the target Sum * (Average, estimated. This is very hard to figure out.) */ int CountChange_r(int targetSum, int[] coins, int coinIdx, int curCount, ref int curBest) { int coinVal = coins[coinIdx]; int newCount = 0; // check to see if we are at the end of the search tree (curIdx=0, coinVal=1) // or we have reached the targetSum if ((coinVal == 1) || (targetSum == 0)) { // just use math get the final total for this path/combination newCount = curCount + targetSum; // update, if we have a new curBest if (newCount < curBest) curBest = newCount; return newCount; } // prune this whole branch, if it cannot possibly improve the curBest int bestPossible = curCount + (targetSum / coinVal); if (bestPossible >= curBest) return bestPossible; //NOTE: this is a false answer, but it shouldnt matter // because we should never use it. // check the cache to see if a remainder-count for this partial sum // already exists (and used coins at least as large as ours) int prevRemCount = PrevAddlCount(targetSum, coinVal); if (prevRemCount > 0) { // it exists, so use it newCount = prevRemCount + targetSum; // update, if we have a new curBest if (newCount < curBest) curBest = newCount; return newCount; } // always try the largest remaining coin first, starting with the // maximum possible number of that coin (greedy-first searching) newCount = curCount + targetSum; for (int cnt = targetSum / coinVal; cnt >= 0; cnt--) { int tmpCount = CountChange_r(targetSum - (cnt * coinVal), coins, coinIdx - 1, curCount + cnt, ref curBest); if (tmpCount < newCount) newCount = tmpCount; } // Add our new partial result to the cache AddPartialCount(targetSum, coinVal, newCount - curCount); return newCount; }

Análisis:

Memoria:

El uso de la memoria es bastante fácil de determinar para este algoritmo. Básicamente solo hay el caché de resultados parciales y la pila. El caché se fija en appx. 1 millón de entradas multiplicado por el tamaño de cada entrada (3 * 4 bytes), aproximadamente 12 MB. La pila está limitada a O(N) , por lo que juntos, la memoria claramente no es un problema.

UPC:

La complejidad del tiempo de ejecución de este algoritmo comienza siendo difícil de determinar y luego se vuelve más difícil, así que discúlpeme porque aquí hay muchos movimientos de mano. Intenté buscar un análisis del problema de la fuerza bruta (búsqueda combinatoria de sumas de valores base de N * kn que suman S), pero no mucho más. Lo poco que se tendía a decir que era O(N^S) , que es claramente demasiado alto. Creo que una estimación más justa es O(N^(S/N)) o posiblemente O(N^(S/AVG(N)) o incluso O(N^(S/(Gmean(N))) donde Gmean(N) es la media geométrica de los elementos de N []. Esta solución comienza con la búsqueda combinatoria de fuerza bruta y luego la mejora con dos optimizaciones significativas.

La primera es la poda de sucursales en función de las estimaciones de los mejores resultados posibles para esa rama en comparación con el mejor resultado que ya ha encontrado. Si los estimadores de los mejores casos fuesen perfectamente precisos y el trabajo para las sucursales estuviera perfectamente distribuido, esto significaría que si encontramos un resultado mejor que el 90% de los otros casos posibles, la poda eliminaría efectivamente el 90% del trabajo de ese punto en adelante Para resumir aquí la historia larga, esto debería resolver que la cantidad de trabajo que queda después de la poda debería reducirse armónicamente a medida que avanza. Suponiendo que se debe aplicar algún tipo de suma / integración para obtener un total de trabajo, me parece que esto funciona con un logaritmo del trabajo original. Así que llamémoslo O(Log(N^(S/N)) , o O(N*Log(S/N)) que es bastante bueno. (Aunque O(N*Log(S/Gmean(N))) es probablemente mas preciso).

Sin embargo, hay dos agujeros obvios con esto. Primero, es cierto que los estimadores de mejor caso no son perfectamente precisos y, por lo tanto, no se podarán tan eficazmente como se supone anteriormente, pero esto es algo contrabalanceado por el ordenamiento codicioso de las sucursales, que brinda las mejores oportunidades para Encontrar mejores soluciones al principio de la búsqueda que aumentan la efectividad de la poda.

El segundo problema es que el mejor estimador de caso funciona mejor cuando los diferentes valores de N están muy separados. Específicamente, si |(S/n2 - S/n1)| > 1 |(S/n2 - S/n1)| > 1 para cualquiera de los 2 valores en N, entonces se vuelve casi perfectamente efectivo. Para valores de N menores que SQRT (S), incluso dos valores adyacentes (k, k + 1) están lo suficientemente separados como para que se aplique esta regla. Sin embargo, para aumentar los valores por encima de SQRT (S), se abre una ventana, de modo que cualquier número de valores N dentro de esa ventana no podrá podarse entre sí. El tamaño de esta ventana es aproximadamente K / SQRT (S). Entonces, si S = 10 ^ 9, cuando K es alrededor de 10 ^ 6, esta ventana tendrá casi 30 números de ancho. Esto significa que N [] podría contener 1 más cada número desde 1000001 hasta 1000029 y la optimización de la poda no proporcionaría casi ningún beneficio.

Para abordar esto, agregué el caché de resultados parciales que permite la memorización de las sumas parciales más recientes hasta el objetivo S. Esto se beneficia del hecho de que cuando los valores de N están cerca, tenderán a tener un número extremadamente alto. De duplicados en sus sumas. Lo mejor que puedo imaginar, esta efectividad es aproximadamente N veces la raíz J-th del tamaño del problema donde J = S/K y K es una medida del tamaño promedio de los valores N (Gmean (N) es probablemente la mejor estimación). Si aplicamos esto a la búsqueda combinatoria de fuerza bruta, asumiendo que la poda es ineficaz, obtenemos O((N^(S/Gmean(N)))^(1/Gmean(N))) , que creo que también es O(N^(S/(Gmean(N)^2))) .

Así que, en este punto, haga su elección. Sé que esto es realmente incompleto, e incluso si es correcto, todavía es muy sensible a la distribución de los valores N, por lo que hay mucha variación.


[He reemplazado la idea anterior acerca de las operaciones de bits porque parece llevar demasiado tiempo]

Una idea algo loca e incompleta pero puede funcionar.

Comencemos con la introducción de f(n,s) que devuelve el número de combinaciones en las que s puede componerse de n monedas.

Ahora, ¿cómo f(n+1,s) está relacionado con f(n) ?

Una de las posibles formas de calcularlo es:

f(n+1,s)=sum[coin:coins]f(n,s-coin)

Por ejemplo, si tenemos monedas 1 y 3,

f(0,)=[1,0,0,0,0,0,0,0] - con cero monedas solo podemos tener una suma de cero

f(1,)=[0,1,0,1,0,0,0,0] - lo que podemos tener con una moneda

f(2,)=[0,0,1,0,2,0,1,0] - lo que podemos tener con dos monedas

Podemos reescribirlo un poco diferente:

f(n+1,s)=sum[i=0..max]f(n,si)*a(i)

a(i)=1 si tenemos la moneda i 0 de lo contrario

Lo que tenemos aquí es convolución: f(n+1,)=conv(f(n,),a)

https://en.wikipedia.org/wiki/Convolution

Calcularlo como sugiere la definición da O(n^2)

Pero podemos usar la transformada de Fourier para reducirla a O (n * log n).

https://en.wikipedia.org/wiki/Convolution#Convolution_theorem

Así que ahora tenemos una forma más o menos barata de averiguar qué números son posibles con n monedas sin ir incrementalmente, simplemente calcule n-th potencia n-th de F(a) y aplique la transformada de Fourier inversa.

Esto nos permite realizar una búsqueda binaria que puede ayudar a manejar los casos cuando la respuesta es grande.

Como dije, la idea es incompleta: por ahora no tengo idea de cómo combinar la representación de bits con las transformadas de Fourier (para satisfacer la restricción de memoria) y si encajaremos en 1 segundo en cualquier CPU "normal" ...