1 Eficiencia

1.1 Conjuntos y diccionarios

1.1.1 Tipos de datos mutables e inmutables

sage: xs = [3,2,5]
sage: ys = xs.reverse()
sage: xs
[5, 2, 3]

1.1.2 Conjuntos

sage: c1 = set()
sage: list(c1)
[]
sage: c1.add(1)
sage: list(c1)
[1]
sage: c1.update([1,2,6,7])
sage: list(c1)
[1, 2, 6, 7]
sage: c1.remove(7)
sage: list(c1)
[1, 2, 6]
sage: c2 = set([1,2,3,4,1])
sage: list(c2)
[1, 2, 3, 4]
sage: c1 | c2
set([1, 2, 3, 4, 6])
sage: c1 & c2
set([1, 2])
sage: c1 - c2
set([6])
sage: c2 - c1
set([3, 4])
sage: 6 in c2 - c1
False
sage: 4 in c2 - c1
True
sage: c1.add([1,2])
TypeError: unhashable type: 'list'
sage: c1.add(c2)
TypeError: unhashable type: 'set'
sage: c1 <= c1 | c2
True
sage: c1 <= c2
False
sage: for x in c1:
....:    print x
....: 
1
2
6

1.1.2.1 Ejemplo: conjunto de sumas

sage: conjunto_sumas(srange(5))
[0, 1, 2, 3, 4, 5, 6, 7, 8]
# 1ª definición (por iteración):
def conjunto_sumas(xs):
    sumas = []
    for k in xs:
        for j in xs:
            if not(k+j in sumas):
                sumas.append(k+j)
    return sumas

# 2ª definición (por iteración con conjuntos):
def conjunto_sumas2(xs):
    sumas = set()
    for k in xs:
        for j in xs:
            sumas.add(k+j)
    return list(sumas)
        
# 3ª definición (por comprensión con conjuntos):
def conjunto_sumas3(xs):
    return list(set([x+y for x in xs for y in xs]))

1.1.2.2 Usar un conjunto para eliminar elementos repetidos de una lista

sage: lista = [3, 3, 2, 1, 1, 6, 1, 2, 3, 3, 7, 3, 7]
sage: conjunto = set(lista)
sage: lista_sin_repeticiones = list(conjunto)
sage: lista_sin_repeticiones
[1, 2, 3, 6, 7]

1.1.3 Diccionarios

sage: diccionario = {'cabeza':3, 'nariz':2, 'mano':4}
sage: diccionario
{'mano': 4, 'cabeza': 3, 'nariz': 2}
sage: diccionario['mano']=2
sage: diccionario
{'mano': 2, 'cabeza': 3, 'nariz': 2}
sage: diccionario['pie']=2
sage: diccionario
{'mano': 2, 'pie': 2, 'cabeza': 3, 'nariz': 2}
sage: del diccionario['nariz']
sage: diccionario
{'mano': 2, 'pie': 2, 'cabeza': 3}
sage: diccionario['cabeza']
3
sage: diccionario.keys()
['mano', 'pie', 'cabeza']
sage: diccionario.values()
[2, 2, 3]
sage: 'pie' in diccionario
True
sage: 3 in diccionario
False
sage: for palabra in diccionario:
....:     print 'La palabra %s tiene %d vocales'%(palabra,diccionario[palabra])
....: 
La palabra mano tiene 2 vocales
La palabra pie tiene 2 vocales
La palabra cabeza tiene 3 vocales
sage: [len(x) for x in diccionario]
[4, 3, 6]

1.1.3.1 Ejemplo: mínimo común múltiplo

sage: factor(200)
2^3 * 5^2
sage: list(factor(200))
[(2, 3), (5, 2)]
sage: mcm([12,15,15,18])
180
def mcm(ls):
    #Primera parte: recopilar los factores
    factores = {}
    for numero in ls:
        for par in list(factor(numero)):
            primo, exponente = par
            if primo not in factores:
                factores[primo] = exponente
            else:
                factores[primo] = max(factores[primo], exponente)
    #Segunda parte: multiplicarlos
    resultado = 1
    for primo in factores:
        resultado = resultado*primo^factores[primo]
    return resultado

