¿Existen algoritmos del mundo real que superen ampliamente en la clase a continuación? [cerrado]


39

Anoche estaba discutiendo con otro programador que, aunque algo puede ser O (1), una operación que es O (n) puede superarlo si hay una gran constante en el algoritmo O (1). No estuvo de acuerdo, así que lo traje aquí.

¿Hay ejemplos de algoritmos que superen en gran medida a los de la clase a continuación? Por ejemplo, O (n) es más rápido que O (1) u O (n 2 ) es más rápido que O (n).

Matemáticamente, esto se puede demostrar para una función con límites superiores asintóticos, cuando se ignoran los factores constantes, pero ¿existen tales algoritmos en la naturaleza? ¿Y dónde encontraría ejemplos de ellos? ¿Para qué tipos de situaciones se usan?


15
Incluso para algoritmos "grandes", más pequeño no es necesariamente mejor. Por ejemplo, la eliminación gaussiana es O (n ^ 3), pero hay algoritmos que pueden hacerlo en O (n ^ 2), pero el coeficiente para el algoritmo de tiempo cuadrático es tan grande que las personas simplemente siguen el O (n ^ 3) uno.
BlackJack

11
Debe agregar "... para problemas del mundo real" o algo así para hacer de esta una pregunta sensata. De lo contrario, solo necesita hacer lo nsuficientemente grande como para compensar la constante (que es el punto de la notación big-O).
Starblue

8
No tome la notación big-O para la velocidad.
Codismo

16
El punto de la notación big-O no es decirte qué tan rápido se ejecuta un algoritmo, sino qué tan bien se escala.
BlueRaja - Danny Pflughoeft

44
Me sorprende que nadie haya mencionado el algoritmo Simplex para resolver LP. Tiene un peor caso exponencial con un tiempo de ejecución lineal esperado. En la práctica, es bastante rápido. Es trivial construir un problema que exhiba el peor tiempo de ejecución también. Además, es muy utilizado.
ccoakley

Respuestas:


45

Búsquedas en tablas de datos fijas muy pequeñas. Una tabla hash optimizada puede ser O (1) y aún más lenta que una búsqueda binaria o incluso una búsqueda lineal debido al costo del cálculo del hash.


14
Más precisamente, la búsqueda de tabla hash es O (m) donde m es el tamaño de la clave. Solo puede llamar a eso O (1) si el tamaño de la clave es constante. Además, generalmente eso se amortiza; de lo contrario, la tabla no puede crecer / reducirse. Los árboles ternarios a menudo pueden superar las tablas hash para las búsquedas de cadenas en contextos donde las cadenas no se encuentran con frecuencia: la búsqueda del árbol ternario a menudo descubrirá que la clave no está presente mientras sigue comprobando el primer carácter o dos de la cadena, donde el la versión de tabla hash aún no ha calculado el hash.
Steve314

2
Me encanta la respuesta de Loren Pechtel y el primer comentario de Steve314. De hecho, he visto que esto suceda. Si crea una clase Java que tiene un método hashcode () que tarda demasiado en devolver el valor hash (y no lo almacena / no puede almacenar en caché), entonces utiliza instancias de dicha clase en una colección de tipo hash (como HashSet) hará que esa colección sea MUCHO más lenta que una colección de tipo matriz (como ArrayList).
Shivan Dragon

1
@ Steve314: ¿por qué supone que las funciones hash son O (m) donde m es el tamaño de la clave? Las funciones de hash pueden ser O (1) incluso si se trata de cadenas (u otro tipo complejo). No hay demasiado valor para ponerlo en una definición formal que simplemente darse cuenta de que la función hash podría cambiar significativamente la complejidad si se elige una estructura de datos incorrecta (tabla hash) para su entrada (el tamaño de la clave es impredecible).
Codism

1
@ Steve314: Tenga en cuenta que dije tablas de datos fijos. No crecen Además, solo obtiene el rendimiento O (1) de una tabla hash si puede optimizar la clave para asegurarse de que no haya colisiones.
Loren Pechtel

1
@Loren: estrictamente, si la mesa tiene un tamaño fijo, hay una cantidad máxima de tiempo constante que puede pasar buscando un espacio libre. Es decir, a lo sumo, puede necesitar verificar n-1 ranuras ya ocupadas donde n es el tamaño de tabla constante. Entonces, una tabla hash de tamaño fijo es realmente O (1), sin necesidad de análisis amortizado. Esto no significa que no le importe que los accesos se vuelvan más lentos a medida que se llena la tabla, solo que no es lo que O grande expresa.
Steve314

25

Multiplicación matricial. El algoritmo ingenuo O (n ^ 3) a menudo se usa en la práctica como más rápido que el O de Strassen (n ^ 2.8) para matrices pequeñas; y Strassen se usa en lugar del algoritmo O (n ^ 2.3) Coppersmith – Winograd para matrices más grandes.



2
Coppersmith-Winograd NUNCA se usa. Implementarlo sería una tarea horrible en sí misma y la constante es tan mala que sería inviable incluso para los problemas de la matriz científica moderna.
tskuzzy

24

Un ejemplo simple es la diferencia entre varios algoritmos de clasificación. Mergesort, Heapsort y algunos otros son O (n log n) . Quicksort es O (n ^ 2) peor de los casos. Pero a menudo Quicksort es más rápido y, de hecho, funciona en promedio como O (n log n) . Más información .

Otro ejemplo es la generación de un solo número de Fibonacci. El algoritmo iterativo es O (n) , mientras que el algoritmo basado en matriz es O (log n) . Aún así, para el primer par de miles de números de Fibonacci, el algoritmo iterativo es probablemente más rápido. ¡Esto también depende de la implementación, por supuesto!

Los algoritmos con un mejor rendimiento asintótico pueden contener operaciones costosas que no son necesarias con un algoritmo con peor rendimiento pero operaciones más simples. Al final, la notación O solo nos dice algo sobre el rendimiento cuando el argumento en el que opera aumenta dramáticamente (se acerca al infinito).


Esta es una gran explicación de Big-O, pero no aborda la esencia de la pregunta, que es para casos específicos en los que un algoritmo O (n) será más rápido que un O (1).
KyleWpppd

El número uno de Fibonacci está ligeramente apagado. El tamaño de salida es exponencial en el tamaño de entrada, por lo que es una diferencia entre O (lg n * e ^ n) y O (lg lg n * e ^ n).
Peter Taylor

Anexo: en el mejor de los casos. El algoritmo basado en matriz multiplica con números del orden de 1.5 ^ n, por lo que O (lg lg n * ne ^ n) podría ser el mejor límite comprobable.
Peter Taylor

1
Quicksort normalmente se describe como O (n log n) rendimiento esperado de todos modos: el peor de los casos es bastante improbable para entradas aleatorias, y la creación de cierta aleatoriedad en una preparación previa o en la selección de pivote significa que el peor de los casos en general es muy poco probable para tamaños de entrada significativos. El peor de los casos es menos relevante que el hecho de que quicksort es (1) muy simple y (2) muy amigable con el caché, lo que conduce a factores constantes significativamente mejores que en muchos otros algoritmos de clasificación.
Steve314

(2) es precisamente el tipo de consideración externa que debe tenerse en cuenta al mirar el rendimiento big-O. Algorítmicamente, Mergesort siempre debe superar a Quicksort, pero el uso de recursos y la localidad de caché generalmente invierten sus posiciones de rendimiento en el mundo real.
Dan Lyons

18

Nota: Lea los comentarios de @ back2dos a continuación y otros gurús, ya que de hecho son más útiles que lo que he escrito. Gracias por todos los colaboradores.

Creo que en el cuadro a continuación (tomado de: notación Big O , busque "La naturaleza pesimista de los algoritmos:"), puede ver que O (log n) no siempre es mejor que decir, O (n). Entonces, supongo que su argumento es válido.

Pic-1


66
La pregunta quería ejemplos específicos de algoritmos en el mundo real. Esto no tiene ninguno como está.
Megan Walker

19
No puede ver nada en ese gráfico, eso respondería la pregunta. Es engañoso Este gráfico simplemente traza las funciones y = 1, y = log xetc., y la intersección de y = 1y y = xes en realidad el punto (1,1). Si esto fuera realmente correcto, de lo que te diría, que los algoritmos de mayor complejidad pueden ser más rápidos para 0 a 2 entradas, que es algo que a la gente apenas le importaría. Lo que el gráfico no tiene en cuenta por completo (y de dónde proviene la diferencia de rendimiento perceptible en cuestión) son factores constantes.
back2dos