1.1.3.2 Construir diccionarios a partir de listas

sage: lista = [('a',1), ('b',2), ('c',3)]
sage: diccionario = dict(lista)
sage: diccionario
{'a': 1, 'c': 3, 'b': 2}
sage: dict(zip(['a','b','c'],[3,2,5]))
{'a': 3, 'c': 5, 'b': 2}

1.1.4 Cómo funcionan los conjuntos y los diccionarios

1.1.4.1 Hash

sage: hash(7)
7
sage: hash('ab')
-1549758589
sage: hash((3,2))
1697177666
sage: hash((3,2))
1697177666
sage: hash('ab')
-1549758589

1.1.4.2 Tabla hash

1.2 Tiempo de ejecución y eficiencia de algoritmos

1.2.1 Medir el tiempo de CPU

sage: time is_prime(factorial(500)+1)
False
Time: CPU 0.29 s, Wall: 0.33 s
sage: timeit('2^10000')
625 loops, best of 3: 2.79 µs per loop
sage: timeit('2+2',precision=2,number=20,repeat=5)
20 loops, best of 5: 1.7 µs per loop
sage: timeit('2^10000', preparse=False, number=50)
50 loops, best of 3: 62 ns per loop

1.2.1.1 Gráficas de tiempo de ejecución

sage: cputime()
2.12
sage: x = factorial(1000000)
sage: cputime()
3.588
sage: walltime()
1407401063.545968
sage: x = factorial(1000000)
sage: walltime()
1407401076.657046
def tiempos_factoriales(ns):
    """
    Tiempo de los factoriales de ns.
       sage: sage: tiempos_factoriales([2^j for j in range(17,20)])
       [0.1120000000000001, 0.3000000000000007, 0.7239999999999984]
    """
    tiempos = []
    for n in ns:
        tcpu0 = cputime()
        x = factorial(n)
        tiempos.append(cputime()-tcpu0)
    return tiempos

def grafica_tiempos_factoriales(ns):
    """
    Dibuja una gráfica con los tiempos de los factoriales de ns. Por
    ejemplo,
       sage: grafica_tiempos_factoriales([2^j for j in range(8,20)])
       Dibuja ../fig/Tiempos_factoriales.png
    """
    g = line(zip(ns,tiempos_factoriales(ns)))
    g.show()

La gráfica es

1.2.2 Contar el número de operaciones

1.2.2.1 O, \Omega y \Theta

1.2.2.2 Complejidad

1.2.2.3 Coste en el peor caso y coste promedio

1.2.3 ¿Cómo funcionan las listas?

def elimina_por_detras(xs):
    """
    Elimina los elementos de xs por detrás.
    """
    while len(xs)>0:
        del xs[-1]

def elimina_por_delante(xs):
    """
    Elimina los elementos de xs por delante.
    """
    while len(xs)>0:
        del xs[0]

# Comparación:
#    sage: time elimina_por_detras(range(100000))
#    Time: CPU 0.06 s, Wall: 0.07 s
#    sage: time elimina_por_delante(range(100000))
#    Time: CPU 45.21 s, Wall: 45.51 s
def agrega_inicio(xs,ys):
    """
    Agrega los elementos de xs al inicio de ys. Por ejemplo,
       sage: agrega_inicio([3,5,7],[2,4])
       [7, 5, 3, 2, 4]
    """
    zs = ys
    for x in xs:
        zs.insert(0,x)
    return zs

def agrega_final(xs,ys):
    """
    Agrega los elementos de xs al final de ys. Por ejemplo,
       sage: agrega_final([3,5,7],[2,4])
       [2, 4, 3, 5, 7]
    """
    zs = ys
    for x in xs:
        zs.append(x)
    return zs
    