@Samuel Walker, gracias por el comentario. El enlace provisto (Link-1) tiene algunos ejemplos de algoritmos por categoría.
NoChance

55
@ back2dos: el gráfico por sí solo no responde la pregunta, pero se puede usar para responderla. La forma de cada función mostrada es la misma para cualquier escala y factor constante. Con esto, el gráfico muestra que, dada una combinación de funciones, hay un rango de entradas para las cuales una es más pequeña y un rango de entradas para las cuales es la otra.
Jan Hudec

2
@dan_waterworth, tienes razón, concederé ese punto y eliminaré ese comentario. Sin embargo, la respuesta es incorrecta o engañosa en dos aspectos: 1) El punto principal de Big-O es que le da un límite superior a la complejidad; solo es significativo para n grande porque explícitamente arrojamos términos más pequeños que se ven abrumados por el término más grande a medida que n crece. 2) El punto de la pregunta es encontrar ejemplos de dos algoritmos en los que el que tiene el límite Big-O superior supera al que tiene un límite inferior. Esta respuesta falla porque no da tales ejemplos.
Caleb

11

Para valores prácticos de n, sí. Esto surge mucho en la teoría de CS. A menudo hay un algoritmo complicado que técnicamente tiene un mejor rendimiento big-Oh, pero los factores constantes son tan grandes que lo hacen impracticable.

Una vez hice que mi profesor de geometría computacional describiera un algoritmo para triangular un polígono en tiempo lineal, pero terminó con "muy complicado. ¡No creo que nadie lo haya implementado realmente" (!!).

Además, los montones de fibonacci tienen mejores características que los montones normales, pero no son muy populares porque no funcionan tan bien en la práctica como los montones normales. Esto puede conectarse en cascada a otros algoritmos que usan montones, por ejemplo, los caminos más cortos de Dijkstra son matemáticamente más rápidos con un montón de Fibonacci, pero generalmente no en la práctica.


Es más rápido para grandes gráficos del orden de aproximadamente 100,000 vértices.
tskuzzy

Los montones de Fibonacci fueron mi primer pensamiento (en realidad, el segundo) también.
Konrad Rudolph

10

Compare la inserción en una lista vinculada y la inserción en una matriz redimensionable.

La cantidad de datos tiene que ser bastante grande para que la inserción de la lista vinculada O (1) valga la pena.

Una lista vinculada tiene una sobrecarga adicional para los siguientes punteros y desreferencias. Una matriz redimensionable tiene que copiar datos. Esa copia es O (n), pero en la práctica es muy rápida.


1
Una matriz redimensionable se duplica en tamaño cada vez que se llena, por lo que el costo promedio del cambio de tamaño por inserción es O (1).
Kevin Cline

2
@kevincline, sí, pero la O (n) proviene de tener que mover todos los elementos después del punto de inserción hacia adelante. La asignación se amortiza O (1) tiempo. Mi punto es que ese movimiento sigue siendo muy rápido, por lo que en la práctica generalmente es mejor que las listas enlazadas.
Winston Ewert

La razón por la que las matrices contiguas son tan rápidas en comparación con las listas vinculadas se debe al almacenamiento en caché del procesador. Recorrer una lista vinculada provocará una pérdida de caché para cada elemento. Para obtener lo mejor de ambos mundos, debe usar una lista vinculada desenrollada .
dan_waterworth

Las matrices redimensionables no siempre se copian. Depende de en qué se esté ejecutando y si hay algo en el camino. Lo mismo para el tamaño de duplicación, implementación específica. Sin embargo, el rollo sobre rollo es un problema. Las listas enlazadas suelen ser las mejores para colas de tamaño desconocido, aunque los buffers rotativos les dan a las colas una buena oportunidad. En otros casos, las listas vinculadas son útiles porque la asignación o la expansión simplemente no le permitirán tener cosas contiguas todo el tiempo, por lo que necesitará un puntero de todos modos.
jgmjgm

@jgmjgm, si inserta en el medio de una matriz redimensionable, absolutamente copia los elementos después de eso.
Winston Ewert

8

La notación Big-Oh se usa para describir la tasa de crecimiento de una función, por lo que es posible que un algoritmo O (1) sea más rápido, pero solo hasta cierto punto (el factor constante).