# Comparación:
#    sage: time x = agrega_inicio(srange(50000),[])
#    Time: CPU 1.52 s, Wall: 1.52 s
#    sage: time x = agrega_final(srange(50000),[])
#    Time: CPU 0.03 s, Wall: 0.04 s

1.2.4 Coste de las operaciones usuales con listas

1.2.4.1 Añadir un elemento

1.2.4.2 Insertar un elemento en una posición arbitraria

1.2.4.3 Quitar elementos

1.2.4.4 Acceder o actualizar elementos

1.2.5 Comparativa entre listas y diccionarios

1.2.5.1 Ejemplo: Intersección de listas y conjuntos

# 1ª definición (Intersección con listas):
def interseccion1(xs,ys):
    return [x for x in xs if x in ys]

# 2ª definición (usando pertenencia de conjuntos):
def interseccion2(xs,ys):
    c = set(ys)
    return [x for x in xs if x in c]

# 3ª definición (usando intersección de conjuntos):
def interseccion3(xs,ys):
    return list(set(xs) & set(ys))

# Comparaciones
#    sage: n = 5000
#    sage: xs = [randint(1,10*n) for k in range(n)]
#    sage: ys = [randint(1,10*n) for k in range(n)]
#    sage: time zs = interseccion1(xs, ys)
#    CPU 3.33 s, Wall: 3.38 s
#    sage: time zs = interseccion2(xs, ys)
#    CPU 0.00 s, Wall: 0.01 s
#    sage: time zs = interseccion3(xs, ys)
#    CPU 0.00 s, Wall: 0.00 s

1.2.5.2 Ejemplo: Números triangulares, pentagonales y hexagonales

def triangulares(n):
    """
    Conjunto de los números triangulares menores que n. Por ejemplo,
       sage: triangulares(20)
       set([1, 10, 3, 6, 15])
    """
    k=1
    triangulares = set()
    while k*(k+1)/2<n:
        triangulares.add(k*(k+1)/2)
        k = k+1
    return triangulares

def pentagonales(n):
    """
    Conjunto de los números pentagonales menores que n. Por ejemplo,
       sage: pentagonales(100)
       set([1, 35, 5, 70, 12, 51, 22, 92])
    """
    k=1
    pentagonales = set()
    while k*(3*k-1)/2<n:
        pentagonales.add(k*(3*k-1)/2)
        k = k+1
    return pentagonales

def hexagonales(n):
    """
    Conjunto de los números hexagonales menores que n. Por ejemplo,
       sage: hexagonales(100)
       set([1, 66, 6, 45, 15, 91, 28])
    """
    k=1
    hexagonales = set()
    while k*(2*k-1)<n:
        hexagonales.add(k*(2*k-1))
        k = k+1
    return hexagonales

def numeros_tph(n):
    '''
    Conjunto de los números menores que n que son a la vez triangulares,
    pentagonales y hexagonales. Por ejemplo, 
       sage: numeros_tph(100)
       set([1])
    '''
    return triangulares(n) & pentagonales(n) & hexagonales(n)

# Cálculos:
#    sage: time numeros_tph(1e6)
#    set([1, 40755])
#    Time: CPU 0.11 s, Wall: 0.11 s
#    sage: time numeros_tph(1e8)
#    set([1, 40755])
#    Time: CPU 1.02 s, Wall: 1.02 s
#    sage: time numeros_tph(1e10)
#    set([1, 40755, 1533776805])
#    Time: CPU 11.38 s, Wall: 11.44 s

1.3 Eficiencia en cálculo científico

1.3.1 Optimiza sólo si es necesario, sólo dónde es necesario

1.3.2 Caso práctico: la constante de Brun

sage: primos(40)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37]
def primos(k):
    def criba(xs):
        ys = []
        while xs:
            p = xs[0]
            ys.append(p)
            xs = [k for k in xs if k % p != 0]
        return ys
    return criba(range(2,k))
sage: gemelos([2,3,5,7,8,10])
[3, 5, 8]
def gemelos(xs):
    return [x for x in xs if x+2 in xs]
sage: brun(1e4)
1.61689355743220
def brun(k):
    return sum((1.0/x + 1/(x+2)) for x in gemelos(primos(k)))
sage: tiempos_brun([2^n for n in srange(14,17)])
[0.9799999999999898, 3.3760000000000048, 12.352000000000004]
def tiempos_brun(ns):
    tiempos = []
    for n in ns:
        tcpu0 = cputime()
        x = brun(n)
        tiempos.append(cputime()-tcpu0)
    return tiempos
sage: import cProfile, pstats
sage: cProfile.runctx("brun(10000)", globals(), locals(), "Profile.prof")
sage: s = pstats.Stats("Profile.prof")
sage: s.strip_dirs().sort_stats("time").print_stats()
Fri Aug  8 13:53:46 2014    Profile.prof

         1446 function calls in 0.422 seconds

   Ordered by: internal time

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.285    0.285    0.287    0.287 <string>:14(criba)
        1    0.130    0.130    0.130    0.130 <string>:23(gemelos)
      206    0.003    0.000    0.003    0.000 <string>:39(<genexpr>)
     1229    0.002    0.000    0.002    0.000 {method 'append' of 'list' objects}
        1    0.001    0.001    0.001    0.001 {range}
        1    0.001    0.001    0.289    0.289 <string>:8(primos)
        1    0.001    0.001    0.003    0.003 {sum}
        1    0.000    0.000    0.422    0.422 <string>:32(brun)
def gemelos2(xs):
    return [xs[j] for j in xrange(len(xs)-1) if xs[j+1]==xs[j]+2]

def gemelos2a(xs):
    return [x for (x,y) in zip(xs,xs[1:]) if x+2 == y]
def primos2(n):
    aux = [True]*int(n)
    aux[0] = False
    aux[1] = False
    for i in xrange(2,floor(sqrt(n))+1):
        if aux[i]:
            for j in xrange(i*i,n,i):
                aux[j] = False
    return [k for k in xrange(n) if aux[k]]
def brun2(k):
    return sum((1.0/x + 1/(x+2)) for x in gemelos2(primos2(k)))
# Tiempos
#    sage: cProfile.runctx("brun2(10000)", globals(), locals(), "Profile.prof")
#    sage: s = pstats.Stats("Profile.prof")
#    sage: s.strip_dirs().sort_stats("time").print_stats()
#    Fri Aug  8 14:22:05 2014    Profile.prof
#    
#             244 function calls in 0.018 seconds
#    
#       Ordered by: internal time
#    
#       ncalls  tottime  percall  cumtime  percall filename:lineno(function)
#            1    0.009    0.009    0.012    0.012 <string>:174(primos2)
#          206    0.004    0.000    0.004    0.000 <string>:191(<genexpr>)
#            1    0.002    0.002    0.002    0.002 <string>:152(gemelos3)
#            1    0.001    0.001    0.001    0.001 homset.py:40(Hom)
#            1    0.001    0.001    0.002    0.002 other.py:1545(_do_sqrt)
#            1    0.001    0.001    0.004    0.004 {sum}
#            1    0.000    0.000    0.000    0.000 other.py:544(__call__)
#            1    0.000    0.000    0.000    0.000 {zip}
#            1    0.000    0.000    0.018    0.018 <string>:184(brun2)

1.4 Ejercicios

1.4.1 Conjuntos y diccionarios

1.4.1.1 Factores primos de una lista

# 1ª definición (por iteración):
def factores(xs):
    c = set()
    for x in xs:
        for (p,e) in factor(x):
            c.add(p)
    return list(c)

# 2ª definición (por comprensión):
def factores2(xs):
    return list(set([p for x in xs for (p,e) in factor(x)]))

1.4.1.2 Frecuencia de las letras de una cadena.

sage: frecuencia_letras("Todo para nada")
{'a': 4, 'd': 2, 'o': 2, 'n': 1, 'p': 1, 'r': 1, 'T': 1}
def frecuencia_letras(cs):
    d = dict([])
    for c in cs:
        if c in d:
            d[c] = 1 + d[c]
        elif c.isalpha():
            d[c] = 1
    return d