Notaciones comunes:

O (1): el número de iteraciones (a veces se puede referir a esto como tiempo de usuario empleado por la función) no depende del tamaño de la entrada y, de hecho, es constante.

O (n): el número de iteraciones crece en una proporción lineal al tamaño de la entrada. Significado: si el algoritmo itera a través de cualquier entrada N, 2 * N veces, todavía se considera O (n).

O (n ^ 2) (cuadrático): el número de iteraciones es el tamaño de entrada al cuadrado.


2
Para agregar un ejemplo a una respuesta excelente: un método O (1) puede tomar 37 años por llamada, mientras que un método O (n) puede tomar 16 * n microsegundos por llamada. ¿Cual es mas rápido?
Kaz Dragon

16
No veo por completo cómo responde esto a la pregunta.
avakar

77
Entiendo big-O. Esto no aborda la pregunta real, que son ejemplos específicos de funciones donde los algoritmos con un big-O más bajo son superados por aquellos con un big-O más alto.
KyleWpppd

Cuando pones la pregunta en el formulario "¿Hay ejemplos ...", inevitablemente alguien responderá "Sí". sin dar ninguno.
rakslice

1
@rakslice: Quizás sí. Sin embargo, este sitio exige una explicación (o mejor aún, pruebas) de cualquier declaración que realice. Ahora, la mejor manera de demostrar que existen ejemplos es dar uno;)
back2dos

6

Las bibliotecas Regex generalmente se implementan para hacer un seguimiento inverso que tiene el peor tiempo exponencial en lugar de la generación DFA que tiene una complejidad de O(nm).

El retroceso ingenuo puede tener un mejor rendimiento cuando la entrada permanece en el camino rápido o falla sin la necesidad de retroceder excesivamente.

(Aunque esta decisión no solo se basa en el rendimiento, también es permitir referencias posteriores).


Creo que también es en parte histórico: el algoritmo para convertir una expresión regular en un DFA se patentó cuando se desarrollaron algunas de las herramientas anteriores (sed y grep). Por supuesto, escuché esto de mi profesor de compiladores que no estaba completamente seguro, así que esta es una cuenta de terceros.
Tikhon Jelvis el

5

Un O(1)algoritmo:

def constant_time_algorithm
  one_million = 1000 * 1000
  sleep(one_million) # seconds
end

Un O(n)algoritmo:

def linear_time_algorithm(n)
  sleep(n) # seconds
end

Claramente, para cualquier valor de ndónde n < one_million, el O(n)algoritmo dado en el ejemplo será más rápido que el O(1)algoritmo.

Si bien este ejemplo es un poco gracioso, es equivalente en espíritu al siguiente ejemplo:

def constant_time_algorithm
  do_a_truckload_of_work_that_takes_forever_and_a_day
end

def linear_time_algorithm(n)
  i = 0
  while i < n
    i += 1
    do_a_minute_amount_of_work_that_takes_nanoseconds
  end
end

Usted debe conocer las constantes y coeficientes en su Oexpresión, y usted debe saber el rango esperado de n, a fin de determinar a priori qué algoritmo va a terminar siendo más rápido.

De lo contrario, debe comparar los dos algoritmos con valores de nen el rango esperado para determinar a posteriori qué algoritmo terminó siendo más rápido.


4

Clasificación:

El orden de inserción es O (n ^ 2) pero supera a otros algoritmos de clasificación O (n * log (n)) para un pequeño número de elementos.

Esta es la razón por la cual la mayoría de las implementaciones de clasificación utilizan una combinación de dos algoritmos. Por ejemplo, utilice la ordenación por fusión para desglosar las matrices grandes hasta que alcancen un tamaño determinado, luego use la ordenación por inserción para ordenar las unidades más pequeñas y fusionarlas nuevamente con la ordenación por fusión.

Consulte Timsort la implementación predeterminada actual de la ordenación de Python y Java 7 que utiliza esta técnica.



3

La clasificación de burbujas en la memoria puede superar a la clasificación rápida cuando el programa se intercambia al disco o necesita leer cada elemento del disco al comparar.

Este debería ser un ejemplo con el que pueda relacionarse.