1.4.1.3 Frecuencia de palabras en un texto

sage: frecuencia_palabras("nunca como como nunca como")
{'nunca': 2, 'como': 3}
def frecuencia_palabras(cs):
    ps = cs.split()
    d = dict([])
    for p in ps:
        if p in d:
            d[p] = 1 + d[p]
        else:
            d[p] = 1
    return d

1.4.1.4 Conjuntos y el problema de Collatz

load Tiempo.sage

# 1ª definición (por iteración):
def conjetura_Collatz(n):
    for k in range(2,n):
        j = k
        while j != 1:
            if is_even(j):
                j = j/2
            else:
                j =  3*j + 1
    return True

# 2ª definición (parando en los menores):
def conjetura_Collatz2(n):
    for k in range(2,n):
        j = k
        while j >= k:
            if is_even(j):
                j = j/2
            else:
                j =  3*j + 1
    return True

# 3ª definición (con conjuntos)
def conjetura_Collatz3(n):
    s = set()
    for k in range(1,n):
        j = k
        while j != 1 and not (j in s):
            s.add(j)
            if is_even(j):
                j = j/2
            else:
                j =  3*j + 1
    return True

# Comparación de eficiencia
#    sage: tiempo1(conjetura_Collatz,500)
#    2.525918960571289
# 
#    sage: tiempo1(conjetura_Collatz2,500)
#    0.3285210132598877
# 
#    sage: tiempo1(conjetura_Collatz3,500)
#    0.10817289352416992

1.4.1.5 Conjetura de Goldbach

def goldbach(n): 
    pares = set(srange(4,n,2))
    primos = [x for x in srange(n) if is_prime(x)]
    suma_dos_primos = set([x+y for x in primos for y in primos])
    return pares <= suma_dos_primos

1.4.1.6 Dos dados

sage: dos_dados()[3]
[(1, 2), (2, 1)]
sage: dos_dados()[7]
[(1, 6), (2, 5), (3, 4), (4, 3), (5, 2), (6, 1)]
def dos_dados():
    d = dict([(k,[]) for k in srange(2,13)])
    for i in srange(1,7):
        for j in srange(1,7):
            d[i+j].append((i,j))
    return d

1.4.2 Gráficas de tiempo de ejecución

1.4.2.1 Pares como sumas de dos primos

sage: sumas(20)
[(3, 17), (7, 13), (13, 7), (17, 3)]
sage: sumas2(21)
[]
# 1ª definición (con listas):
def sumas1(n):
    if is_odd(n):
        return []
    else:
        lista = []
        primos = prime_range(n)
        for k in primos:
            if n-k in primos:
                lista.append((k,n-k))
        return lista

# 1ª definición (con conjuntos):
def sumas2(n):
    if is_odd(n):
        return []
    else:
        lista = []
        primos = set(prime_range(n))
        for k in primos:
            if n-k in primos:
                lista.append((k,n-k))
        return lista

# 3ª definición (con conjuntos y comprensión):
def sumas3(n):
    primos = set(prime_range(n))
    return [(k,n-k) for k in primos if n-k in primos]

# Comparaciones
#    sage: time x = sumas1(10000)
#    Time: CPU 0.25 s, Wall: 0.25 s
#    sage: time x = sumas2(10000)
#    Time: CPU 0.00 s, Wall: 0.00 s
#    sage: time x = sumas3(10000)
#    Time: CPU 0.01 s, Wall: 0.01 s
#    
#    sage: time x = sumas2(1000000)
#    Time: CPU 0.34 s, Wall: 0.34 s
#    sage: time x = sumas3(1000000)
#    Time: CPU 0.34 s, Wall: 0.34 s

def tiempos_sumas1(ns):
    """
    Tiempo de sumas1 para los elementos de ns.
       sage: tiempos_sumas1(srange(1000,1006,2))
       [0.007999999999995566, 0.004001000000002364, 0.0040000000000048885]
    """
    tiempos = []
    for n in ns:
        tcpu0 = cputime()
        x = sumas1(n)
        tiempos.append(cputime()-tcpu0)
    return tiempos