¿Las complejidades citadas en quicksort y bubbleort no suponen O (1) acceso aleatorio a la memoria? Si este ya no es el caso, ¿no sería necesario reexaminar la complejidad de las clasificaciones rápidas?
Viktor Dahl

@ViktorDahl, el tiempo de acceso al elemento no es parte de lo que tradicionalmente se mide en las complejidades del algoritmo de clasificación, por lo que "O (1)" no es la elección correcta de palabras aquí. Use "tiempo constante" en su lugar. PHK escribió un artículo hace un tiempo sobre algoritmos de clasificación, sabiendo que algunos elementos son más caros de recuperar que otros (memoria virtual) - queue.acm.org/detail.cfm?id=1814327 - puede resultarle interesante.

Ahora veo mi error. Por lo general, se mide el número de comparaciones y, por supuesto, no se ven afectadas por la velocidad del medio de almacenamiento. Además, gracias por el enlace.
Viktor Dahl

3

A menudo, los algoritmos más avanzados suponen una cierta cantidad de configuración (costosa). Si solo necesita ejecutarlo una vez, podría estar mejor con el método de fuerza bruta.

Por ejemplo: la búsqueda binaria y la búsqueda de la tabla hash son mucho más rápidas por búsqueda que una búsqueda lineal, pero requieren que ordene la lista o cree la tabla hash, respectivamente.

El tipo le costará N log (N) y la tabla hash costará al menos N. Ahora si va a realizar cientos o miles de búsquedas, eso sigue siendo un ahorro amortizado. Pero si solo necesita hacer una o dos búsquedas, podría tener sentido hacer una búsqueda lineal y ahorrar el costo de inicio.


1

El descifrado es a menudo 0 (1). Por ejemplo, el espacio clave para DES es 2 ^ 56, por lo que el descifrado de cualquier mensaje es una operación de tiempo constante. Es solo que tienes un factor de 2 ^ 56, así que es una constante realmente grande.


¿No es el descifrado de un mensaje O ( n ), donde n es proporcional al tamaño del mensaje? Mientras tenga la clave correcta, el tamaño de la clave ni siquiera tiene en cuenta; algunos algoritmos tienen procesos de configuración / expansión mínimos o nulos (DES, RSA; tenga en cuenta que la generación de claves aún puede ser una tarea compleja, pero eso no tiene nada que ver con la expansión de claves) mientras que otros son extremadamente complejos (viene a la mente Blowfish), pero Una vez hecho, el tiempo para hacer el trabajo real es proporcional al tamaño del mensaje, por lo tanto, O (n).
un CVn

¿Probablemente te refieres al criptoanálisis en lugar de descifrar?
Leftaroundabout

3
Bueno, sí, hay muchas cosas que puede tomar para ser constante y declarar un algoritmo como O (1). [clasificación supone implícitamente los elementos toman una cantidad constante de tiempo para comparar, por ejemplo, o cualquier matemáticas con números no bignum]
Random832

1

Me vienen a la mente diferentes implementaciones de conjuntos. Uno de los más ingenuos es implementarlo sobre un vector, lo que significa removeque, containsy por lo tanto, también addtodos toman O (N).
Una alternativa es implementarlo sobre algún hash de propósito general, que asigna los hashes de entrada a los valores de entrada. Tal implementación de conjunto se realiza con O (1) para add, containsy remove.

Si suponemos que N es aproximadamente 10, entonces la primera implementación es probablemente más rápida. Todo lo que tiene que hacer para encontrar un elemento es comparar 10 valores con uno.
La otra implementación tendrá que iniciar todo tipo de transformaciones inteligentes, que pueden ser mucho más caras, que hacer 10 comparaciones. Con toda la sobrecarga, incluso puede tener errores de caché y, en teoría, realmente no importa cuán rápida sea su solución.

Esto no significa que la peor implementación que se te ocurra superará a una decente, si N es lo suficientemente pequeña. Simplemente significa para un N suficientemente pequeño, que una implementación ingenua, con poca huella y sobrecarga, puede requerir menos instrucciones y causar menos errores de caché que una implementación que prioriza la escalabilidad y, por lo tanto, será más rápida.

Realmente no puedes saber qué tan rápido es algo en un escenario del mundo real, hasta que lo pones en uno y simplemente lo mides. A menudo los resultados son sorprendentes (al menos para mí).


1

Sí, para un N. adecuadamente pequeño Siempre habrá una N, por encima de la cual siempre tendrá el orden O (1) <O (lg N) <O (N) <O (N log N) <O (N ^ c ) <O (c ^ N) (donde O (1) <O (lg N) significa que en un algoritmo O (1) tomará menos operaciones cuando N es adecuadamente grande y c es una constante fija que es mayor que 1 )

Digamos que un algoritmo O (1) particular toma exactamente f (N) = 10 ^ 100 (un googol) operaciones y un algoritmo O (N) toma exactamente g (N) = 2 N + 5 operaciones. El algoritmo O (N) dará un mayor rendimiento hasta que N sea aproximadamente un googol (en realidad cuando N> (10 ^ 100 - 5) / 2), por lo que si solo esperaba que N estuviera en el rango de 1000 a mil millones sufriría una penalización importante usando el algoritmo O (1).

O para una comparación realista, digamos que está multiplicando números de n dígitos. El algoritmo de Karatsuba es a lo sumo 3 n ^ (lg 3) operaciones (que es aproximadamente O (n ^ 1.585)) mientras que el algoritmo de Schönhage-Strassen es O (N log N log log N) que es un orden más rápido , pero para citar wikipedia:

En la práctica, el algoritmo Schönhage – Strassen comienza a superar a los métodos más antiguos, como la multiplicación de Karatsuba y Toom – Cook para números más allá de 2 ^ 2 ^ 15 a 2 ^ 2 ^ 17 (10,000 a 40,000 dígitos decimales). [4] [5] [6 ]

Entonces, si está multiplicando números de 500 dígitos, no tiene sentido usar el algoritmo que es "más rápido" por grandes argumentos O.

EDITAR: Puede encontrar determinar f (N) comparado g (N), tomando el límite N-> infinito de f (N) / g (N). Si el límite es 0, entonces f (N) <g (N), si el límite es infinito, entonces f (N)> g (N), y si el límite es alguna otra constante, entonces f (N) ~ g (N) en términos de notación O grande.


1

El método simplex para la programación lineal puede ser exponencial en el peor de los casos, mientras que los algoritmos de punto interior relativamente nuevos pueden ser polinómicos.

Sin embargo, en la práctica, el peor caso exponencial para el método simplex no aparece: el método simplex es rápido y confiable, mientras que los primeros algoritmos de punto interior eran demasiado lentos para ser competitivos. (Ahora hay algoritmos de puntos interiores más modernos que son competitivos, pero el método simple también lo es ...)


0

El algoritmo de Ukkonen para construir intentos de sufijo es O (n log n). Tiene la ventaja de estar "en línea", es decir, puede agregar más texto de forma incremental.

Recientemente, otros algoritmos más complejos han afirmado ser más rápidos en la práctica, en gran parte porque su acceso a la memoria tiene una mayor localidad, lo que mejora la utilización de la memoria caché del procesador y evita los bloqueos de la tubería de la CPU. Véase, por ejemplo, esta encuesta , que afirma que el 70-80% del tiempo de procesamiento se gasta esperando memoria, y este documento describe el algoritmo "wotd".

Los intentos de sufijo son importantes en genética (para la coincidencia de secuencias de genes) y, algo menos importante, en la implementación de diccionarios Scrabble.


0

Siempre existe el algoritmo más rápido y más corto para cualquier problema bien definido . Sin embargo, es puramente teóricamente el algoritmo más rápido (asintóticamente).

Dado cualquier descripción de un problema P y una instancia para ese problema I , enumera todos los algoritmos posibles A y pruebas Pr , la comprobación de cada uno de tales pares si Pr es una prueba válida que A es el algoritmo asintóticamente más rápida para P . Si encuentra una prueba de este tipo, a continuación, ejecuta una de I .

La búsqueda de este par a prueba de problemas tiene una complejidad O (1) (para un problema fijo P ), por lo que siempre utiliza el algoritmo asintóticamente más rápido para el problema. Sin embargo, dado que esta constante es tan indescriptiblemente enorme en casi todos los casos, este método es completamente inútil en la práctica.


0

Muchos lenguajes / frameworks usan una coincidencia de patrones ingenua para unir cadenas en lugar de KMP . Buscamos cuerdas como Tom, Nueva York en lugar de ababaabababababaababababababab.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.