def grafica_tiempos_sumas1(ns):
    """
    Dibuja una gráfica con los tiempos sumas1 de los elementos de ns. Por
    ejemplo,
       sage: grafica_tiempos_sumas1(srange(1000,1500,2))
       Dibuja ../fig/Tiempos_sumas_de_dos_primos.png
    """
    g = line(zip(ns,tiempos_sumas1(ns)))
    g.show()

def tiempos_sumas2(ns):
    """
    Tiempo de sumas2 para los elementos de ns.
       sage: tiempos_sumas2(srange(1000,1006,2))
       [0.0, 0.004000000000019099, 0.0]
    """
    tiempos = []
    for n in ns:
        tcpu0 = cputime()
        x = sumas2(n)
        tiempos.append(cputime()-tcpu0)
    return tiempos

def grafica_tiempos_sumas2(ns):
    """
    Dibuja una gráfica con los tiempos sumas2 de los elementos de ns. Por
    ejemplo,
       sage: grafica_tiempos_sumas2(srange(1000,1500,2))
       Dibuja ../fig/Tiempos_sumas_de_dos_primos2.png
    """
    g = line(zip(ns,tiempos_sumas2(ns)))
    g.show()

La gráfica de los tiempos de sumas1 es

La gráfica de los tiempos de sumas2 es

1.4.2.2 Menor primo con k cifras decimales distintas.

  • Ejercicio. Calcular el menor número primo con k dígitos decimales distintos, para k=5,6,7.

  • Solución.

def decimales(n):
    """
    Números de cifras distintas de n. Por ejemplo,
       sage: decimales(42554)
       3
    """
    return len(set(str(n)))

def primo_decimales(k,n):
    """
    El menor primo menor que n con k cifras distintas. Por ejemplo,
       sage: primo_decimales(5,10000000)
       10243
       sage: primo_decimales(6,10000000)
       102359
       sage: primo_decimales(7,10000000)
       1023467
    """
    primos = prime_range(n)
    for x in primos:
        if decimales(x) == k:
            return x

def primo_decimales2(k):
    """
    El menor primo menor con k cifras distintas. Por ejemplo,
       sage: primo_decimales2(5)
       10243
       sage: primo_decimales2(6)
       102359
       sage: primo_decimales2(7)
       1023467
    """
    p = 2
    while decimales(p) != k:
        p = next_prime(p)
    return p
        
# Comparación
#    sage: time primo_decimales(7,10000000)
#    1023467
#    Time: CPU 1.94 s, Wall: 1.94 s
#    sage: time primo_decimales2(7)
#    1023467
#    Time: CPU 3.88 s, Wall: 3.89 s

# Se puede mejorar ajustando la cota inferior, ya que si n tiene k
# cifras distintas entonces n > 10^(k-1)

def primo_decimales3(k,n):
    """
    El menor primo menor que n con k cifras distintas. Por ejemplo,
       sage: primo_decimales3(5,10000000)
       10243
       sage: primo_decimales3(6,10000000)
       102359
       sage: primo_decimales3(7,10000000)
       1023467
    """
    primos = prime_range(10^(k-1),n)
    for x in primos:
        if decimales(x) == k:
            return x

# Comparación
#    sage: time primo_decimales3(7,10000000)
#    1023467
#    Time: CPU 0.49 s, Wall: 0.50 s

def primo_decimales4(k):
    """
    El menor primo menor con k cifras distintas. Por ejemplo,
       sage: primo_decimales4(5)
       10243
       sage: primo_decimales4(6)
       102359
       sage: primo_decimales4(7)
       1023467
    """
    p = 10^(k-1)
    while decimales(p) != k:
        p = next_prime(p)
    return p

# Comparación:
#    sage: time primo_decimales4(7)
#    1023467
#    Time: CPU 0.10 s, Wall: 0.09 s