                                CC40A
                                =====

Author: Jérémy Barbay <jeremy.barbay@dcc.uchile.cl>
Date: 2009-11-18 19:05:54 CLST


Table of Contents
=================
1 Introducción 
    1.1 Filosofía del curso: 
    1.2 Programa del curso previo, CC30A: 
    1.3 Programa Oficial del curso CC40A: 
    1.4 Otros Recursos 
        1.4.1 Programa en la  Pagina de Ricardo, de 2000 
        1.4.2 Pagina de Gonzalo sobre el curso 
2 Complejidad y Fundamentos Matemáticos (7 clases) 
    2.1 Proceso de diseño y análisis de un algoritmo 
    2.2 Complejidad en Promedio 
    2.3 Análisis de Algoritmos 
    2.4 Recurrencias 
    2.5 Recurrencias Part 2: Teorema Maestro 
3 Técnicas básicas de diseño de algoritmos (6 clases) 
    3.1 Técnicas clásicas de Diseño de Algoritmos 
    3.2 Dividar para Reinar 
    3.3 Caminos mínimos en grafos y análisis amortizado 
    3.4 Ejemplo:  Algoritmo Median 
    3.5 Programación Dinámica 
4 Técnicas avanzadas de diseño de algoritmos (8 clases) 
    4.1 Uso de dominios discretos y finitos 
    4.2 Tries o Arboles  Digitales 
    4.3 Colas de prioridades y Heapsort 
    4.4 Memoria secundaria 
    4.5 Merge Sort en Memoria Secundaria 
    4.6 van Emde Boas 
    4.7 Online Algorithms - List Accessing 
    4.8 Algoritmos en Linea: Paginamiento Deterministico 
5 Algoritmos no convencionales (9 clases) 
    5.1 Algoritmos Aleatorizados 
    5.2 Arboles Binarios de Busque da aleatorizados 
    5.3 SkipLists 
    5.4 Complejidad Probabilistica: cotas inferiores 
    5.5 Paginamiento al Azar
    5.6 Hashing 
    5.7 Algoritmos Aproximados 
    5.8 Algoritmos paralelos 


1 Introducción 
~~~~~~~~~~~~~~~

1.1 Filosofía del curso: 
=========================

   Analizar la eficiencia de diversos algoritmos para resolver una
   variedad de problemas, principalmente no numéricos. Enseñar al
   alumno a diseñar y analizar nuevos algoritmos.

1.2 Programa del curso previo, CC30A: 
======================================
   (copiado de [http://www.dcc.uchile.cl/web/article-22130.html] el [2009-10-20 Tue])

   1. Métodos de programación

       * Nociones básicas de verificación de programas
       * Invariantes
       * Diagramas de estados
       * Recursividad
       * Co-rutinas
       * Desarrollo de programas "top-down" y "bottom-up"
       * Encapsulamiento y tipos de datos abstractos (TDAs)

   2. Nociones de análisis de algoritmos

       * Cómo medir la eficiencia de los algoritmos: pero caso, caso promedio, costo amortizado
       * Notación "O"
       * Técnicas para plantear y resolver ecuaciones de recurrencia

   3. Técnicas básicas para la estructuración de datos

       * Arreglos, punteros, listas, árboles

   4. TDAs básicos

       * Stacks (con aplicaciones a eliminación de recursividad), Colas

   5. TDA "diccionario"

       * DEFINICIÓN, variantes
       * Implementaciones en base a búsqueda secuencial
       * Búsqueda binaria
       * "Skip lists"
       * Arboles de búsqueda binaria
       * Arboles óptimos
       * Arboles balanceados : AVL, 2-3
       * B-trees
       * "Splay trees"
       * Arboles digitales
       * "Hashing"

   6. Métodos de ordenación

       * Cota inferior
       * Métodos cuadráticos
       * "Quicksort"
       * "Heapsort"
       * "Bucketsort"
       * Ordenamiento externo

   7. Búsqueda en texto

       * Método de fuerza bruta
       * Knuth-Morris-Pratt
       * Boyer-Moore-Horspool

   8. Algoritmos en grafos

       * Representación de grafos
       * Recorridos
       * Arboles de costo mínimo
       * Distancias mínimas
       * Algoritmos probabilísticos
       * Problemas NP-completos

1.3 Programa Oficial del curso CC40A: 
======================================

   (Copiado de [http://www.dcc.uchile.cl/docs/CC40A-2007.pdf] en
   [http://www.dcc.uchile.cl/1877/article-17501.html] el [2009-10-20
   Tue])

  1) Complejidad  y fundamentos matemáticos (7 clases)
     1. [ ] Proceso de diseño y análisis de un algoritmo.
     2. [ ] Cómo medir la eficiencia de un algoritmo. 
        - [ ] Modelo de un computador y sus medidas de complejidad.
        - [ ] Tiempo y espacio. 
        - [ ] Peor caso, caso promedio.
     3. [ ] Complejidad de problemas y de algoritmos. 
        - [ ] Medidas de orden. 
        - [ ] Notación O y 
        - [ ] manipulación algebraica.
     4. [ ] Solución de recurrencias. 
        - [ ] Recetas básicas. 
        - [ ] Cambios de variable. 
        - [ ] Teorema maestro. 
        - [ ] Funciones generatrices.
     5. [ ] Técnicas para demostrar cotas inferiores: 
        - [ ] adversario,
        - [ ] teoría de la información, 
        - [ ] reducción.
     6. [ ] Principales casos de estudio (o similares): 
        - [ ] caso promedio del Quicksort, 
        - [ ] cota inferior para mínimo y máximo de un arreglo, 
        - [ ] cota inferior para búsqueda en un arreglo con distintas
              probabilidades de acceso. (Via coding)

  2) Técnicas básicas de diseño de algoritmos (6 clases)
     1. [ ] Inducción simple y reforzada.
     2. [ ] Dividir para reinar.  
        - [ ] Balanceo de la partición.  
        - [ ] Estructuras de datos recursivas.
     3. [ ] Programación dinámica. 
     4. [ ] Algoritmos avaros (greedy) y su optimalidad. 
     5. [ ] Principales casos de estudio (o similares): 
        - [ ] Subsecuencia de suma máxima 
        - [ ] subsecuencia común más larga
        - [ ] selección
        - [ ] árboles de búsqueda óptimos
        - [ ] multiplicación de matrices
        - [ ] árbol cobertor mínimo
        - [ ] caminos mínimos en grafos
        - [ ] scheduling de tareas    *********************

  3) Técnicas avanzadas de diseño de algoritmos (8 clases)
     1. [ ] Análisis amortizado de algoritmos y estructuras de datos:
        - [ ] análisis completo, 
        - [ ] contabilidad de costos, 
        - [ ] función potencial. 
     2. [ ] Uso de dominios discretos y finitos en el diseño de algoritmos. 
     3. [ ] Algoritmos en línea.  
        - [ ] Competitividad. 
     4. [ ] Principales casos de estudio (o similares): 
        - [ ] estructuras para union-find, 
        - [ ] colas binomiales, 
        - [ ] radix sort, 
        - [ ] paginamiento, 
        - [ ] árboles de van Emde Boas,
        - [ ] búsqueda por interpolación   ********************, 
        - [ ] búsqueda no acotada (unbounded search, doubling search).
        - [ ] splay trees,
        - [ ] árboles digitales, 
        - [ ] técnica de los cuatro rusos,

  4) Algoritmos no convencionales (9 clases)
     1. [ ]Algoritmos aleatorizados y probabilísticos.  
        - [ ] Ejemplos en que no hay otra alternativa.  
        - [ ] Relación con la NP- completitud.
     2. [ ] Algoritmos tipo Monte Carlo y Las Vegas. 
     3. [ ] Aleatorización de la entrada.  
        - [ ] Independencia de la distribución de la entrada.
        - [ ] Estructuras de datos aleatorizadas.
     4. [ ] Solución de problemas NP-completos: 
        - [ ] búsqueda exhaustiva.  
        - [ ] Concepto de algoritmos aproximados.
     5. [ ] Nociones de aproximabilidad.  
        - [ ] Problemas que son o no aproximables.
     6. [ ] Algoritmos paralelos y distribuidos.  
        - [ ] Medidas de complejidad.  
        - [ ] Técnicas de diseño.
     7. [ ] Principales casos de estudio (o similares): 
        - [ ] primalidad,
        - [ ] Karp-Rabin para búsqueda en strings, 
        - [ ] número mayoritario, 
        - [ ] árboles binarios de búsqueda aleatorizados, 
        - [ ] skip lists, 
        - [ ] quicksort, 
        - [ ] Hashing perfecto, 
        - [ ] aproximaciones para recubrimiento de vértices, 
        - [ ] vendedor viajero, 
        - [ ] mochila.  
        - [ ] Ordenamiento paralelo, 
        - [ ] parallel-prefix.

1.4 Otros Recursos 
===================

1.4.1 Programa en la  Pagina de Ricardo, de 2000 
-------------------------------------------------
    (Copiado de [http://www.dcc.uchile.cl/~rbaeza/cursos/cc40a.html] el
    [2009-10-20 Tue])

   1) Introducción (3 horas)

      Algoritmos y su complejidad. ¿Como se mide la eficiencia de un
      algoritmo? Tiempo y Espacio. Peor caso, caso promedio. Medidas
      de orden. Modelo de un computador y sus medidas de complejidad.

   2) Fundamentos Matemáticos (6 horas)

Solución de Recurrencias (Números de Fibonacci). Funciones Generatrices. Cálculos asintóticos. Técnicas para cotas inferiores (teoría de la información, adversarios, complejidad de Kolmogorov).

Técnicas Básicas (9 horas)

Inducción: ejemplos simples. Otros paradigmas como casos particulares de inducción. Inducción doble y reforzada.

Dividir para reinar: quicksort, mergesort, algoritmos de selección, algoritmo de Strassen, árboles de búsqueda y otras estructuras de datos recursivas.

Programación dinámica. Arboles binarios de búsqueda. Algoritmos en grafos. Subsecuencia común más larga.

Algoritmos Competitivos (3 horas)

Algoritmos en línea vs. los que saben toda la información. Paginamiento. Búsqueda en el plano.

Aleatorización (6 horas)

Aleatorizar la entrada: hashing. Algoritmos aleatorios: listas saltadas, hashing perfecto. Algoritmos probabilísticos.

Uso de finititud (6 horas)

Búsqueda por interpolación, Bucketsort, árboles digitales, búsqueda de strings.

Heurísticas (12 horas)

Heurísticas en estructuras de datos: auto-organizadas, splay trees. Análisis amortizado. Operaciones sobre conjuntos: Union-Find. Algoritmos golosos: algoritmo de Dijkstra, árboles de cobertura mínimos.

Algoritmos Aproximados (3 horas)

Resolución de problemas NP-completos: búsqueda exhaustiva, algoritmos con soluciones aproximadas acotadas.

Tópicos Avanzados (9 horas)

Algoritmos paralelos y distribuidos. Algoritmos genéticos. Algoritmos Geométricos (Convex Hull). Búsqueda multidimensional: k-d trees, grid files. Manejo de Memoria Dinámica. 

1.4.2 Pagina de Gonzalo sobre el curso 
---------------------------------------
    ( Copiado de [http://www.dcc.uchile.cl/~gnavarro/cc40a/] el
    [2009-10-20 Tue])


   1. Introducción (3 horas)

      Algoritmos y su complejidad. Cómo se mide la eficiencia de un
      algoritmo? Tiempo y Espacio. Peor caso, caso promedio. Modelo de
      un computador y sus medidas de complejidad.

   2. Fundamentos Matemáticos (6 horas)

      Notación. Medidas de Orden. Solución de Recurrencias. Funciones
      Generatrices. Cálculos asintóticos. Manipulación de big-O.

   3. Paradigmas (9 horas)

      Técnicas para el diseño de algoritmos: Búsqueda exhaustiva,
      heurísticas, algoritmos probabilísticos, algoritmos aproximados,
      avaricia (greedy), dividir para reinar, programación dinámica,
      inducción. En cada caso, revisión de problemas característicos y
      de un mismo problema resuelto con cada técnica.

   4. Manipulación de Conjuntos (9 horas)

      Breve repaso de técnicas básicas de ordenación (quicksort,
      mergesort, etc.) y de estructuras de datos para manipulación de
      conjuntos (árboles binarios, AVL, árboles 2-3,
      hashing). Algoritmos de selección: Max-Min, k-ésimo. Colas de
      Prioridad: heaps. Heapsort. Memoria secundaria: árboles B,
      hashing extendible. Union-find.

   5. Algoritmos Paralelos (4.5 horas)

      Modelos de máquinas paralelas: memoria compartida, pasaje de
      mensajes. El modelo PRAM: EREW, CREW, CRCW. Ejemplos de
      algoritmos para cada modelo. El modelo BSP para pasaje de
      mensajes. Ejemplos.

   6. Algoritmos en Grafos (4.5 horas)

      Definiciones. Propiedades básicas. Arboles cobertores de costo
      mínimo. Búsqueda en grafos. Caminos mínimos. Clausura
      transitiva.

   7. Búsqueda en Texto (4.5 horas)

      Búsqueda de strings: autómata finito, algoritmos de
      Knuth-Morris-Pratt, Boyer-Moore, Rabin-Karp, Shift-Or,
      BDM. Arboles digitales: tries, Patricia. Arboles de
      sufijos. Búsqueda aproximada.

   8. Problemas NP-completos (4.5 horas)

      Máquinas de Turing no determinísticas. Las clases P y NP. El
      problema de la satisfactibilidad (SAT). Otros problemas
      NP-Completos.


   9. Bibliografía 

     "*" indica los textos más recomendados, "+" los recomendados como
          referencia pero no para aprender, "-" otros.

     1. [+] A.V. Aho, J.E. Hopcroft y J.D. Ullman, "The Design and Analysis of Computer Algorithms", Addison-Wesley, 1974.
     2. [+] A.V. Aho, J.E. Hopcroft y J.D. Ullman, "Data Structures and Algorithms", Addison-Wesley, 1982.
     3. [-] S. Baase, "Computer Algorithms: Introduction to Design and Analysis", Addison-Wesley 1988.
     4. [-] Brassard, G. and Bratley, P., "Algorithmics: Theory and Practice", Prentice Hall, 1988. 1988.
     5. [*] Cormen, Leiserson, Rivest, "Introduction to Algorithms", MIT Press, 1991.
     6. [+] Gonnet, G. y Baeza-Yates, R., "Handbook of Algorithms and Data Structures", Addison-Wesley, 2ed, 1991.
     7. [-] Harel, D. Algorithmics, the spirit of computing, Addison Wesley 1987.
     8. [+] D.E. Knuth, "The Art of Computer Programing", vol. 1, "Fundamental Algorithms", Addison-Wesley, segunda edición, 1973.
     9. [+] D.E. Knuth, "The Art of Computer Programming", Vol. 3, "Sorting and Searching", Addison-Wesley, 1973.
     10. [*] Manber, U., "Introduction to Algorithms: A Creative approach", Addison Wesley, 1989.
     11. [*] Rawlins, G., "Compared to What? An Introduction to Analysis of Algorithms", Computer Science Press, 1991.
     12. [+] R. Sedgewick y P. Flajolet, "An Introduction to the Analysis of Algorithms", Addison-Wesley, 1996.
     13. [*] R. Sedgewick, "Algorithms", Addison Wesley, 1987. 
     

2 Complejidad y Fundamentos Matemáticos (7 clases) 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

2.1 Proceso de diseño y análisis de un algoritmo 
=================================================
  1) Búsqueda en peor caso y codificacion de enteros 
     1) Cuanto bits se necesitan para codificar $n$ enteros $x_1,\ldots,x_n$?
        - $\lg n+\lg x_1+\ldots\lg x_n$ no es suficiente: como
          demarcar los códigos? Es importante que los códigos no son
          prefijos unos de los otros.
     2) Cuanto comparaciones se necesitan para buscar un entero a
        dentro de tamaño no conocido?
        - $\lg n$ no es la respuesta: que es $n$?
     3) Búsqueda Secuencial y codificacion unaria
     4) Búsqueda binaria "desequilibrada" y gamma codificacion

  2) Cuanto comparaciones se necesitan para ordenar?
     - consideran el arboló de decisión para ordenar n elementos
       (dibujarlo para n=3)
     - $n!$ horas
     - altura $\lg(n!)\approx n\lg n - \Theta(n)$
     - sigue, $\Omega(n \lg n) es una cota inferior en el *peor caso*.
     - Actualmente, $\Omega(n \lg n) es una cota inferior en el
       *promedio* también (con probabilidad uniforme).

  3) En general
     - Si tengo $m$ objetos con mismas probabilidades, necesito $\lceil \lg
       m\rceil$ bits para representarlos.
     - no puedo representar permutaciones equiprobables usando
       menos de $\lceil \lg n! \rceil$ bits en promedio.
     - Cada algoritmo de ordenamiento utilizando comparaciones es
       definiendo una codificación de las permutaciones
     - Sigue que una cota inferior de los códigos es llegando una
       cota inferior sobre la complejidad en promedio.

  4) Aplicación
     - Búsqueda de un elemento $x$ en un arreglo $A[1...n]$
     - $n$ o $2n+1$ horas, dependiente de si sabemos que $x$ es en
       $A$ o no. 
     - altura $\lg(n)$ o $\lg(2n+1)$.

 * TAREA 
      - Hufman permite de comprimir una secuencia de enteros.
      - Se puede usar o no para buscar de maniera adoptiva las
        frecuencias?

2.2 Complejidad en Promedio 
============================

       1) Con probabilidades
           - Supongamos que los elementos se buscan con probabilidad $p_i$
           - Supongamos que tenemos una secuencia de consulta
             $6,6,4,1,4,3,6,6,4,....$
           - anotando los caminos en el arbol va a dar un código para la
             secuencia.
           - Una cota inferior del tamaño del código es una cota inferior
             de la complejidad en promedio.
           - *Teorema*: Una fuente donde los símbolos $X_i$ tienen
             probabilidades $p_i$ no se puede comprimir a menos de 
             $H(p_1,\ldots,p_n)=\Sum p_i\lg 1/p_i$ bits por símbolo.
           - Un algoritmo basado en comparaciones donde el elemento $i$
             se buscó con probabilidad $p_i$, no puede realizar menos de
             $\sum p_i \lg 1/p_i$ comparaciones en promedio.

       2) Biografía de Shannon
          ([http://en.wikipedia.org/wiki/Claude_Shannon])

          - Claude Elwood Shannon (April 30, 1916 – February 24, 2001), an American electronic engineer and mathematician, is known as "the father of information theory".[1]

            Shannon is famous for having founded information theory with
            one landmark paper published in 1948. But he is also
            credited with founding both digital computer and digital
            circuit design theory in 1937, when, as a 21-year-old
            master's student at MIT, he wrote a thesis demonstrating
            that electrical application of Boolean algebra could
            construct and resolve any logical, numerical
            relationship. It has been claimed that this was the most
            important master's thesis of all time.[2]

          - Shannon was interested in juggling, unicycling, and chess. 

       3) Codigo de Shannon-Fano

          - Algoritmo:
            - the symbols are arranged in order from most probable to
              least probable, and then 
            - divided into two sets whose total probabilities are as
              close as possible to being equal. 
            - All symbols then have the first digits of their codes
              assigned; symbols in the first set receive "0" and symbols
              in the second set receive "1". 
            - As long as any sets with more than one member remain, the
              same process is repeated on those sets, to determine
              successive digits of their codes. 
            - When a set has been reduced to one symbol, of course, this
              means the symbol's code is complete and will not form the
              prefix of any other symbol's code.

       4) Algoritmo de Huffman

          - Cuando sabemos las probabilidades, se puede optimisar el
            algoritmo de busqueda.

          - Historia (dixit wikipedia [http://es.wikipedia.org/wiki/Codificación_Huffman] )

            En 1951, a David Huffman y sus compañeros de clase de la
            asignatura “Teoría de la Información” se les permitió
            optar entre la realización de un examen final o la
            presentación de un trabajo. El profesor Robert. M Fano
            asignó las condiciones del trabajo bajo la premisa de
            encontrar el código binario más eficiente. Huffman, ante la
            imposibilidad de demostrar qué código era más eficiente, se
            rindió y empezó a estudiar para el examen final. Mientras
            estaba en este proceso vino a su mente la idea de usar
            árboles binarios de frecuencia ordenada y rápidamente probó
            que éste era el método más eficiente.

            Con este estudio, Huffman superó a su profesor, quien había
            trabajado con el inventor de la teoría de la información
            Claude Shannon con el fin de desarrollar un código
            similar. Huffman solucionó la mayor parte de los errores en
            el algoritmo de codificación Shannon-Fano. La solución se
            basaba en el proceso de construir el árbol desde el fondo
            hasta la raíz en vez de al contrario.

          - *Algoritmo*
            1. Poner los símbolos, valuados con sus probabilidades, en
               una fila de prioridad, y crear $n$ horas.
            2. Reemplazar los dos símbolos $a$ y $b$ de probabilidades
               menores $pª$ y $p_b$ para un nuevo símbolo $c$, con valor
               la suma de sus probabilidades $p_c=pª+p_b$, en la fila, y
               crear un nodo $c$ con hijos $a$ y $b$.
            3. si hay mas que uno símbolo, iterar en paso 2.

          - Complejidad $O(n\lg n)$: $2n$ accesos a la fila de prioridad.

          - Implementación alternativa: ordenar en $O(n\lg n)$, y
            construir el arbol en tiempo $O(n)$. Misma complejidad en el
            peor caso, pero mas fácil de optimizar para instancias
            particulares (además, a veces las probabilidades ya son
            ordenadas).

          - Complejidad $\Omega(n\lg n)$ *en el peor caso* y *en el
            modelo de comparaciones* par reducción al problema de
            ordenamiento: consideran una permutación $\pi$ de
            $[n]=\{1,\ldots,n\}$, y el arreglo definido par
            $A[i]=2^{\pi_i}, \forall i\in[n]$: la definición del código
            da el orden de las valores en $\pi$.

          - *Propiedad*: Sea $l_1,\ldots,l_n$ los tamaños de los códigos
            calculados par el algoritmo de Huffman, entonces $\sum l_i
            p_i < H(p_1,\ldots,p_n)+1$.

          - Nota que $H(p_1,\ldots,p_n)=\Sum p_i\lg 1/p_i \leq \lg n$,
            que $H(p_1,\ldots,p_n)=\lg n$ si $p_i=1/n \forall i\in[n]$

          - El mejor caso es cuando $\forall i, p_i=1/2^{k_i}$ 

          - Un de los peores casos es cuando $p_i>1/2$ 

          - Ejemplo: (dixit wikipedia [http://es.wikipedia.org/wiki/Codificación_Huffman] )

            Una sonda espacial ha sido lanzada al espacio para contar
            cierto tipo de perturbaciones estelares. Ha de contar
            cuántas se producen en cada minuto, y tiene cada día una
            ventana de tiempo bastante reducida para enviar los datos a
            Tierra; por tanto, interesa reducir al máximo el tiempo de
            transmisión, y para ello se recurre a codificar las muestras
            mediante un código de Huffman.

            En la siguiente tabla se muestran los valores a transmitir,
            junto con sus frecuencias relativas, su código en una
            codificación binaria de 3 bits, y su código en un posible
            código Huffman para estos valores.
                Valor   Frecuencia   Código binario   Código Huffman  
                    0          10%              000              010  
                    1          20%              001               10  
                    2          30%              010               00  
                    3          25%              011               11  
                    4          10%              100             0110  
              5 o más           5%              101             0111  

            Puede observarse que, en la codificación binaria, todos los
            posibles valores reciben códigos del mismo número de bits,
            mientras que en la codificación Huffman, cada valor tiene un
            número diferente de bits: los códigos más frecuentes poseen
            dos bits, mientras que los menos frecuentes poseen cuatro
            bits.

            A continuación se observa el código necesario para
            transmitir la siguiente serie de valores:

            5,4,2,3,2,2,1,0,1,3,2,4,3,4,3,2,3,4,2,4

            Utilizando la codificación binaria, sería una serie de 60
            bits; es decir, 3 bits por símbolo.

            101100010011010010001000001011010100011100011010011100010100

            nota: se ha añadido la misma serie separada en bloques con
            la única razón de facilitar una transcripción manual libre
            de errores para un estudio por parte del lector interesado.

            101.100.010.011.010.010.001.000.001.011.010.100.011.100.011.010.011.100.010.100


            Utilizando, en cambio, la codificación Huffman, se tendría
            que enviar una secuencia de 53 bits; es decir, 2,65 bits por
            símbolo.

            01110110001100001001010110001101101101100110110000110

            nota: la misma serie dividida en bloques de 4 bits para la misma observación anterior.

            0111.0110.0011.0000.1001.0101.1000.1101.1011.0110.0110.1100.0011.0


            En este ejemplo, la media de bits por símbolo que cabría
            esperar de esta codificación, en cadenas de valores más
            largas, es de 2,4.

            Para su comparación, la entropía del conjunto de símbolos es
            de 2,366; es decir, el mejor método de compresión sería
            capaz de codificar estos valores utilizando 2,366 bits por
            símbolo.

            Es posible, también, apreciar cómo se pueden extraer sin
            ninguna ambigüedad los valores originales a partir de la
            cadena codificada mediante Huffman.

            Hay que añadir que la codificación de Huffman no puede ser
            aplicada a imágenes en blanco y negro porque es incapaz de
            producir compresión sobre un alfabeto binario.


       5) Algoritmo de Hu Tucker
          - Objetivo: construir un código con buenas propiedades:
            - ningún código sea prefijo de un otro
            - $\sum l_i p_i$ sea mínimo
            - las horas, leí de la izquierda a la derecha, son
              $x_1,x_2,\ldots,x_n$.
          - *Propiedad*: Sea $l_1,\ldots,l_n$ los tamaños de los códigos
            calculados par el algoritmo de Hu Tucker, entonces $\sum l_i
            p_i < H(p_1,\ldots,p_n)+2$.

       6) Búsqueda secuencial
          - Sigue vallando la costa $\Omega(H(p_1,\ldots,p_n))$
          - Ordenando los símbolos en un orden no creciente para
            $p_1\geq p_2\geq \ldots \geq p_n$, el algoritmo cuesta $\sum
            ip_i$
          - *Ejemplo*: 
            - $p_i=z^{i-1}(1-z)$ si $z\in]0,1[$
            - nota que $\sum p_i = \sum z^{i-1}(1-z) 
              = (1-z) \times (1+z+z²+z³+\ldots+z^{n-1})
              = (1-z) \times (1-z^n)/(1-z)
              = 1-z^n$, con limita en $1$ cuando $n$ va en el infinito.
            - $H(p_1,\ldots,p_n) 
              = \sum z^{i-1}(1-z)\lg(1/(z^{i-1}(1-z)))
              = \lg (1/(1-z)) + (1-z) \lg 1/z \sum i z^i$
            - pero $\sum i z_i = z \sum i z^{i-1} = z \sum (z^i)' 
              = z(\sim z^i)'= z(1/(1-z)-1)'= z/(1-z)²$
            - entonces $H(p_1,\ldots,p_n)$ = \log (1/(1-z)) +
              \frac{z}{1-z}\lg \frac{1}{z}$
            - Del otro lado 
              - (...)
              - $\sum i p_i =  1/(1-z)
            - Dibujando el costo de HuTucker, Hufman y Búsqueda Lineal
              para este distribución, se puede ver que Hufman es el
              mejor para las valores extrema de $z$, pero que la
              búsqueda lineal es mejor para valores de $z$ cerca de $1½$.

           - Tarea: resolve Sum i c^i
             - $\sum_{i=1}^n \frac{1}{2^i}=1-1/2^n$ 
             - $\sum_{i=1}^n \frac{i}{2^i}=2-1/2^n$ 

2.3 Análisis de Algoritmos 
===========================

       1) consideran el algoritmo siguiente:
          - Algoritmo
            - min <- A[3]
            - for i<- 2 to n do
              - if A[3]<min
              - then min <- A[3]
            - return min
          - Como calcular su rendimiento?
            - tiempo real
            - cantidad de instrucciones
            - resumen de la cantidad de instrucciones.
          - Historia: O(), o() y otros símbolos "místicos"
            - Históricamente, la primera aparición del símbolo O(x) fue
              en el segundo volumen de la tesis de Bachmann sobre la
              teoría de los números (Bachmann 1894), y Landau aprendió el
              usaje de este notación desde el libre de Bachman (Landau 1909, p. 883;
              Derbyshire 2004, p. 238). 
            - Lando es a la origen del símbolo o(x), en vez de la
              notación {x} (Narkiewicz 2000, p. XI).
       2) Medidas de complejidad
          - ignorando 
            - pipelining
            - branch prediction
            - cache
          - considerando
            - tasa de crecimiento
            - contar *operaciones representativas*
            - representarle como una *función de la entrada*
       3) Tipos de análisis
          - de peor caso (y de mejor caso)
            - T(n) = max_{|A|=n} T(A)
            - Análisis robusta y fácil
            - para aplicaciones de alta seguridad.
          - de caso promedio
            - T(n) = \sum_{|A|=n} Pr(A) T(A)
            - depende de Pr(A)
            - Más difícil que el peor caso, pero a veces mas cercano de
              las aplicaciones practica (e.g. servidor red).
       4) Tasa de Crecimiento
          - Vamos a exprimir la complejidad de los algoritmos par su
            tasa de crecimiento
          - *Ejemplo*
            - 3n - \sqrt{n} + 2(\lg n)² -7     \in O(n)
            - 5n + 3 \lg \lgg n -2             \in O(n)
            - 0.1 n² -30 n + ...               \in O(n²)
          - *DEFINICIÓN*
            - $f(n) \in O(g(n))$ si $\exists c,n_0>0 tq \forall n>n_0, f(n)\leq c g(n)$
          - Más *Ejemplos*
            - $n \in O(n²)$
            - $n² \not\in O(n)$
            - $(\lg n)^{\lg n} \not \in O(n²)$, 
              - porque $(\lg n)^{\lg n} = 2^{\lg n \lg\lg n} \not \in O(2^{2\lg n}) = O(n²)$
            - (...)
       5) Más *definiciones*
          - $f(n) \in O(g(n))$ si 
            - $\exists c,n_0>0 tq \forall n>n_0, f(n)\leq c g(n)$
          - $f(n) \in \Omega(g(n))$ si 
            - $\exists c,n_0>0 tq \forall n>n_0, f(n)\geq c g(n)$
            - $g(n)\in O(f(n))$
          - $f(n) \in \Theta(g(n))$ si 
            - $\exists c_1,c_2,n_0>0 tq \forall n>n_0, c_1 g(n) \leq f(n) \leq c_2 g(n)$
            - $f(n) \in O(g(n))$ y $f(n) \in \Omega(g(n))$
          - $f(n) \in o(g(n))$ si 
            - $\forall c \exists n>0 tq \forall n>n_0, f(n)< c g(n)$
            - $\lim_{n\rightarrow\infty} \frac{f(n)}{g(n)}=0$
            - $f(n) \not\in\Omega(g(n))$
          - $f(n) \in \omega(g(n))$ si 
            - $\forall c \exists n>0 tq \forall n>n_0, f(n)> c g(n)$
            - $\lim_{n\rightarrow\infty} \frac{g(n)}{f(n)}=0$
            - $f(n) \not\in O(g(n))$
            - $g(n) \in o(f(n))$
       6) *Ejemplos*
          - $3n - \sqrt{n} + 2 (\lg n)² - 7 $
            - $\in 3n+o(n)$
            - $\in 3n + O(\sqrt{n})$
          - $3n+O(\sqrt{n}) = 3n(1+O(\frac{1}{\sqrt{n}}))
       7) Complejidad de Problemas vs Algoritmos
          - Estas notaciones son validas para la complejidad de los
            algoritmos, pero también para los problemas.
          - La complejidad de un problema es la mejor complejidad de un
            algoritmo para este problema.
          - De esta manera podemos definir cotas inferiores y
            superiores para los problemas (en el peor caso sobre las
            instancias de tamaño fijado, en el caso promedio,
            etcétera...).
          - Ejemplos:
            - Ordenar
              - "Ordenar $n$ elementos con comparaciones $\in\Omega(n\lg n)$"
              - "Ordenar $n$ elementos $\in O(n\lg n)$"
              - entonces "Ordenar $n$ elementos con comparaciones $\in\Theta(n\lg n)$"
            - Max
              - encontrar el máximo en un arreglo arbitrario de $n$
                elementos con comparaciones $\in\Theta(n)$

2.4 Recurrencias 
=================

       1) Recurrencias en la análisis de Algoritmos y Problemas
          - Algunas recurrencias tienen soluciones complexas y
            difíciles a describir de manera non recursiva. Aquí
            describimos algunas recurrencias más "fáciles".

          - Ejemplo 1: Torre de Hanoi(n)   (con juguetes de niños)

            - Pequeña historia: hay una ciudad que se llama Hanoi en
              Vietnam, y también una torre aquí. Pero el problema no es
              originario de Vietnam, pero Francesa: es un rompecabezas
              o juego matemático inventado en 1883 por el matemático
              francés Éduard Lucas.

            - DEFINICIÓN en Wikipedia:

              Se cuenta que un templo de Benarés (Uttar Pradesh,
              India), se encontraba una cúpula que señalaba el centro
              del mundo. Allí estaba una bandeja sobre la cual existían
              tres agujas de diamante. En una mañana lluviosa, un rey
              mandó a poner 64 discos de oro, siendo ordenados por
              tamaño: el mayor en la base de la bandeja y el menor
              arriba de todos los discos.Tras la colocación, los
              sacerdotes del templo intentaron mover los discos entre
              las agujas, según las leyes que se les habían entregado:
              "El sacerdote de turno no debe mover más de un disco a la
              vez, y no puede situar un disco de mayor diámetro encima
              de otro de menor diámetro". Hoy no existe tal templo,
              pero el juego aún perduró en el tiempo...

              Otra leyenda cuenta que Dios al crear el mundo, colocó
              tres varillas de diamante con 64 discos en la
              primera. También creó un monasterio con monjes, los
              cuales tienen la tarea de resolver esta Torre de Hanói
              divina. El día que estos monjes consigan terminar el
              juego, el mundo acabará. No obstante, esta leyenda
              resultó ser un invento publicitario del creador del
              juego, el matemático Éduard Lucas. En aquella época, era
              muy común encontrar matemáticos ganándose la vida de
              forma itinerante con juegos de su invención, de la misma
              forma que los juglares hacían con su música. No obstante,
              la falacia resultó ser tan efectista y tan bonita, que ha
              perdurado hasta nuestros días. Además, invita a
              realizarse la pregunta: "si la leyenda fuera cierta,
              ¿cuándo será el fin del mundo?".

              El mínimo número de movimientos que se necesita para
              resolver este problema es de 2^64-1. Si los monjes
              hicieran un movimiento por segundo, los 64 discos
              estarían en la tercera varilla en algo menos de 585 mil
              millones de años. Como comparación para ver la magnitud
              de esta cifra, la Tierra tiene como 5 mil millones de
              años, y el Universo entre 15 y 20 mil millones de años de
              antigüedad, sólo una pequeña fracción de esa cifra.

          - Ejemplo 2: Disk Piles(h,n_1,...,n_h) 

            Supongo que los disco no son de tamaño todos distintos,
            pero que hay $h$ tamaños distintos, y $n_1$ discos del
            tamaño mas pequeño, hasta $n_h$ discos del tamaño mas
            grande. Cuando movimiento se necesitan para mover la torre?
            Cual es el peor caso para $n,h$ fijados?

            - f(0) = 0
            - f(h,n_1,...,n_h)  = 2*f(h-1,n_1,...,n_{h-1}) + n_h

            - f(h,n_1,...,n_h)  = 2*f(h-2,n_1,...,n_{h-2}) + 2*n_{h-1} + n_h
            - f(h,n_1,...,n_h)  = 2*f(h-3,n_1,...,n_{h-3}) 4* n_{h-2} + 2*n_{h-1} + n_h
            - f(h,n_1,...,n_h)  = (...)
            - f(h,n_1,...,n_h)  = \sum_{i=0}^{h-1} 2^i n^{h-i} 

            - El peor caso para $n,h$ fijados es cuando n_1=n-h+1 En
              este caso la complejidad es de \Theta((n-h)2^h)
              asimptoticamente en n y h.

       2) Soluciones de Recurrencias Telescópica
          - Forma general
            - $X_0 = a_0$
            - $X_{n+1} = X_n + a_n$
          - Solución:
            - $X_{n+1} = X_n + a_n = ... = a_0+a_1+\ldots+a_n = \sum_{i=0}^n a_i$
          - Ejemplos
            - $X_0=0$, $X_{n+1} = n + X_1$, 
              - $X_{n+1}= n(n+1)/2$ pero $X_n = n(n-1)/2$
            - $X_0=0$, $X_{n+1}=1/(n+1)+X_n$, 
              - $X_n = \sum 1/i$
              - $\ln (n+1)= \integral_1^{n+1} 1/x dx 
                \leq X_n \leq 
                1+\integral_1^n 1/x dx \leq 1+\ln(n)$
              - $X_n\in\Theta(\log n)$
              - $X_n\in ln(n) + O(1)
            - Torre de Hanoï
              - $h_1 = 1$, $h_n = 1+2h_{n-1}$
                - $h_{n} = 2^n -1  $

       3) Recurrencias Lineales Homogéneas

          - Ejemplo: Fibonacci
            - f(0)=0
            - f(1)=1
            - f(n+2) = f(n) + f(n+1)
             
          - Se llaman "Lineales" porque su definición es de la forma
            - a_d X_{n+d} + ... + a_1 X_{n+1} + a_0 X_n = 0

          - Las constantes describen un espacio vectorial de dimensión
            d, y las condiciones iniciales describen cual es la
            solución particular del sistema.

          - Una manera de  solucionar el sistema:
            - encontrar las $d$ soluciones del sistema de la forma
              $X_n=\lambda^n$, donde \lambda es la solución del
              *polinomio característico*

              $a_k \lambda^k + \ldots + a_1 \lambda + a_0 = 0 $

            - Este polinomio tiene $d$ raíces
              $\lambda_1,\ldots,\lambda_d$, cada uno correspondiendo a
              una solución al sistema (sin condiciones iniciales)

            - Las otras soluciones del sistema son combinaciones
              lineales de estas soluciones, de la forma

              $c_0
              +c_1 \lambda_1^n
              +c_2 \lambda_2^n
              +\ldots
              +c_d \lambda_d^n
              $

              donde las constantes $c_0,\ldots,c_d$ dependen de las
              condiciones iniciales de la recurrencia.

          - Una otra manera de solucionar el sistema:
            - Escribir la matrica M tal que 
              (X_{n+k},\ldots,X_{n}).M = (X_{n+k-1},\ldots,X_{n-1}) 
            - Diagonalizar M tal que M = P.D. P^{-1} 
              (eso corresponde a solucionar el polinomio característico)
            - Aplicar P.D^{n}.P^{-1} (X_k,\ldots,X_0) para tener la solución.

          - Volviendo al ejemplo de Fibonacci
            - $f_{n+2}-f_{n+1}-f_n = 0$
            - Consideran las soluciones de la forma f_n = \lambda^n:
              $\lambda^{n+2} -\lambda^{n+1} - \lambda^n = 0$
              $\lambda^{n}(\lambda^2 -\lambda - 1) = 0$
              $\lambda_1 = \frac{ 1+ \sqrt{5}}{2} \approx 1.618$
              $\lambda_2 = \frac{ 1- \sqrt{5}}{2} \approx -0.618$
              $f_n = C_1 (\frac{ 1+ \sqrt{5}}{2})^n
                   + C_2 (\frac{ 1- \sqrt{5}}{2})^n$
              $f_0 = 0 = C_1+C_2$
              $f_1 = 1 = C_1(\frac{ 1+ \sqrt{5}}{2}) + C_2(\frac{ 1- \sqrt{5}}{2})
              $C_1 = 1/\sqrt{5}$
              $C_2 = -1/\sqrt{5}$
            - $f_n = \frac{1}{\sqrt{5}} 
                     \left( 
                            (\frac{ 1+ \sqrt{5}}{2})^n
                          + (\frac{ 1- \sqrt{5}}{2})^n
                     \right)$

2.5 Recurrencias Part 2: Teorema Maestro 
=========================================

       1) Teorema Maestro (versión simple)

          - La solución a la recurrencia   $T(n) = a T(n/b) + cn$
            tiene solución en los casos siguiente:
            - caso a<b,  $T(n)\in O(n)$
            - caso a=b,  $T(n)\in O(n\lg n)$
            - caso a>b,  $T(n)\in O(n^{\log_b a})

          - Demostración con reducción a $S_k = T(b^k) / a^k$
            - $S_0 = T(1) / a^1$ conocido
            - $S_k = T(b^k)/a^k 
                   = a T(b^{k-1})/a^{k} + cb^k/a^k$
            - $S_k = S_{k-1} + c (\frac{b}{a})^k$
            - $S_k = c \sum_{i=0}^k (\frac{b}{a})^i$

            - si $\frac{b}{a}=1$,
              $S_k = c k 
              y $T(n)\leq S_{\lceil\log_b n\rceil} \times a^{\log_b n} 
              \approx c \lg n \times a^\lg n 
              \in O(n\lg n)$

            - si $\frac{b}{a}<1$,
              $S_k = c \frac{ 1 - (\frac{b}{a})^{k+1} }{1-\frac{b}{a}}
              < c \frac{1}{1-\frac{b}{a}} = c \frac{a}{b-a} \in O(1)$
              y $T(n)\leq S_{\lceil\log_b n\rceil} \times a^{\log_b n} 
              \in O( a^{\log_b n} )

            - si $\frac{b}{a}>1$,
              $S_k = c \frac{ 1 - (\frac{b}{a})^{k+1} }{1-\frac{b}{a}}
              \in O( (\frac{b}{a})^{k} )
              y $T(n)\leq S_{\lceil\log_b n\rceil} \times a^{\log_b n} 
              \approx (\frac{b}{a})^{\log_b n} \times a^{\log_b n} 
              \approx b^{\log_b n} \in O(n)


       2) Teorema Maestro (versión completa)

          - La solución a la recurrencia $T(n) = a T(n/b) + f(n)$ tiene
            soluciones en los casos siguiente (dependiente del ratio
            entre $f(n)$ y $n^{\log_b(a)}$:

            - caso $f(n) \in O( n^{\log_b(a)-\varepsilon} )$,
              $T(n)\in O(n^{\log_b a})

            - caso $f(n) \in \Theta(n^{\log_b(a)} \log^k n)$, para alguno $k\geq 0$,
              $T(n)\in \Theta(n^{\log_b a}\log^{k+1}n)

            - caso $f(n) \in \Omega( n^{\log_b(a)+\varepsilon} )$
              y $a f(n/b)\leq c f(n)$ para alguna constante $c$,
              $T(n)\in\Theta(f(n))$

          - La demostración es para un otro año...

       3) Que hacer con los otros tipos de recurrencia?

          - Ejemplo:
            $T(n) = n
                 + T(\alpha_1 n)
                 + (...)
                 + T(\alpha_k n)$
            donde $\alpha_1\leq\alpha_2\leq\ldots\leq\alpha_k<1$
            (no es lineal, y no se puede aplicar el teorema maestro!)

          - Si "adivinan" una solución, pueden verificarla!

          - Por ejemplo: adivinamos que hay una solución lineal:
            $\exists d\geq 0$ tq $T(n)\leq dn$

            - en cuales condiciones, si $T(n) \leq dn$, entonces $T(\alpha_i n)\leq d\alpha_i n$?

            - si $n+ dn (\alpha1 +\ldots + \alpha_k )\leq dn$, podemos afirmar que $T(n)\leq dn$
              eso es equivalente a $d\geq\frac{1}{1- (\alpha1 +\ldots + \alpha_k ) }$

            - en esta condición, hay una solución lineal (si no, no probamos nada).

3 Técnicas básicas de diseño de algoritmos (6 clases) 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

3.1 Técnicas clásicas de Diseño de Algoritmos 
==============================================
        1) Ejemplo: cuando cotas inferiores sugieran algoritmos.
           - Calcular el mínimo en un arreglo desordenado $A[1,\ldots,n]$
             - Podemos mostrar una cota inferior de $n-1$ con la definición
               de una estrategia de adversario:
             - si el algoritmo es mirando en menos que n lugares, el
               adversario puede poner el elemento mínima en un lugar
               donde no miraba.
           - Mismo para calcular el máxima: $n-1$ comparaciones.
           - La cota inferior (exacta) para calcular el min y max es mas difícil
             - (2n-2) no es óptima.
             - Supone que todas las valores son distintas
               - a = elementos nunca comparados
               - b = elementos *comparados al menos una vez y* quienes
                 ganaron todos su comparaciones hasta ahora (potencial
                 max)
               - c = elementos *comparados al menos una vez y* quienes
                 ganaron todos su comparaciones hasta ahora (potencial
                 min)
               - d = elementos eliminados
             - Al inicio, a=n y b=c=d=0 
             - siempre a+b+c+d=n
             - Al final, a=0, b=c=1, d=n-2
             - cada comparación es 
               - reduciendo a para crecer b+c   (n/2 veces)
               - o reduciendo b+c para crecer d (n-2 veces)
             - en total, cualquier algoritmo realiza $\lceil 3n/2\rceil -2$ comparaciones.
           - La cota superior es "tight":
             - forma $n/2$ pares para dividir en dos partes con $n/2$ comparaciones
             - calcula el min en una parte y el max en la otra
             - complejidad total es $\lceil 3n/2\rceil -2$ comparaciones.
           - El ESPACIO de este algoritmo es O(n), pero se puede
             escribir un algoritmo que es ejecutando los dos tipos de
             comparaciones en paralelo para mantener espacio
             O(1). [Este algoritmo tiene mejor localidad de referencia
             también]
        2) Técnicas clásicas
           1) Lista
              - dividir y reinar
              - *Programación dinámica*
              - inducción
           2) Vamos a considerar los ejemplos siguiente:
              - Fibonacci
              - Subsec suma Maxima
              - Subsec común mas larga
           3) Fibonacci
              - $f_0=1, f_1=1, f_n=f_{n-1}+f_{n-1}$
              - Un programa ingenuo:
                - int fib(n)
                  - if n=0 return 0
                  - if n=1 return 1
                  - return fib(n-1)+fib(n-2)
                - el algoritmo es correcto, pero toma tiempo fib(n), que
                  es exponencial O(1.618^n)
              - Solución mas eficiente en tiempo lineal, espacio lineal
                - int fibdynamico(n)
                  - int f[n]
                  - f [ 0 ]<-0; f[ 1 ]<- 1;
                  - for i=2 to n
                    - f[i]<-f[n-2]+f[n-1]
                  - return f[n] 
              - Solución  en tiempo lineal, espacio constante:
                - int fibinduccion(n)
                  - int A,B,C
                  - A<-0; B<- 1;
                  - for i=2 to n
                    - C<-A+B
                    - A<-B
                    - B<-C
                  - return C
              - Solución mas rápida:
               - $f_n = \frac{1}{\sqrt{5}} 
                        \left( 
                               (\frac{ 1+ \sqrt{5}}{2})^n
                             + (\frac{ 1- \sqrt{5}}{2})^n
                        \right)$
               - Se puede calcular un exponente en tiempo logarítmico, con
                 el algoritmo "Egyptiano":
                 - X^n = (X^{n/2})^2 si n par y (X^{n-1/2})^2 si n impar 
               - El espacio de este algoritmo es O(n) bits, que es
                 lineal en el tamaño del output.
           4) Subsecuencia de suma Maxima:
              - Consideran una secuencia de enteros S[1,\ldots,n]
              - estamos buscando para el intervalo $i,j$ tal que la suma parcial
                $\sum_k=i^j} sea máxima.
              - Hay una solución trivial en tiempo $O(n^2)$
              - Hay una cota inferior de $\Omega(n)$.
              - Para una solución en tiempo O(n), utilizamos una forma de inducción:
                - suf[n] = max (0,suf[n-1]+S[n])
                - subs[n] = max(subs[n-1]+suf[n])
              - Eso se puede calcular en tiempo O(n) y en espacio O(1).
           5) Subsecuencia común mas larga (= LCS = Longest Common Subsequence)
              - ACACGATA
              - vs TAAGAGATTGA
              - Solucion ingenua
                - LCS(n,m,X_1,\ldots,X_n,Y_1,\ldots,Y_m)
                  - si n=0 o m=0
                    - return 0
                  - si X_n=Y_m
                    - return 1+LCS(n-1,m-1,X_1,\ldots,X_{n-1},Y_1,\ldots,Y_{m-1})
                  - sino
                    - return max( 
                      LCS(n,m-1,X_1,\ldots,X_n,Y_1,\ldots,Y_{m-1})
                      LCS(n-1,m,X_1,\ldots,X_{n-1},Y_1,\ldots,Y_m)
                      )
                - Este algoritmo tienen tiempo O(2^{n+m}):
                  - T(n,m) = 1+T(n-1,m)+T(n-1,m)
              - Solucion en tiempo polinomial (pero espacio O(n^2))
                - el razonamiento es que hay solamente (n+1)(m+1)
                  valores distintas, entonces no es razonable de tomar
                  tiempo exponencial.
                - Se puede calcular todas las valores en una matrica.
              - Solucion en espacio lineal (y tiempo O(nm))
                - modifica el algoritmo precedente para calcular
                  mantener solamente una linea a cada momento.

3.2 Dividar para Reinar 
========================
       1) Ejemplos conocidos de aplicaciones de "Dividir para Reinar"
          - Búsqueda Binaria (Binary Search)
          - Merge Sort 
          - Quick Sort (recuerda el algoritmo)
          - Policía de Estados Unidos vs OPEP (Organization of the
            Petroleum Exporting Countries) [http://www.opec.org/home/])

            El teorema maestro es perfecto para analizar la mayoría de
            los algoritmos basados en este técnica (no tan los casos
            políticos).

       2) Es puede aplicar para buscar $x$ en un arreglo desordenado $A$?
          - Cota inferior? 
            - $\Omega(n)$
          - Cotas superiores para buscar el $k$th mínima?
            - $O(n k)$    con $k$ búsquedas del mínima
            - $O(n(1+lg k))$ con un heap.
          - Cotas superiores para buscar la mediana (k=n/2)?
            - $O(n^2)$
            - $O(n lg n)$ 

       3) Algoritmo Quick Select
          + Similar a Quick Sort
          + QuickSelect(A,l,r,k)
            1. si r-l+1=k
               - return el máxima de $A$
            2. elige un pivoto $x$ (e.g. x=A[ 1 ], o x=A[n/2], o
               x=A[k], o x=A[r] con r aleatorio)
            3. produce una partición en A[i]<x, A[p]=x y A[j]>x, donde i<p<j
               (i.e. encontra la posición p de x en el arreglo ordenado)
            4. si k=p
               - return x
            5. sino si k<p
               - return QuickSelect(A,l,p-1,k)
            6. sino (si k>p)
               - return QuickSelect(A,p+1,r,k)
          + Cual es su complejidad en el peor caso?
            - O(n^2)
          + Cual es su complejidad en el caso promedio?
            - O(n) 
            - Prueba:
              * Q(n,k) = complejidad cuando r-l+1=n
              * Q(n,k) = n 
                         + 1/n \sum_{p=1}^{k} Q(n-p,k-p)
                         + 1/n \sum_{p=k+1}^{n} Q(p-1,k)
              * Q(n) = n 
                       + 1/n \sum_{p=1}^{k} Q(n-p)
                       + 1/n \sum_{p=k+1}^{n} Q(p-1)
                     \leq n + 2/n \sum_{p=n/2}^{n-1} Q(p) (en el peor caso)

              * Como resolver la recurrencia siguiente?

                - C(n) =  n + 2/n \sum_{p=n/2}^{n-1} Q(p) 

                - Teorema Maestro (tarea a hacer en la casa)
                  - (...)

                - Adivinando una solución: queremos probar que C(n)\in
                  O(n):
                  - aseguramos de las condiciones iniciales:
                    - C(k) = n-1 < c.n para c\geq 1
                  - supongamos que existe un N tal que C(n)\leq c.n para todos n<N
                  - Cual son las condiciones para cuales C(N) \leq C.N?
                    - C(N) =  N + 2/N \sum_{p=N/2}^{N-1} Q(p) 
                           \leq N + 2/N \sum_{p=N/2}^{N-1} c p  
                           = N + 2/N c \sum_{p=N/2}^{N-1} p  
                           = N + 2c/N ( N(N-1)/2 - N/2(N/2-1)/2 )
                           = N + c    (   N-1    -    (N/2-1)/2 )
                           = N + c    (       3N/4-1/2          )
                    - Para cual c tenemos N + c (3N/4-1/2) \leq cN  ?
                      - N + c (3N/4-1/2) \leq cN
                      - N \leq c (N/4 + 1/2)  
                      - N/(N/4 + 1/2) \leq c 
                      - c \geq 4/(1 + 2/N)
                      - por ejemplo, c=4
              * Entonces, Q(n)\leq C(n) \in O(n)

3.3 Caminos mínimos en grafos y análisis amortizado 
====================================================

       Ejemplos de Algoritmos "greedy" (= avaros, codiciosos, calosos,
       glotones): Caminos Mínimos (Dijkstra), Arbol Cobertor mínimo
       (Kruskal)
          
       1) Caminos mínimos, una fontana: Dijkstra

          * Problema: 
            - Dado un grafo (V,E) con costo sobre las aristas c:E->R
            - una fontana s
            - calcula la distancia mínima de s a cada punto.

          * Algoritmo de Dijkstra

            - Dijkstra(V,E,c,s)
              1. S <- \emptyset
              2. for v\in V 
                 - d[v] <- \infty
                 - parent[v] <- \infty
              3. d[s] <- 0; parent[s] <- s
              4. Q <- heap; Q.insert(s)
              5. while not Q.empty()
                 1. u <- Q.deleteMin()
                 2. for each neighbor v of u
                    - if d[u]+c(e) < d[v]
                      - parent[v] <- u
                      - if v in Q then Q.decreaseKey(v)
                      - else Q.insert(v)
              6. return(d[],parent[]) 

          * *Lema* Dijkstra obtiene las distancias minimales.
            1. todos los nodos conectados con s van a ser visitados.
            2. d[u] es mínima en paso 5.1 (otra mente existaria un ciclo
               negativo)

          * Complejidad 

            - en el peor caso: 

              O( m decrease_key + n (deletion + insertion) )

              - con un (binary) heap:
                - deletion O(\lg n)
                - insertion O(\lg n)
                - decrease_key O(\lg n) (variante de insertion)
                - total O( (m+n)\lg n )

              - con un Fibonacci heap:   [Mehlhorn, Sanders p. 
                - deletion O(\lg n)
                - insertion O(\lg n)
                - decrease_key O(1) 
                - total O( m  + n\lg n )


       2) Caminos mínimos, todas las pares (si hay tiempo, sino en tarea)

          1. Problema:
            - Dado un grafo (V,E) con costo sobre las aristas c:E->R
            - calcula la distancia mínima entre cada paras.

          2. Multiplicación de matricias

            - (M\times M)_{(i,j)} = \min \{
                        M_{(i,j)},
                        \min_k \{ M_{(i,k)}+M_{(k,j)} \}
              \}

            - Complejidad para calcular M^n?

              - M^2 es O(n^3) en general

              - M^n es O(n^3\lg n) en general

          3. n Fontanas 

             - O(n(n+m)\lg m) \subset O(n^2\lg n + nm\lg n)
               - mejor que O(n^3\lg n) si m\in O(n)
               - peor que O(n^3\lg n) si m\in \Theta(n^2)

          4. Existe una solución en O(nm+n^2\lg n)
             [Melhoran and Sanders, p.207]

             - solvando 
               - un general mono fontana problema
               - mas n  mono fontana problema con costo non-negativa.





       3) Arbol de Cubertura Mínima (Kruskal)

          1. Problema: 
             - dado un grafo con costos en las aristas,
             - elijar un subconjunto de las aristas 
             - formando un arbol
             - de costo total mínimo.

          2. Algoritmo de Kruskal:
             - elijar las aristas minimales que no crean ciclos, de
               maniera iterativa (con un heap):
             - Eso es el óptimo (global) o no?

             - *Lema* Kruskal obtiene el arbol cobertor mínimo.

             - *Proof* 
               - En paso intermedio, el algoritmo es manteando una
                 silva de arboles. 
               - *Hipótesis H_i* Luego del paso i, existe una solución
                 óptima que contiene todas las aristas elegidas por el
                 algoritmo.
               - Obvio, H_0 es verdad (cuando no hay aristas elegidas)
               - Si vale luego de conectar el grafo, (cuando el arbol
                 tiene n-1 aristas), la solución es oprimo.
               - *Paso inductivo*: (contradicción)
                 - si Kruskal es agregando una arista (a,b) que no
                   pertenece a una solución óptima, significa que el
                   costo de la solución oprima puede ser reducido
                   intercambiando (a,b) con la otra arista conectando
                   V(a) con V(b) -> contradicción.

          3. Implementación 

             * Ingenua: 

               - Kruskal (V,E, c:E->R)
                   1. T <- empty set
                   2. Ordena  las arestas en E    O(m\lg m)
                   3. while |T|<n-1
                      - toma la ares-ta (a,b) mas pequeña y remuda la de E.   O(1)
                      - si (a,b) no están conectados en T                O(n)!!!
                        - T <- T \cup {(a,b)}
                   4. Return T

               - Complejidad?
                 - O(m\lg m + n.m) \subset O(m\lg n + n.m)
                 - (CUIDADO: hay mas que n-1 iteraciones en el peor caso, hasta m!!!)


             * Primera Tentativa de mejorar?

               - Kruskal (V,E, c:E->R)
                   1. R <- empty set                        O(1)
                   2. marca con su numero cada punto        O(n)
                   3. heapify las arestas en E              O(m)
                   4. while |T|<n-1
                      - remuda la cabeza (a,b) del heap     O(\lg m)
                      - si a b tiene marcas tas      O(1)
                        - T <- T \cup {(a,b)}
                        - si |V(a)|>|V(b)|
                          - marca V(b) con la marca de a
                        - sino 
                          - marca V(a) con la marca de b
                   5. Return T

               - Complejidad?
                 - O(m + m\lg m + n.m) \subset O(m\lg n + n.m): no es mejor

             * Ultima mejoración: Estructuras de datos para Union-Find 

               - Como se puede mejorar la union de marcas?

                 + con cadenas de elementos?

                   - iniciar como n cadenas O(n)
                   - probar si dos elementos son de la misma clase O(n)
                   - unificar las dos clases O(1)

                 + con arboles (inversados, puntadores al padre)
                   - un arreglo de tamaño n con puntadores a cada nodo.
                   - cada nodo tiene un puntadore a un de su representante
                   - iniciar como n arboles O(n)
                   - probar si dos elementos son de la misma clase O(altura)
                   - unificar las dos clases O(1) (guardando la raíz obtenida en la prueba)

                   - *Lema* En esta estructura, cada arbol de raíz $z$ y
                     tamaño $S(z)$ tiene altura $h(z)\leq \lg S(z)$.

                   - *Proof* Con inducción
                     - *Hipótesis*: $H_i$ es que todos los arboles de menos
                       que $i$ elementos tienen altura logarítmica en su
                       tamaño.
                     - Paso inicial: S(z)= 1, h(z)=0
                     - Paso inductivo: 
                       - Consideramos que colgamos x en el arbol de y
                       - Llamamos z al nuevo arbol con raíz y
                       - S(z) = S(x)+S(y)
                       - h(z) = \max{1+h(x),1+h(y)}
                       - si h(z) = h(y) \leq \lg(S(y)) \leq \lg(S(z)) 
                       - si h(z) = 1+h(x) \leq 1+\lg(S(x)) \leq \lg(S(z)) 
                     - Conclusión: la hipótesis es verificada en todos puntos.
                     - QED

                   - Conclusión O(\lg n) para saber si dos elementos
                     pertenecen a la misma clase.

               - Nueva Complejidad?
                 - O(m+m\lg m + n\lg n) \subset O(m\lg n)

               - *Notas*: Cada búsqueda puede simplificar el arbol sin
                 costo adicional, mejorando la complejidad de manera
                 amortizada de la estructura de union hasta O(\lg^* m).
                 Pero eso no cambia la complejidad del algoritmo de
                 Kruskal.

3.4 Ejemplo:  Algoritmo Median 
===============================

       1) Solución en tiempo lineal en el peor caso de Select k-ésimo

          + Necesitamos una mejor elección del pivoto, de maniera que
            siempre elimina c.n elementos en el peor caso, para alguna
            constante c.

            * ElijePivot(A,l,r)
              2. g <- (r-l+1)/5 
              2. Hace g grupos de tamaño 5
              3. Calcula la mediana de cada grupo en un arreglo B[1,..,g]
              4. x <- LinealSelect(B,1,g,g/2)   (recursivamente)
              5. return p.

            * LinealSelect(A,l,r,k)
              1. si l-r+1 = 1
                 - return A[l]
              2. x <- ElijePivot(A,l,r)
              3. partitiona en A[i]<x, A[p]=x y A[j]>x, donde i<p<j
                 (i.e. encontra la posición p de x en el arreglo ordenado)
              4. si k=p
                 - return x
              5. sino si k<p
                 - return LinealSelect(A,l,p-1,k)
              6. sino (si k>p)
                 - return LinealSelect(A,p+1,r,k)

          + Análisis:

            - x = ElijePivot(A,1,n)
              - es tal que al menos 3n/10 elementos son menos que x
              - es tal que al menos 3n/10 elementos son mayores que x
              - se vea con una figura, 
                - dibujando cinco grupos de 5 puntos cada uno.
                - la median de las medianas es mejor (y menos) que 3/5
                  valores en n/2 grupos, resultando en 3n/10.

            - L(n) = complejidad de LinealSelect(A,1,n)
            - P(n) = complejidad de ElijePivot(A,1,n,k)
            - L(n) = e.n + P(n/5) \in O(n) + P(n/5)
            - P(n) \leq L(n) + 1 * P(7n/10) \in O(n) + P(n/5) + P(7n/10)

            - Como resolver C(n) = e.n + C(n/5) + C(7n/10)?

              + Teorema Maestro:
                - Se podría usar, pero podemos mas simple:

              + Adivinando (Queremos mostrar complejidad lineal)
                - Verificamos las condiciones iniciales: 
                 - C(1) = 1
                - Supongamos que existe N y c tal que todo n<N, C(n)<cn
                - C(N) = e.N + C(N/5) + C(7N/10) 
                       < e.N + c.N/5 + c.7N/10
                       = N (e+c/5+7c/10)
                       = N (e+9c/10)       
                - Cuales son  condiciones suficientes para que C(N)<cN?
                  - e+9c/10 < c => C(N)<cN
                  - e < c/10 => C(N)<cN
                  - c > 10 e => C(N)<cN

            - Entonces, la complejidad de LinealSelect es lineal en n,
              (con una constante de 10 e, donde e es el coste de
              calcular la median de 5 elementos)

          + Porque grupos de tamaño 5?

            - con n/t grupos mas pequeños, de tamaño t<5  (e.g. 3)

              - Calculamos n/3 medianas 

              - p \geq n/3 (en ves de 3n/10)

              - S(n) = n + S(n/3) + S(2n/3)

              - La recurrencia tiene como solución S(n)\in O(n\lg n)
                (Teorema Máximo)

              - Nota: que se pasa si probamos la hipótesis S(n)<cn?
                - S(N) = N + S(N/3) + S(2N/3)
                       < N + cN/3 + 2cN/3
                       = (1 + c)N
                - Eso no puede ser mas pequeño que cN...
                  

            - con n/t grupos mas grande, de tamaño t>5 (e.g. 7)

              - Calculamos n/t medianas (supongamos t impar) en tiempo
                e_t, que es mas que e_5

              - p \geq (t+1)n/4 

              - S(n) = n.e_t + S(n/t) + S((t+1)n/4)

                + Adivinando (Queremos mostrar complejidad lineal)
                  - Verificamos las condiciones iniciales: 
                   - S(1) = 1
                  - Supongamos que existe N y c tal que todo n<N, C(n)<cn
                  - S(N) = e_t.N + S(N/t) + C((t+1)N/4) 
                         < e_t.N + c.N/t + c.(t+1)N/4
                         = N (e_t+c/t+(t+1)c/4)
                  - Cuales son  condiciones suficientes para que C(N)<cN?
                    - (e+c/t+(t+1)c/4) < c => C(N)<cN
                    - t 4e  + 4c + (t+1)tc < 4ct => C(N)<cN
                    - t 4e  < c(3t-4-t^2)  => C(N)<cN
                    - c> e * 4* t/(-t^2+3t-4)  => C(N)<cN

            - Entonces, la complejidad de LinealSelect todavía es lineal en n,
              pero con una constante creciendo con e_t.

3.5 Programación Dinámica 
==========================
       
       1) Programación Dinámica

          Principia: para algunos problemas, la solución óptima puede
          ser dividida en sub-soluciones optima de subproblemas, y si un
          subproblema tiene varia soluciones optimas, cada una tiene la
          misma contribución a la solución del problema original.

          La estrategia para resolver estos problemas es de resolver
          todos los subproblemas: eso se llama programación dinámica.

       2) Ejemplos

          1) Distancia de edición mínima (la tarea de hace dos semanas)

             Problema: dado dos secuencias A y B de tamaño n y m,
             cuanto símbolos al mínima tenemos de borrar en cada uno
             para reducir A y B a una subsecuencia común?

             Solucion:
             - Recurrencia:
               - define f(i,j) como el tamaño de la subsecuencia común
                 máxima de A[1,i] y B[1,j]: queremos calcular f(n,m).
               - sin calculación, sabemos que f(0,0)=0.
               - f(i,j) = f(i-1,j-1)+1 si A[i]=A[j],
               -        = max{ f(i-1,j), f(i,j-1) } sino.
             - Implementación:
               - For i=1 to n
                 - For k=i downto 1 
                   - calcula f(i,i-k+1)


          2) Multiplicación de matriz en cadena
             
             Problema: Dado $k$ matriz a_1,\ldots,A_k de tamaños n0*n1,
             n1*n2, n2*n3, \ldots, n_{k-1}*n_k, cual es la mejor
             maniera de multiplicarlas, para minimizar la cantidad de
             operaciones?

             - Cuanto operaciones se necesitan para multiplicar A y B,
               donde A es de tamaño n_0*n_1 y B de tamaño n_1*n_2? De
               cual tamaño es el resultado?

               - Se necesita n_0*n_1*n_2 operaciones, el output es una
                 matriz de tamaño n_0*n_2

             - Cuanto maneras hay de multiplicar A,B, y C?  Cuanto
               operaciones se necesitan para cada una de estas
               maneras, si A es de tamaño n_0*n_1, B de tamaño n_1*n_2,
               y C de tamaño n_2*n_3? Como elegir la mejor (si hay?)?

               - De verdad, hay mucho mas que dos, pero si consideramos
                 solamente la reducción a multiplicaciones de matrices,
                 hay dos: 
                 - (AB)C, que costa  n_0*n_1*n_2 + n_0*n_2*n_3 = n_0*n_2*(n_1+n_3)
                 - A(BC), que costa  n_1*n_2*n_3 + n_0*n_1*n_3 = n_1*n_3*(n_2+n_1)

               - n_0*n_1*n_2 + n_0*n_2*n_3 < n_1*n_2*n_3 + n_0*n_1*n_3 iff
                 0 < n_1*n_2*(n_3-n_0) + n_0*n_3*(n_1-n_2) o iff
                 0 < n_2*n_3*(n_1-n_0) + n_0*n_1*(n_3-n_2)

               - entonces, 
                 - calcular A(BC) es mejor si 
                   - n_0>n_3 y n_2>n_1, o
                   - n_1<n_0 y n_3<n_2
                   - (...)
                 - calcular (AB)C es mejor si
                   - n_0<n_3 y n_2<n_1, o
                   - n_1>n_0 y n_3>n_2
                   - (...)

             - Cual son la recurrencia y el algoritmo de programación
               dinámica para calcular el producto de k matrices?

               - Sea M[i,j]=k la división óptima de la cadena
                 A_i\times\ldots\times A_j en 

                 ( A_i\times\ldots\times A_{k-1} ) 
                 \times
                 ( A_k\times\ldots\times A_j ) 

               - Sea C[i,j] su costo óptimo, igual a

                 C[i,j] =   C[i,k-1] + C[k,j] 
                          + n_{i-1}*n_k*n_j

               - La recurrencia es obvia, dado que C[i,i+1] =
                 n_{i-1}*n_i*n_{i+1}

               - La complejidad del algoritmo es O(n^3)


          3) Cambio de moneda
             
             Problema: Dado una soma $S$ y un conjunto de tipos de
             moneda $C=\{c_1,\ldots,c_k\}$, cual es la repartición en
             bloques minimizando el nombre de bloques?

             - Como resolver  si C={1,2,5,10,20,50,100}?
               - con solamente divisiones enteras (y sustracciones)

             - En cual caso la solución es valida pero no óptima?
               - C={1,2,4,5,10,20,50,100}?

             - Cual son la recurrencia y el algoritmo de programación
               dinámica para calcular el cambio óptima sobre S
               utilizando monedas de C?


          4) Arboles de búsqueda óptimos (con Carlos)

             Problema: Cual es el arbol de búsqueda óptimo para un
             conjunto ordenado de elementos, donde el precio c_i de
             acceder a cada elemento x_i puede ser distinto?

             - ya veamos un problema similar cuando los precios son
               probabilidades, y una solución con Hu-Tucker.
               - Complejidad: O(n\lg n)
               - Cualidad de la solución: n(1+H)

             - Si cada elemento $x_i$ tiene un costo $c_i$

4 Técnicas avanzadas de diseño de algoritmos (8 clases) 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

4.1 Uso de dominios discretos y finitos 
========================================
  
      1) Algoritmos de Ordenamiento ( Counting Sort, Bucket sort,
         radix sort, string sort)

         1. Counting Sort   O(\sigma + n)

            1. for j=1 to \sigma do C[j] <- 0
            2. for i=1 to n do C[A[i]]++
            3. p<-1 
            4. for j=1 to \sigma do
               - for i=1 to C[j] do
                 - A[p++] <- j

           Este algoritmo es bueno para ordenar multi conjuntos (donde
           cada elementos puede ser presente muchas veces), pero pobre
           para diccionarios, para cual es mejor usar la extensión
           lógica, Bucket Sort.

         2. Bucket Sort   O(\sigma+n)

            1. for j=1 to \sigma do C[j] <- 0
            2. for i=1 to n do C[A[i]]++
            3. P[3] <- 1
            4. for j<- 2 to \sigma do 
               - P[j] <- P[j-1] + C[j-1]
            5. for i<-1 to n
               - B[P[A[i]]++] <- A[i]

            Este algoritmo es particularmente practica para ordenar
            llaves asociadas con objetos, donde dos llaves pueden ser
            asociadas con algunas valores distintas. Nota que el
            ordenamiento es *estable*.

         3. Radix Sort O(n \lg_n \sigma) = O(c n)


            - Considera un arreglo A de tamaño n sobre alfabeto $\sigma$
              - si $\sigma=n$, se recuerdan que bucket sort puede
                ordenar A en O(n)
              - si $\sigma=n^2$, bucket sort puede ordenar A en O(n):
                - 1 ves con los $\lg n$ bits de la derecha
                - 1 ves con los $\lg n$ bits de la izquierda
                  (utilizando la estabilidad de bucket sort)
              - si $|A|=n^c$, bucket sort puede ordenar A 
                - en tiempo $O(cn)$
                - con espacio 2n + \sigma \approx 3n
                  (\sigma \approx n a cada iteración de bucket sort)

              El espacio se puede reducir a 2n+\sqrt{n} con \lg n/ 2
              bits a cada iteración de Bucketsort, cambiando la
              complejidad solamente por un factor de 2.

              En final, si $A$ es de tamaño $n$ sobre un alfabeto de
              tamaño $\sigma$, radix sort puede ordenar $A$ en tiempo
              O( n \lceil \frac{\lg \sigma}{\lg n}\rceil )

         4. String Sort   

            + Problema: Ordenar $k$ strings sobre alfabeto [\sigma], de
              largo total n=\sum n_i.

            + Si \sigma \leq k, y cada string es de mismo tamaño.

              - Si utilizamos bucket-sort de la derecha a la izquierda,
                podemos ordenar en tiempo $O(n)$, porque O(n \lceil \lg
                \sigma/\lg l \rceil ) y \sigma<n.

            + Si \sigma \in O(1)

              - Radix Sort sobre $c$ simboles, donde $c$ es el tamaño
                mínima de una string, y iterar recursivamente sobre el
                restos de la strings mas grande con mismo prefijo.

              - En el peor caso, la complejidad corresponde a la suma de
                las superficias de los bloques, aka O(n).
              

4.2 Tries o Arboles  Digitales 
===============================

      1) Tries o Arboles Digitales

         Ordenamos usualmente como pre-computación para buscar
         después. En el caso donde n es demasiado grande, ordenar puede
         ser demasiado carro. Consideramos alternativas para buscar.

         1. Ejemplo de trie

            - Insertar los nodos siguiente en un trie
              - hola
              - holistico
              - holograme
              - hologramas
              - ola
              - ole

            - .
              - h
                - o
                  - l
                     - a -> hola
                     - i -> holistica
                     - o 
                       - g
                         - r
                           - a
                             - m
                               - a
                                 - $ -> holograma
                                 - s -> hologramas
              - o
                - l
                  - a -> ola
                  - e -> ole

         2. Búsqueda

            - con arreglos de tamaño \sigma en cada nodo: 
              - O(l) tiempo, pero O(L\sigma) espacio
            - con arreglos de tamaño variables en cada nodo:
              - O(l \lg sigma) tiempo (búsqueda binaria),  O(L) espacio
                (óptima).
            - con hashing
              - O(l) tiempo en promedio, O(L) espacio.

         3. Inserción

            - Insertar "hora" en el arbol precedente
              - .
                - h
                  - o
                    - l
                       - a -> hola
                       - i -> holistica
                       - o 
                         - g
                           - r
                             - a
                               - m
                                 - a
                                   - $ -> holograma
                                   - s -> hologramas
                    - r
                      - a -> *hora*
                - o
                  - l
                    - a -> ola
                    - e -> ole

            - Insertar "holistico" en el arbol precedente
              - .
                - h
                  - o
                    - l
                       - a -> hola
                       - i
                         - s
                           - t
                             - i
                               - c
                                 - a -> holistica
                                 - o -> holistico
                       - o 
                         - g
                           - r
                             - a
                               - m
                                 - a
                                   - $ -> holograma
                                   - s -> hologramas
                    - r
                      - a -> *hora*
                - o
                  - l
                    - a -> ola
                    - e -> ole

            - Borrar "hola" y "holistica"
              - (TAREA)
              - Tiene de "limpiar", pero no costo mas que un factor
                constante de la búsqueda.



         4. PAT Trie: TAREA.


      2) Arboles de Sufijos

         - Espacio O(n)

         - Construcción O(n)

         - Búsqueda O(m)

         - expresión regular O(n^\lambda) donde 0\leq \lambda \leq 1

      3) Arreglo de Sufijos

         - Lista de sufijos ordenados
         - búsqueda de patrones = dos búsquedas binarias, donde cada
           comparación costa \leq m, resultando en una complejidad de
           O(m \lg n)


      5) Rank en Bitmaps

         - rank(B,i) = cantidad de unos en B[1,i]
         - consideramos 
           - B estático
           - se puede almacenar $\lg n$ bits.

         - Solucion de Munro, Raman y Raman:

           - b= 1/2 lg n   y    s = lg^2  n

           - Dividimos el index de B en 

             - $s$ Superbloques de tamaño n/s \lg n bits

             - $b$ Mini bloques de tamaño n/b \lg s bits
               - \frac{n}{1/2 \lg n} \lg(\lg^2 n) 
               - = \frac{4n \llg n}{\lg n}
               - \in o(n)

             - un diccionario con todos los bit vectores de tamaño

               - \sqrt{n} \lg n /2 \llg n  \in o(n)

4.3 Colas de prioridades y Heapsort 
====================================

      1) Análisis amortizada   

         * Costo Amortizado:
           - Se tiene una secuencia de n operaciones con costos
             c1,c2,...,cn. Se quiere determinar C=\sum c_i.
           - Se puede tomar el peor caso de ci\leq t para tener una
             cota superior de C\leq t n.
           - Un mejor análisis puede analizar el costo amortizado,
             con varias técnicas:
             - análisis agragada
             - contabilidad de costos
             - función potencial.

         * Ejemplo simple: Incremento binario

            - Incrementar n veces un numero binario de k bits,
            - e.g. desde cero hasta 2^k-1, con n=2^k.
            - costo \leq k n   (brute force)
            - costo \leq n + n/2 + ... \leq 2n (costo amortizado)
            - La técnica usada aquí es la contabilidad de costos:
              - un flip de 0 a 1 cuesta 2
              - un flip de 1 a 0 cuesta 0
              - cada incremento cuesta 2.


         * Función Potencial para contar el costo

           - \phi_0 = 0
           - \phi_i es el tamaño de la bolsa luego de ejecutar c_1,\ldots,c_i
           - \Delta \phi_i = \phi_i - \phi_{i-1}
           - costo amortizado = \overline{c_i} = c_i + \Delta\phi_i
           - \sum \overline{c_i} = \sum c_i + \phi_n -\phi_0 \geq \sum c_i

         * En el ejemplo de Incrementacion binaria: 
           - \phi = cantidad de unos en el numero
           - \phi_0 = 0
           - c_i = l+1 cuando hay l unos
           - \Delta \phi_i = -l+1
           - \sum \overline{c_i} = \sum c_i + \phi_n -\phi_0 
                                \geq 2

         * Ejemplo: realocación amortizada

           - considera el tipo "Vector" en Java.
           - de tamaño fijo n
           - cuando accede a n+1, crea un otro arreglo de tamaño 2n, y
             copia todo.
           - cual es el costo amortizado si agregando elementos uno a
             uno?



      2) Colas de Prioridades

         * REFERENCIA:    [http://www.leekillough.com/heaps/]
         
         * Problema: Dado un conjunto (dinámica) de $n$ tareas con
           valores, elegir y remudar la tarea de valor máxima.

         * operaciones básicas:
           - M.build({e1,e2,...,2n})
           - M.insert(e)
           - M.min
           - M.deleteMin

         * operaciones adicionales ("Addressable priority queues")
           - M.insert(e), volviendo un puntero h ("handle") al elemento insertado
           - M.remove(h), remudando el elemento especificado para h
           - M.decreaseKey(h,k), reduciendo la llave del elemento especificado para h
           - M.merge(Q), agregando el heap Q al heap M.
            
         * Soluciones conocidas o no:

                Linked List   Binary Tree     (Min-)Heap      Fibonacci Heap   Brodal Queue [3]  
 -------------+-------------+---------------+---------------+----------------+------------------
  insert        O(1)          O(log n)        O(log n)        O(1)             O(1)              
  accessmin     O(n)          O(1)            O(1)            O(1)             O(1)              
  deletemin     O(n)          O(log n)        O(log n)        O(log n)*        O(log n)          
  decreasekey   O(1)          O(log n)        O(log n)        O(1)*            O(1)              
  delete        O(n)          O(n)            O(log n)        O(log n)*        O(log n)          
  merge         O(1)          O(m log(n+m))   O(m log(n+m))   O(1)             O(1)              


      1) Colas de prioridades binarias ("Binary Heaps")

         * La tradicional solución
           - un arreglo de n elementos
           - hijos del nodo i en posiciones 2i y 2i+1
           - la valor de cada nodo es mas pequeña que las valores de su hijos.

         * Complejidad: Espacio n+O(1), y 
             Operación                 Tiempo    
            -------------------------+----------
             M.build({e1,e2,...,2n})   O(n)      
             M.insert(e)               O(\lg n)  
             M.min                     O(1)      
             M.deleteMin               O(\lg n)  

         * Detalles de implementacion (mejorando las constantes)

           - Cuidado de no implementar M.build({e1,e2,...,2n}) con n
             inserciones (sift up -> O(n\lg n)), pero con n/2
             sift-down (-> O(n)).

           - En M.deleteMin(), algunas variantes de implementación
             (después de cambiar el min con A[n]):
             1. dos comparaciones en cada nivel hasta encontrar la
                posición final de A[n] 
                - 2\lg n comparaciones en el peor caso
                - \lg n copias
             2. una comparación en cada nivel para definir un camino
                de tamaño lg n, y una búsqueda binaria para encontrar
                la posición final
                - \lg n + O(\lg \lg n) comparaciones en el peor caso
                - \lg n copias en el peor caso
             3. una comparación en cada nivel para definir un camino
                de tamaño lg n, y una búsqueda *secuencial* up para encontrar
                la posición final
                - 2\lg n  comparaciones en el peor caso
                - \lg n copias en el peor caso
                - pero en practica y promedio mucho mejor.


         * Flexibilidad de estructuras
           + Porque la complejidad es mejor con un heap que con un
             arreglo ordenado?
             - Para cada conjunto, hay solamente un arreglo ordenado
               que puede representarlo, pero muchos heaps posibles:
               eso da mas flexibilidad para la mantención dinámica de
               la estructura de datos.

           + Colas de prioridades con punteros ("Addressable priority queues")
             - Algún que mas flexible que los arreglos ordenados, las
               colas de prioridades binarias todavía son de estructura
               muy estricta, por ejemplo para la union de
               filas. Estructuras mas flexibles consideran un "bosque"
               de arboles. Además, estas estructuras de arboles son
               implementadas con puntadores (en ves de implementarlos
               en arreglos).
             - Hay diferentes variantes. Todas tienen en común los
               puntos siguientes:
               - un puntero *minPtr* indica el nodo de valor mínima
                 en el bosque, raíz de alguno arbol.
               - *insert* agregas un nuevo arbol al bosque en tiempo O(1)
               - *deleteMin* remudas el nodo indicado par minPtr,
                 dividiendo su arbol en dos nuevos arboles. Buscamos
                 para el nuevo min y fusionamos algunos arboles (los
                 detalles diferencian las variantes)
               - *decreaseKey(h,k)* es implementado cortando el arbol
                 al nodo indicado por h, y rebalanceando el arbol
                 cortado.
               - *delete()* es reducido a *decreaseKey(h,0)* y
                 *deleteMin*

      2) "Pairing Heaps"
         
         - Malo rendimiento en el peor caso, pero bastante buena en
           practica.

         - rebalancea los arboles solamente en deleteMin, y solamente
           con pares de raises (i.e. la cantidad de arboles es
           reducida por dos a cada deleteMin).

             Operación          Amortizado                
            ------------------+--------------------------
             Insert(C,x)        O(1)                      
             Merge              O(1)                      
             ExtractMin         O(lg n)                   
             decreaseKey(h,k)   \Omega(n \lg n \lg\lg n)  


      3) Colas de prioridades binomiales ("Binomial Heaps")

         [http://en.wikipedia.org/wiki/Binomial_heap]
         o p136 de Melhorn y Sanders

         * Definición
           - Un *arbol binomial* (de orden k) tiene exactamente $k$
             hijos de orden distintos k-1,k-2,..., 0. [Un arbol
             binomial de orden 0 tiene 0 hijos.]
           - Un *bosque binomial* es un conjunto de arboles binomiales
             de orden *distintas* (i.e. hay cero o uno arboles de cada
             orden).

         * Propiedades
           - Para cada arbol T
             - h(T) \leq lg |T|
             - |T| \geq 2^{h(T)}
           - Para el bosque
             - \forall n hay solamente uno bosque binomial con n nodos.
             - al máxima tiene $\lfloor \lg (n+1) \rfloor$ arboles.
             - la descomposición del bosque en arboles de orden k
               corresponde a la descomposición de n en base de dos.

         * Definición
           - una *cola binomial* es un bosque binomial donde cada nodo
             almacena una clave, y siempre la clave de un padre es
             inferior o igual a la clave de un hijo.

         * Operaciones
             Operación          Peor Caso  
            ------------------+-----------
             Merge              O(lg n)    
             FindMin            O(lg n)    
             ExtractMin         O(lg n)    
             Insert(C,x)        O(lg n)    
             Heapify            O(n)       
            ------------------+-----------
             remove(h)          O(lg n)    
             decreaseKey(h,k)   O(lg n)    
             merge(Q)           O(lg n)    

         * Union

           - Union de dos arboles binomiales de mismo orden:
             - agrega T_2 a T_1 si T_1 tiene la raíz mas pequeña.
           - Union de dos bosques binomiales:
             - si hay uno arbol de orden k, es lo de la union
             - si hay dos arboles de orden k, calcula la union en un arbol de orden k+1
             - la propagación es similar a la suma de enteros en binario.

           - Complejidad 
             - O(lg n) en el peor caso 

         * Insert
           - agrega un arbol de orden 0 y hace la union si necesitado
           - Complejidad O(log n) en el peor caso
           - Puede ser O(1) sin corregir el bosque, que tiene de ser
             corregido mas tarde, que puede ser en tiempo O(n) peor
             caso, pero sera O(lg n) en tiempo amortizado.

         * Mínima
           - leí la lista de al máxima $\lfloor \lg (n+1) \rfloor$ raíces
           - Complejidad O(lg n)
           - Puede ser O(1) si precalculando un puntero al mínima, que
             tiene de ser corregido (en tiempo O(\lg n)) a cada modificación.

         * DeleteMin
           - encontra el min
           - remuda el min de su arbol (la raíz)
           - reordena su hijos para su orden, en un bosque binomial
           - hace la union con el bosque binomial original, menos el arbol del min
           - complejidad O(lg n)

         * DecreaseKey
           - sigue el camino abajo hasta que la condición del heap es
             corregida.
           - cada arbol tiene altura lg n, entonces la complejidad es
             O(lg n) en el peor caso.

         * Delete 
           - reducido a DecreaseKey+DeleteMin
        


      4) Colas de prioridades de Fibonacci ("Fibonacci Heaps")

         [http://en.wikipedia.org/wiki/Fibonacci_heap] o pagina 135 de
         Melhorn y Sanders.


         * Diferencia con la cola binomial:

           - relax la estructura de los arboles (heap-forma), pero de
             forma controlada.
           - el tamaño de un sub-arbol cual raíz tiene k hijos es al
             máxima F_k+2, donde F_k es el k-ésimo numero de
             Fibonacci.

         * Operaciones
             Operación          Peor Caso   Amortizado  
            ------------------+-----------+------------
             Merge              O(lg n)     O(1)        
             FindMin            O(lg n)     O(1)        
             ExtractMin         O(lg n)     .           
             Insert(C,x)        O(lg n)     O(1)        
             Heapify            O(n)        .           
            ------------------+-----------+------------
             remove(h)          O(lg n)     .           
             decreaseKey(h,k)   O(lg n)     O(1)        
             merge(Q)           O(lg n)     O(1)        

      5) Overview
         (copy de [http://en.wikipedia.org/wiki/Fibonacci_heap])


                           Linked List   Binary Tree     (Min-)Heap      Fibonacci Heap   Brodal Queue [3]  
            -------------+-------------+---------------+---------------+----------------+------------------
             insert        O(1)          O(log n)        O(log n)        O(1)             O(1)              
             accessmin     O(n)          O(1)            O(1)            O(1)             O(1)              
             deletemin     O(n)          O(log n)        O(log n)        O(log n)*        O(log n)          
             decreasekey   O(1)          O(log n)        O(log n)        O(1)*            O(1)              
             delete        O(n)          O(n)            O(log n)        O(log n)*        O(log n)          
             merge         O(1)          O(m log(n+m))   O(m log(n+m))   O(1)             O(1)              

      6) Heapsort

         - in place
         - O(n lg n) con cualquiera de estas variantes.
         - la próxima ves veamos como ordenar mas rápidamente.

4.4 Memoria secundaria 
=======================

     1) Modelo de Memoria Jerárquica

        - disco duro (vs RAM)
          - latencia
          - bloques
          - Branching heuristics
        - Fast Expensive Memory and Slow Cheap Memory
        - Otras razones para una jerarquía:
          - indexing space
          - localidad in 3D


     2) Características Físicas de la memoria

        - Estructura de un disco duro, en algunos discos con una cabeza
          cada uno, que tiene de mudar en la pista necesaria.

        - Descomposición del tiempo de búsqueda:
          - t_seek   SEEK
          - + t_lat    LATENCIA
          - + t_trf    TRANSFERENCIA

        - Leamos bloques, no bytes
        - Es mas barato de leer 2 bloques siguiendo, o cerca, que
          lejos. => intentamos de construir estructuras de datos con
          acceso secuencia.

     3) Modelo de memoria Secundaria

        - Bloques de tamaño B
        - memoria RAM de tamaño M
        - contamos la cantidad de lecturas y escrituras de bloques.

     4) Búsqueda (iterated) en memoria jerárquica

          1. en una arreglo
             - búsqueda binaria
             - vs búsqueda secuencial
             - vs doubling search
             - vs doubling search blocked

          2. en arboles 
             - arbol de búsqueda binaria
             - B-arbol
             - arbol de van Emde Boas

4.5 Merge Sort en Memoria Secundaria 
=====================================

      1) Binary Merge Sort

         Ya le veamos en CC30A. 

         1. Algoritmo ingenuo

            - considera cada elemento como un arreglo ordenado

            - construir un arbol binario balanceado sobre los elementos

            - en cada nodo, calcula la union ordenada de los arreglos
              de su hijos.

            - Análisis: el tiempo es $O(n\lg n)$

         2. Algoritmo adaptativo [Knuth]

            - en tiempo lineal, identificar $\rho$ partes que ya están
              ordenadas, como "runs". Por ejemplo

              4,5,6,1,2,3,7,8

              tiene dos "runs": $4,5,6$ y $1,2,3,7,8$

            - Construir un arbol binario donde estos runs están las
              hojas, y iterar mergesort en los nodos

            - Análisis: el tiempo es $O(n\lg \rho)\subset O(n\lg n)$

         3. Algoritmo mas adaptativo [Barbay y Navarro]           :OPTIONAL:

            - en tiempo lineal, identificar $\rho$ partes que ya están
              ordenadas, como "runs", de tamaños respectivos
              $n_1,\ldots,n_\rho$. Por ejemplo:

              9,10,4,5,6,1,2,3,7,8 tiene tres "runs": $9,10$, $4,5,6$
              y $1,2,3,7,8$, de tamaños respectivos <2,3,5>

            - Construir un arbol binario de Hufman sobre estos runs
              minizando el nombre de comparaciones para calcular su
              union.

            - Análisis: el tiempo es $O(nH(n_1,\ldots,n_\rho))\subset O(n\lg \rho)$


      2) B-ary Merge Sort
   REFERENCIA: Data Structures and Algorithm Analysis, Mark Allen Weiss
   REFERENCIA: Algorithms in C, Robert Sedgewick, p 177


         Merge sort tiene bueno localidad de acceso, que es bueno
         para el acceso a la memoria, pero todavía se puede mejorar
         el acceso a la memoria,

         La idea es de utilizar el hecho que merge sort produce los
         elementos de manera ordenada: se puede utilizar un "buffer"
         para construir un bloque antes de escribirlo en memoria
         secundaria.

         Por ejemplo, con dos bloques en memoria, de $B$ elementos
         ordenados cada unos, y un bloque libre en memoria que puede
         contener $B$ elementos, se puede hacer el "merge" de los dos
         bloques hasta llenar el bloque libre, escribirlo en
         secundaria memoria, terminar de fusionar los bloques, y
         escribir el bloque en memoria: los accesos a la memoria
         secundaria están optamos.


4.6 van Emde Boas 
==================


   1) Arboles de Van Emde Boas

      [http://en.wikipedia.org/wiki/Van_Emde_Boas_tree]

      - Generalización de 
        - arboles binarios sobre dominio $n$ con comparaciones, de altura $\lg n$  
        - arboles con $\sqrt{n}$ hijos sobre dominio $n=2^m=2^{2^k}$, de altura $k=\lg m=\lg\lg n$.

      - Intuición: 
        - el arbol tiene altura $\lg\lg n$ 
        - llave $x$ vas en hijo $\lfloor x/\sqrt{n}\rfloor$ 
        - Un indexe indica cual es el próximo hijo no vacilo.
        - operadores
          * Insert
          * Delete
          * Lookup
          * FindNext
          * FindPrev

      - Implementación
        * Cada arbol/subárbol con $n$ elementos tiene
          - min
          - max
          - $\sqrt{n} hijos
          - un indexe de cual hijos están vasillos

        * FindNext(T, x)
          1. if (x <= T.min)
             return T.min
          2. i = floor(x/sqrt(M))
          3. if (x <= T.children[i].max)
             return FindNext(T.children[i], x % sqrt(M))
          4. return T.children[FindNext(T.aux,i+1)].min

        * Insert(T, x)
          1.  if (T.min > T.max)    // T is empty
              T.min = T.max = x;
              return
          2.  if (x < T.min)
              swap(x, T.min)
          3.  if (x > T.max)
              swap(x, T.max)
          4.  i = x/sqrt(M)
          5.  Insert(T.children[i], x % sqrt(M))
          6.  if (T.children[i].min = T.children[i].max)
              Insert(T.aux, i)

        * Delete(T, x)
          1. if (T.min `= T.max =' x)
             T.min = m
             T.max = -1
             return
          2. if (x == T.min)
             1) if (T.aux is empty)
                  - T.min = T.max
                  - return
             3) else
                - x = T.children[T.aux.min].min
                - T.min = x
          3. if (x == T.max)
               1) if (T.aux is empty)
                  T.max = T.min
                  return
               2) else
                  x = T.children[T.aux.max].max
                  T.max = x
          4. i = floor(x/sqrt(M))
          5. Delete(T.children[i], x%sqrt(M))
          6. if (T.children[i] is empty) 
             Delete(T.aux, i)

   2) Generalidades

      + Otros problemas:
        - ordenar en memoria jerárquica
          - mergesort
      + Modelos
        - cache-aware 
        - cache-oblivious
        - one pass computing

4.7 Online Algorithms - List Accessing 
=======================================

       REFERENCIA: Capitulo 1 en "Online Computation and Competitive
       Analysis", de Allan Borodin y Ran El-Yaniv

       1) "List Accessing"

          - Considera la secuencia de búsqueda de tamaño $n$, en un
            diccionario de tamaño $\sigma$:
            "1,1,1,1,1,1,2,2,2,3,3,3,4,4,4,5,5,5,6,6,6,..."

          - Cual es el rendimiento de una estructura de diccionario
            *estática* (tal que AVL) en este secuencia?
            - $n\lg\sigma$
            
          - Se puede mejorar?
            - si, utilizando la *localidad* de las consultas, en
              *estructuras de datos dinámicas*.

       2) Soluciones

          1. MTF ("Move To Front"): 
             - pone las llaves en un arreglo desordenado
             - buscas secuencialmente en el arreglo
             - muda la llave encontrada en frente
          2. TRANS ("Transpose"):
             - pone las llaves en un arreglo desordenado
             - buscas secuencialmente en el arreglo
             - muda la llave encontrada de una posición mas cerca del  frente
          3. FC ("Frequency Count"):
             - mantiene un contador para la frecuencia de cada elemento
             - mantiene la lista ordenada para frecuencia decreciente.

          4. Splay Trees
             ([http://www.dcc.uchile.cl/~cc30a/apuntes/Diccionario/#8b])

       3) Estos son "Algoritmos en Linea"
          - algoritmo de optimización
          - que conoce solamente una parte de la entrada al tiempo t.
          - se compara a la competitividad con el algoritmo offline que
            conoce toda la instancia.

          - Como se puede medir su complejidad?
            - cada algoritmo ejecuta O(n) comparaciones para cada
              búsqueda en el peor caso!!!!
            - tiene de considerar instancies "fáciles" y "difíciles"
            - una medida de dificultad
            - e.g. el rendimiento del mejor algoritmo offline


       4) Competitive Analysis: instancias "difíciles" o "fáciles"

          - Las estructuras de datos dinámicas pueden aprovechar de
            secuencias "fáciles" de consultas: eso se llama "online".

          - pero para muchos problemas online, todas las heurísticas se
            comportan de la misma manera en el peor caso.

          - Por eso se identifica una medida de dificultad de las
            instancias, y se comparan los rendimientos de los
            algoritmos sobre instancias que tienen una valor fijada de
            este medida de dificultad.

          - Tradicionalmente, esta medida de dificultad es el
            rendimiento del mejor algoritmo "offline": eso se llama
            *competitive analysis*, resultando en el *competitive
            ratio*, el ratio entre la complejidad del algoritmo ONLINE
            y la complejidad del mejor algoritmo OFFLINE.

            - por ejemplo, veamos que MTF tiene un competitive ratio de
              2

          - Pero todavía hay algoritmos con performancia practicas muy
            distintas que tienen el mismo competitive ratio. Por eso se
            introduce otras medidas de dificultadas mas sofisticadas, y
            mas especialidades en cada problema.

       5) Competitividad

          * Optimización/aproximación

            - A es k(n) competitiva  para un problema de *minimización* si
              - \exist b, \forall n,x |x|=n
                - C_A(x) - k(n) C_{OPT}(x) \leq b

            - A es k(n) competitiva  para un problema de *maximización* si
              - \exist b, \forall n,x |x|=n
                - C_{OPT}(x) - k(n) C_{A}(x) \leq b

          * Competitiva Ratio

            - an algoritmo en linea es c-competitiva si
              - \exists \alpha, \forall I 
                - ALG(I) \leq c OPT(I) + \alpha

            - an algoritmo en linea es estrictamente c-competitiva si
              - \forall I 
                - ALG(I) \leq c OPT(I) 

       6) Sleator-Tarjan sobre MTF

            - costo de una búsqueda negativa (la llave NO esta en el
              diccionario)
              - $\sigma$
            - costo de una búsqueda positiva (la llave esta en el
              diccionario)
              - la posición de la llave, no mas que $\sigma$
              - en promedio para una distribución de probabilidad fijada:
                - $MTF \leq 2 OPT$, 
              - Prueba: (from my notes in my CS240 slides)

                How does MTF compare to the optimal ordering?          
                - Assume that:
                  - the keys $k_1,\ldots,k_n$ have probabilities 
                    $p_1 \ge p_2 \ge \ldots \ge p_n \ge 0$
                  - the list is used sufficiently to reach a steady state.
                - Then: 
                  $$C_{MTF} < 2\cdot C_{OPT}$$
                - Proof:
                  \begin{eqnarray*}
                  C_{OPT} & = & \sum_{j=1}^{n} jp_j 
                  C_{MTF} & = & \sum_{j=1}^{n} p_j(\mbox{cost of finding $k_j$}) 
                  & = & \sum_{j=1}^{n} p_j(1+\alert{\mbox{number of keys before } k_j})
                  \end{eqnarray*}
   
                  - To compute the average number of keys before $k_j$:
                    \begin{eqnarray*}
                    \Pr[\mbox{ $k_i$ before $k_j$}] &=& \frac{p_i}{p_i+p_j} 
                    E(\mbox{ \alert{number of keys before $k_j$}}) &=&
                    \sum_{i\neq j} \frac{p_i}{p_i+p_j} 
                    \end{eqnarray*}

                  - $k_i$ is before $k_j$ if and only if
                    $k_i$ was accessed more recently than $k_j$. 

                  - Consider the last time either $k_i$ or $k_j$ was looked up. What is the probability that it was $k_i$?
                    \begin{eqnarray*}
                    P(k_i \textrm{ before } k_j) &=
                    %& P(k_i \textrm{ chosen }|\ k_i \textrm{ or }k_j\textrm{ chosen }) \\ &=
                    & \frac{P(k_i \textrm{ chosen })}{P(k_i \textrm{ or } k_j \textrm{ chosen })} 
                    &=& \frac{p_i}{p_i+p_j}
                    \end{eqnarray*}

                  - Therefore,
                    \begin{eqnarray*}
                    C_{MTF} & = & \sum_{j=1}^{n} p_j (1+\sum_{i\not = j} \frac{p_i}{p_i+p_j}) 
                    \mbox{\hspace{.5cm} (Joining both previous formulas.)}
                    
                    & = & 1 + 2 \sum_{j=1}^n \sum_{i<j} \frac{p_i p_j }{p_i+p_j} 
                    \mbox{\hspace{1cm} (By reordering the terms.)}
                    
                    & \leq &  1 + 2 \sum_{j=1}^n p_j (\sum_{i<j} 1) 
                    \mbox{\hspace{2cm} (Because $\frac{p_i}{p_i+p_j}\leq1$.)}
                    
                    & = & 1 +  2 \sum_{j=1}^n p_j (j-1) 
                    
                    & = & 1 + 2C_{OPT} + 2 \sum_{j=1}^n(-p_j) 
                    
                    & = & 2C_{OPT}-1.
                    \mbox{\hspace{3cm} (Because $\sum_{j=1}^n(p_j)=1$.) \qedhere}
                    \end{eqnarray*}  



       7) [OPCIONAL] Applicaciones a la compression de textos
                    

          * [Bentley, Sleator, Tarjan and Wei] proponieron de
            comprimir un texto utilizando una lista dinámica, donde el
            código para un símbolo es la posición del símbolo en la
            lista.

            - Experimentalmente, se compara a Huffman:
              - a veces mucho mejor
              - nunca mucho peor.

          * [Burrows and Wheeler] proponieron una transformación
            (biyectiva) del texto, y de comprimir el resultado de esta
            transformación con MTF

            - Experimentalmente, 6% mejor que GZip, que es enorme!

4.8 Algoritmos en Linea: Paginamiento Deterministico 
=====================================================

       REFERENCIA: Capitulo 2 en "Online Computation and Competitive
       Analysis", de Allan Borodin y Ran El-Yaniv
       
       1) Paginamiento

          - Definición:
            - elegir cual paginas guardar en memoria, dado
              - una secuencia online de $n$ consultas para paginas, y
              - un cache de $k$ paginas.
          - Políticas:  
            - LRU (Least Recently Used)
            - CLOCK (1bit LRU)
            - FIFO (First In First Out)
            - LFU (Least Frequently Used)
            - LIFO = MRU (Most Recently Used)
            - FWF (Flush When Full)
            - LFD (Offline, Longuest Forward Distance)

          - Ustedes tienen una idea de cuales son las peores/mejores?

       2) Relación con "List Accessing"

          1. Cada "List accessing" algoritmo corresponde a un algoritmo
             de paginamiento:
             - cada miss, borra el ultimo elemento de la lista y
               "inserta" el nuevo elemento.
          2. No hay una reducción tan clara en la otra dirección.


       3) Offline analysis

          - LFD performa O(n/k) misses
          - Cualquier algoritmo Offline performa Omega(n/k) en el peor
            caso.

       4) Online análisis: resultados básicos

          1. $\forall A$ online, hay una entrada con $n$ fallas.

             - Estrategia de adversario.

          2. No algoritmo online puede ser mejor que $k$ competitivo.

             - Obvio, comparando con LFD.

          3. MRU=LIFO NO es competitivo
             - Considera S=
               p_1,p_2,\ldots,p_k,p_{k+1},p_k,p_{k+1},p_k,p_{k+1},p_k,...
             - después las k primeras consultas, MRU va a tener un miss
               cada consulta, cuando LFD nunca mas.

          4. LFU no es competitivo 
             - Considera l>0 y S=
               p_1^l,p_2^l,\ldots,p_{k-1}^l,(p_k,p_{k+1})^{l-1}
             - Después de las (k-1)l primeras consultas, LFU va a tener
               un miss cada consulta, cuando LFD solamente dos.

          5. Que tal de FWF?

             - MRU=LIFO es un poco estúpido, su mala rendimiento no es
               una sorpresa.

             - FWF es un algoritmo muy ingenuo también, pero vamos a
               ver que no tiene un rendimiento tan mal "en teoría".

       5) Competive Analysis: Algoritmos a Marcas

          1. $k$-fases particiones

             Para cada secuencia $S$, partitionala en secuencias
             $S_1,\ldots,S_\delta$ tal que 
             - $S_0 = \emptyset$
             - $S_i$ es la secuencia Maxima después de $S_{i-1}$ que
               contiene al máximum $k$ consultas distintas.

             - Llamamos "fase $i$" el tiempo que el algoritmo considera
               elementos de la subsecuencia $S_i$.
             - Nota que eso es independiente del algoritmo considerado.

          2. Algoritmo con marcas 

             - agrega a cada pagina de memoria lenta un bit de marca.

             - al inicio de cada fase, remuda las marcas de cada
               pagina en memoria.

             - a dentro de una fase, marca una pagina la primera vez
               que es consultada.

             - un algoritmo a marca ("marking algorithm") es un
               algoritmo que nunca remuda una pagina marcada de su
               cache.

          3. Un algoritmo con marcas es $k$-competitiva

             - En cada fase, 
               - un algoritmo ONLINE con marcas performa al máximum $k$ miss.
               - Un algoritmo OFFLINE (e.g. LFD) performa al mínimum
                 $1$ miss.
             - QED

          4. LRU, CLOCK y FWF son  algoritmos con marcas

          5. LRU, CLOCK y FWF tienen un ratio competitivo ÓPTIMO


       6) Mas resultados:

          1. La análisis se puede generalizar al caso donde el
             algoritmo offline tiene h paginas, y el algoritmo online
             tiene $k\geq h$ paginas.

             - Cada algoritmo *con marcas* es
               $\frac{k}{k-h+1}$-competitiva.

          2. Definición de algoritmos (conservadores) da resultado
             similar para FIFO (que no es con marcas pero es
             conservador).

          4. En practica, sabemos que LRU es mucho mejor que FWF (for
             instancia). Había mucha investigación para intentar de
             mejorar la análisis por 20 anos, ahora parece que hay una
             análisis que explica la mejor rendimiento de LRU sobre
             FWF, y de variantes de LRU que pueden saber $x$ pasos en
             el futuro (Reza Dorrigiv y Alex Lopez-Ortiz).

5 Algoritmos no convencionales (9 clases) 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

5.1 DONE Algoritmos Aleatorizados 
==================================
   - Nota: Trae los juguetes/cajas de colores, con un tesoro a esconder a dentro.
   - REFERENCIA: 
     - Capitulo 1 en "Randomized Algorithms", de Rajeev Motwani and Prabhakar Raghavan.
     - Primalidad: Capitulo 14.6 en "Randomized Algorithms", de Rajeev Motwani and Prabhakar Raghavan.
   - Notes

    1) Algoritmos Aleatorizados: Introducción

       * Ejemplos

         1. Algunos que ya veamos?
            - Quicksort
            - (...)
         3. Decidir si un elemento pertenece en un lista desordenada de tamaño k
            * Si una sola instancia de la valor buscada
              - k en el peor caso deterministico
              - k/2 en (promedio y en) el peor caso aleatorio
                - con una dirección al azar
                - con lg(k!) bits aleatorios
            * Si r instancias de la valor buscada
              - k-r en el peor caso deterministico
              - O(k/r) en (promedio y en) el peor caso aleatorio
         4. Decidir si un elemento pertenece en una lista ordenadas de tamaño n
            * \Theta(\lg n) comparaciones en ambos casos, deterministico y probabilístico. 
         4. Decidir si un elemento pertenece en k listas ordenadas de tamaño n/k
            * Si una sola lista contiene la valor buscada
              - k búsquedas en el peor caso deterministico, que da
                k\lg(n/k) comparaciones
              - k/2 búsquedas en (promedio y en) el peor caso
                aleatorio, que da k\lg(n/k)/2 comparaciones
            * Si r<k listas contienen la valor buscada
              - k-r búsquedas en el peor caso deterministico, que dan
                (k-r)\lg(n/k-r) comparaciones
              - k/r búsquedas en (promedio y en) el peor caso
                aleatorio, que dan k/r\lg(rn/k) comparaciones
            * Si r=k listas contienen la valor buscada
              - k búsquedas en el peor caso deterministico, en
                promedio y en el peor caso aleatorio, que dan k\lg n
                comparaciones
         5. Eso se puede aplicar a la intersección de posting lists
            (Google Queries).


    2) Formalización

             Algoritmos clásicos      Non clásicos                   
            ------------------------+-------------------------------
             Siempre hacen lo mismo   aleatorizados                  
             Nunca se equivocan       probabilísticos (Monte Carlo)  
             Siempre terminan         probabilísticos (Las Vegas)    


    3) Clasificación de los algoritmos *de decisión* aleatorios

         1. Probabilístico: Monte Carlo
            - P(error) < épsilon
            - Ejemplo:
              - detección de cliquas
            - Se puede considerar también variantes mas fines:
              - two-sided error ( => clase de complejidad BPP)
                - P(accept | negative) < \epsilon
                - P(refuse | negative) > 1-\epsilon
                - P(accept | positive) > 1-\epsilon
                - P(refuse | positive) < epsilon
              - One-sided error
                - P(accept | negative) < epsilon
                - P(refuse | negative) > 1 -\epsilon 
                - P(accept | positive) = 1
                - P(refuse | positive) = 0

         2. Probabilístico: Las Vegas
            - Tiempo es una variable aleatoria
            - Ejemplos: 
              - determinar primalidad [Miller Robin]
              - búsqueda en arreglo desordenado 
              - intersección de arreglos ordenados
              - etc...

      * Relación
        - Si se puede verificar el resultado en tiempo razonable,
          Monte Carlos iterado hasta correcto => Las Vegas


    4) Complejidad de un algoritmo aleatorio

      * Considera algoritmos con comparaciones
        - algoritmos deterministicos se pueden ver como arboles de
          decisión.
        - algoritmos aleatorios se pueden ver (de manera
          intercambiable) como
          - una distribución sobre los arboles de decisión,
          - un arbol de decisión con algunos nodos "aleatorios".
        - La complejidad en una instancia de un algoritmo aleatorio
          es el promedio de la complejidad (en este instancia) de
          los algoritmos deterministicos que le compasan:

          C((A_r)_r,I) = E_r( C(A_r,I) )

        - La complejidad en el peor caso de un algoritmo aleatorio
          es el peor caso del promedio de la complejidad de los
          algoritmos deterministicos que le composan:

          C((A_r)_r) = \max_I C((A_r)_r,I) =  \max_I  E_r( C(A_r,I) )

        - Vamos a ver en la sección siguiente la relación entre el
          max y la esperanza.

5.2 Arboles Binarios de Busque da aleatorizados 
================================================
    - Conrado Martínez. Randomized binary search trees. Algorithms
      Seminar. Universitat Politecnica de Catalunya, Spain, 1996.

    - Conrado Martínez and Salvador Roura. Randomized binary search
      trees. J. ACM, 45(2):288–323, 1998.

    - [http://en.wikipedia.org/wiki/Treap#Randomized_binary_search_tree]

5.3 DONE SkipLists 
===================

      1) Estructuras de datos para diccionarios

       - [X] Arreglo ordenado
       - [ ] "Move To Front" list (did they see it already?)
       - [X] Arboles binarios
       - [X] Arboles binarios aleatorizados
       - [X] Arboles 2-3   ( they saw it already?)
       - [X] Red-Black Trees  ( they saw it already?)
       - [X] AVL
       - [X] Skip List 
       - [ ] Splay trees

      2) Skip Lists

         1. Motivación
            - un arbol binario con entradas aleatorizadas tienen una
              altura O(lg n), pero eso supone un orden de input
              aleatorizados.
            - El objetivo de las "skip lists" es de poner el aleatorio
              a dentro de la estructura.
            - también, es el equivalente de una búsqueda binaria en
              listas via un resumen de resumen de resumen...

         2. Definición 
            
            - una skip-list de altura $h$ para un diccionario $D$ de
              $n$ elementos es una familia de lists $S_0,\ldots,S_h$ y
              un puntero al primero elemento de $S_h$, tal que

              - $S_0$ contiene $D$;
              - cada $S_i$ contiene un subconjunto aleatorio de $S_{i-1}$
                (en promedio la mitad) 
              - se puede navegar de la izquierda a la derecha, y de la
                cima hasta abajo.

            - se puede ver como $n$ torres de altura aleatorizadas,
              conectadas horizontalmente con punteros de la
              izquierda a la derecha.

            - la información del diccionario están solamente en S_0 (no
              se duplica)

         3. Ejemplo

            4   X         -    -    -    -    -    -    ->   X       
            3   X         -    ->   X    -    -    -    ->   X       
            2   X         -    ->   X    X    ->   X    ->   X       
            1   X         X    ->   X    X    ->   X    ->   X       
            0   X         X    X    X    X    X    X    X    X       
           ---+---------+----+----+----+----+----+----+----+--------
                -\infty   10   20   30   40   50   60   70   \infty  

         4. Operaciones

            * Search(x): 
              - Start a the first element of $S_h$
              - while not in $S_0$
                - go down one level
                - go right till finding a key larger than x

            * Insert(x)
              - Search(x)
              - create a tower of random height (p=1/2 to increase
                height, typically)
              - insert it in all the lists it cuts.

            * Delete(x)
              - Search(x)
              - remove tower from all lists.

         5. Ejemplos 

            * Insert(55) con secuencia aleatora (1,0)

            4   X         -    -    -    -    -      -      -    ->   X       
            3   X         -    ->   X    -    -      -      -    ->   X       
            2   X         -    ->   X    X    -      ->     X    ->   X       
            1   X         X    ->   X    X    *->*   *X*    X    ->   X       
            0   X         X    X    X    X    X      *X*    X    X    X       
           ---+---------+----+----+----+----+------+------+----+----+--------
                -\infty   10   20   30   40   50     *55*   60   70   \infty  

            * Insert(25) con (1,1,1,1,0)

            5   *X*       -    -      -      -    -    -    -    -    *->*   *X*     
            4   X         -    *->*   *X*    -    -    -    -    -    ->     X       
            3   X         -    *->*   *X*    X    -    -    -    -    ->     X       
            2   X         -    *->*   *X*    X    X    -    ->   X    ->     X       
            1   X         X    *->*   *X*    X    X    ->   X    X    ->     X       
            0   X         X    X      *X*    X    X    X    X    X    X      X       
           ---+---------+----+------+------+----+----+----+----+----+------+--------
                -\infty   10   20     *25*   30   40   50   55   60   70     \infty  

            * Delete(60) 


            5   X         -    -    -    -    -    -    -    ->     X       
            4   X         -    ->   X    -    -    -    -    ->     X       
            3   X         -    ->   X    X    -    -    -    ->     X       
            2   X         -    ->   X    X    X    -    -    *->*   X       
            1   X         X    ->   X    X    X    ->   X    ->     X       
            0   X         X    X    X    X    X    X    X    X      X       
           ---+---------+----+----+----+----+----+----+----+------+--------
                -\infty   10   20   25   30   40   50   55   70     \infty  

         6. Analisis

            * Espacio: Cuanto nodos en promedio?
              - cuanto nodos en lista S_i?
                - $n/2^i$ en promedio
              - Summa sobre todos los niveles
                - $n \sum 1/2^i < 2n$

            * Tiempo: 
              - altura promedio es O(\lg n)
              - tiempo promedio es O(\lg n)

5.4 Complejidad Probabilistica: cotas inferiores 
=================================================
   * REFERENCIA: 
     - Capitulo 1 en "Randomized Algorithms", de Rajeev Motwani and Prabhakar Raghavan.

   * Notes:
    1) Problema

        - Strategia de adversario no funciona

        - En algunos casos, teoría de códigos es suficiente 
          - e.g. búsqueda en arreglo ordenado
          - eso es una cota inferior sobre el tamaño del
            certificado

        - En otros caso, teoría de códigos no es suficiente
          - en particular, cuando el precio para verificar un
            certificado es mas pequeño que de encontrarlo.
          - En estos casos, utilizamos otras técnicas:
            - teoría de juegos (que vamos a ver) y equilibro de Nash
            - cotas sobre la comunicación en un sistema de
              "Interactive Proof"

    2) Algunas Notaciones Algebraicas

       Sea:

       - A una familia de $n_a$ algoritmos deterministicos
       - $a$ un vector (0,...,0,1,0,...,0) de dimensión $n_a$
       - $\alpha$ una distribución de probabilidad de dimensión $n_a$

       - B una familia de $n_n$ instancias
       - $b$ un vector (0,...,0,1,0,...,0) de dimensión $n_b$
       - $\beta$ una distribución de probabilidad de dimensión $n_b$

       - M una matriz de dimensión $n_a\times n_b$ tal que M_{a,b}
         es el costo del algoritmo $a$ sobre la instancia $b$.
         Por definición,
         - a^t M b = M_{a,b}
         - \alpha^t M b es la complejidad en promedio (sobre el
           aleatorio del algoritmo \alpha) de \alpha sobre b
         - a^t M \beta es la complejidad en promedio (sobre la
           distribución de instancias \beta) de a sobre \beta
         - \alpha^t M \beta es la complejidad en promedio del
           algoritmo aleatorizados \alpha sobre la distribución de
           instancia \beta.


    3) von Neuman's theorem: infsup = supinf = minmax = maxmin

       1. OPCIONAL Existencia de $\tilde{\alpha}$ et $\tilde{\beta}$ 

          Dado  $\phi$ y $\psi$ definidas sobre $\mathbb{R}^m$ y $\mathbb{R}^n$ por
          $$\phi(\alpha) = \sup_\beta \alpha ^T M \beta 
          \,\mbox{  y  }\,
          \psi(\beta)  = \inf_\alpha \alpha ^T M \beta$$
          Entonces:

          - $\phi(\alpha) = \max_\beta \alpha ^T M \beta$ 
          - $\psi(\beta)  = \min_\alpha \alpha ^T M \beta$
          - hay estrategias mixtas  
            - $\tilde{\alpha}$ por \A\
            - $\tilde{\beta}$ por \B\ 
          - tal que 
            - $\phi$ es a su mínima  en $\tilde{\alpha}$ y
            - $\psi$ es a su máxima en $\tilde{\beta}$.

       2. Resultado de von Neuman: 

          Dado un juego $\Gamma$ definido por la matrica  $M$~:
          $$
          \min_\alpha \max_\beta  \alpha ^T M \beta 
          =
          \max_\beta  \min_\alpha \alpha ^T M \beta 
          $$

       3. Interpretación:

          + Este resultado significa que si consideramos ambos
            distribuciones sobre algoritmos y instancias, no
            importa el orden del max o min:
               - podemos elegir el mejor algoritmo (i.e. minimizar
                 sobre los algoritmos aleatorizados) y después
                 elegir la peor distribución de instancias para el
                 (i.e. maximizar sobre las distribuciones de
                 instancias), o al reves
               - podemos elegir la peor distribución de instancias
                 (i.e. maximizar sobre las distribuciones de
                 instancias), y considerar el mejor algoritmo
                 (i.e. minimizar sobre los algoritmos
                 aleatorizados) para este distribución.
          + ATENCIÓN!!!!  Veamos que 
            - El promedio (sobre las instancias) de las
              complejidades (de los algoritmos) en el peor caso
            - no es igual 
            - al peor caso (sobre las instancias) de la complejidad
              en promedio (sobre el aleatorio del algoritmo)
            - donde el segundo termo es realmente la complejidad de
              un algoritmo aleatorizados.

          + Todavía falta la relación con la complejidad en el
            peor caso $b$ de un algoritmo aleatorizados $\alpha$:

            $\max_b  \min_\alpha \alpha ^T M b$


    4) Lema de Loomis 

       * Dado una estrategia aleatoria $\alpha$, emite una instancia
         $b$ tal que $\alpha$ es tan mal en $b$ que en el
         peor $\beta$.

         $$
         \forall \alpha \exists b,
         \max_\beta \alpha^T M \beta = \alpha^T M b
         $$

       * Dado una distribución de instancias $\beta$, existe un
         algoritmo deterministico $a$ tal que $a$ es tan
         bien que el mejor algoritmo aleatorizados $\alpha$ sobre
         la distribución de instancias $\beta$:

         $$
         \forall \beta \exists a,
         \min_\alpha \alpha^T M \beta = a^T M \beta
         $$

       * Interpretación:

         + En frente a una distribución de instancias especifica,
           siempre existe un algoritmo deterministico óptima en
           comparación con los algoritmos aleatorizados (que
           incluen los deterministicos).

         + En frente a un algoritmo aleatorizados, siempre existe
           una instan ca tan mal que la pero distribución de
           instancias.

    5) Príncipe de Yao

       * Del leima de Loomis podemos concluir que 

         $$
         \max_\beta \alpha^T M \beta = \max_b \alpha^T M b
         $$

         $$
         \min_\alpha \alpha^T M \beta = \min_a a^T M \beta
         $$

       * Del resultado de von Neuman sabemos que maxmin=minmax
         (sobre \alpha y \beta):

          $$
          \min_\alpha \max_\beta  \alpha ^T M \beta 
          =
          \max_\beta  \min_\alpha \alpha ^T M \beta 
          $$

       * Entonces    

         $$
         \min_alpha \max_b \alpha^T M b
         = (Loomis)
         \min_\alpha \max_\beta  \alpha ^T M \beta 
         = (von Neuman)
         \max_\beta  \min_\alpha \alpha ^T M \beta 
         = (Loomis)
         \max_\beta \min_a a^T M \beta
         $$

       * Interpretación

          \min_\alpha   \max_b   \alpha^T M b                                     
                                 \_______       La complejidad                    
              \______  _______   ________       del mejor algoritmo aleatorizado  
                        \___     ___________    en el peor caso                   

         es igual a 

          \max_\beta   \min_a    \alpha^T M b                                             
                                 \_______       La complejidad                            
                       \___      ___________    del mejor algoritmo deterministico        
          \______      _______   ________       sobre la peor distribución de instancias  


         "El peor caso del mejor algoritmo aleatorizado
         corresponde a
         la peor distribución para el mejor algoritmo deterministico."


   * Ejemplos de cotas inferiores:

     1. Decidir si un elemento pertenece en una lista ordenadas de tamaño n

        - Cual es el peor caso $b$ de un algoritmo aleatorizado $\alpha$?
        - Buscamos una distribución $\beta_0$ que es mala para todos
          los algoritmos deterministicos $a$ (del modelo de comparaciones)
        - Consideramos la distribución uniforma.
        - Cada algoritmo deterministico se puede representar como un
          arbol de decisión (binario) con $2n+1$ hojas.

        - Ya utilizamos para la cota inferior deterministica que la
          altura de un tal arbol es al menos $\lg(2n+1)\in\Omega(\lg
          n)$. Esta propiedad se muestra por recurrencia.
        - De manera similar, se puede mostrar por recurrencia que la
          altura en promedio de un tal arbol binario es al menos
          $\lg(2n+1)\in\Omega(\lg n)$.

        - Entonces, la complejidad promedio de cada algoritmo
          deterministico $a$ sobre $\beta_0$ es al menos
          $\lg(2n+1)\in\Omega(\lg n)$.

        - Entonces, utilizando el principie de Yao, la complejidad en
          el peor caso de un algoritmo aleatorizado en el modelo de
          comparaciones es al menos $\lg(2n+1)\in\Omega(\lg n)$.

        - El corolario interesante, es que el algoritmo
          deterministico de búsqueda binaria es *oprima* a dentro de
          la clase mas general de algoritmos aleatorizados.

     2. Decidir si un elemento pertenece en un lista desordenada de tamaño k
        * Si una sola instancia de la valor buscada ($r=1$)
          * Cotas superiores
            - k en el peor caso deterministico
            - (k+1)/2 en el peor caso aleatorio
              - con una dirección al azar
              - con lg(k!) bits aleatorios
          * Cota inferior
            - Buscamos una distribución $\beta_0$ que es mala para
              todos los algoritmos deterministicos $a$ (en el modelo de
              comparaciones).
            - Consideramos la distribución uniforma (cada algoritmo
              reordena la instancia a su gusto, de toda manera,
              entonces solamente la distribución uniforma tiene
              sentido): cada posición es elegida con probabilidad $1/k$
            - Se puede considerar solamente los algoritmos que no
              consideran mas que una ves cada posición, y que
              consideran todas las posiciones en el peor caso:
              entonces cada algoritmo puede ser representado por una
              permutación sobre $k$.

            - Dado un algoritmo deterministico $a$, para cada
              $i\in[1,k]$, hay una instancia sobre cual el performe
              $i$ comparaciones. Entonces, su complejidad en promedio
              en este instancia es $\sum_i i/k$, que es $k(k+1)/2k =
              (k+1)/2$. Como eso es verdad para todos los algoritmos
              deterministicos, es verdad para el mejor de ellos
              también.

            - Entonces, utilizando el principio de Yao, la complejidad
              en el peor caso de un algoritmo aleatorizado en el
              modelo de comparaciones es al menos $(k+1)/2$.

        * Si r instancias de la valor buscada
          * Cotas superiores
            - k-r en el peor caso deterministico
            - O(k/r) en (promedio y en) el peor caso aleatorio
          * Cota inferior
            - Buscamos una distribución $\beta_0$ que es mala para
              todos los algoritmos deterministicos $a$ (en el modelo de
              comparaciones).
            - Consideramos la distribución uniforma (cada algoritmo
              reordena la instancia a su gusto, de toda manera,
              entonces solamente la distribución uniforma tiene
              sentido): cada posición es elegida con probabilidad $1/k$
            - Se puede considerar solamente los algoritmos que no
              consideran mas que una ves cada posición. 
            - De verdad, no algoritmo tiene de considerar mas
              posiciones que $k-r+1$, entonces hay menos algoritmos
              que de permutaciones sobre $k$ elementos. Para
              simplificar la prueba, podemos exigir que los algoritmos
              especifican una permutación entera, pero no vamos a
              contar las comparaciones después que un de las $r$
              valores fue encontrada.



     3. Decidir si un elemento pertenece en k listas ordenadas de tamaño n/k
        * Cotas superiores
           * Si una sola lista contiene la valor buscada
             - k búsquedas en el peor caso deterministico, que da
               k\lg(n/k) comparaciones
             - k/2 búsquedas en (promedio y en) el peor caso
               aleatorio, que da k\lg(n/k)/2 comparaciones
           * Si r<k listas contienen la valor buscada
             - k-r búsquedas en el peor caso deterministico, que dan
               (k-r)\lg(n/(k-r)) comparaciones
             - k/r búsquedas en (promedio y en) el peor caso
               aleatorio, que dan (k/r)\lg(n/k) comparaciones
           * Si r=k listas contienen la valor buscada
             - k búsquedas en el peor caso deterministico, en promedio y
               en el peor caso aleatorio, que dan k\lg(n/k) comparaciones

     4. Aplicación: 

        algoritmos de intersección de listas ordenadas.



   * Conclusión:

     * Relación fuerte entre algoritmos aleatorizados y complejidad en
       promedio

     * El peor caso de un algoritmo aleatorio corresponde a la
       peor distribución para un algoritmo deterministico.

   * Otras aplicaciones importantes de los algoritmos aleatorizados

     * "Online Algorithms", en particular paginamiento.

     * Algoritmos de aproximación

     * Hashing

5.5 Paginamiento al Azar :OPTIONAL:
===================================
   - REFERENCIA: 
     - Capitulo 3 en "Online Computation and Competitive Analysis", de
       Allan Borodin y Ran El-Yaniv
     - Capitulo 13 en "Randomized Algorithms", de Rajeev Motwani and
       Prabhakar Raghavan, p. 368


   1) Tipos de Adversarios (cf p372 [Motwani Raghavan]

      * Veamos en el caso deterministico un tipo de adversario
        offline, como medida de dificultad para las instancias online.
        Para el problema de paginamiento con k paginas, el ratio
        óptima entre un algoritmo online y offline es de $k$
        (e.g. entre LRU y LFD).
      
      * DEFINICIÓN: 

        En el caso aleatorizado, se puede considerar mas tipos de
        adversarios, cada uno definiendo una medida de dificultad y
        un modelo de complejidad.

        1. Adversario "Oblivious" ("Oblivious Adversary")

           El adversario conoce $A$ pero no $R$: el elija su instancia
           completamente al inicial, antes de la ejecución online del
           algoritmo.

        2. Adversario Offline adaptativo

           Para este definición, es mas fácil de pensar a un
           adversario como un agente distinto del algoritmo offline
           con quien se compara el algoritmo online.

           El adversario conoce $A$ en total, pero $R$ online, y le
           utiliza para generar una instancia peor $I$. Este instancia
           $I$ es utilizada de nuevo para ejecutar el algoritmo
           offline (quien conoce el futuro) y producir las
           complejidades (a cada instante online) con cual comparar la
           complejidad del algoritmo online.

        3. Adversario Online adaptativo

           En este definición, el algoritmo conoce $A$ en total,
           construir la instancia $I$ online como en el caso
           precedente, pero tiene de tiene de resolverla online también
           (de una manera, no se ve en el futuro).

      * Comparación de los Tipos de adversarios.

        - Por las definiciones, es claro que 
          - el adversario offline adaptativo 
            - es mas poderoso que
          - el adversario online adaptativo
            - es mas poderoso que
          - el adversario oblivious

      * Competitiva Ratios

        * Para un algoritmo online $A$
          - Para cada tipo de adversario se define un competitivo ratio:
            - C_A^{obl}: competitivo ratio con adversario oblivious
            - C_A^{aon}: competitivo ratio con adversario adaptativo online
            - C_A^{aof}: competitivo ratio con adversario adaptativo offline

          - Es obvio que, para $A$ fijada, considerando un adversario
            mas poderoso va aumentar el competitivo ratio:
            C_A^{obl} \leq C_A^{aon} \leq C_A^{aof}

        * Para un problema
          - el competitivo ratio de un problema es el competitivo
            ratio mínima sobre todos los algoritmos correctos para
            este problema.
            C^{obl} \leq C^{aon} \leq C^{aof} \leq C^{det}
          - donde  C^{det} es el competitivo ratio de un algoritmo
            online deterministico.

5.6 Hashing 
============
   REFERENCIA: Capítulos 8.4 y 8.5 en "Randomized Algorithms", de
   Rajeev Motwani and Prabhakar Raghavan, p. 213 y Capitulo 4 de
   "Algorithms and Data Structures, The Basic Toolbox", de Kurt
   Lehlhorn y Peter Sanders.

   
   1) Hash table: Que se recuerdan de CC30A?

      * Dado 
        - un universo de tamaño M
        - una tabla de tamaño N (con listas de colisiones)
        - una función de hashing de [0,M-1] a [0,N-1]

      * Queremos
        - insertar
        - buscar
        - borrar

      * Como manejar colisiones?
        + "Closed Hashing"
          - cada celda de la tabla tiene una lista de elementos
          - cuando insertando en un celda que ya tiene un elemento,
            agrega el nuevo elemento a la lista (en frente
            usualmente: saben porque?).
          - se llama cerrada ("closed") porque las celdas no
            comunican entre ellas.
        + "Open hashing"
          - cuando insertando en un celda que ya tiene un elemento,
            busque una otra celda libre en la tabla (de manera
            deterministica)
          - cuando borrando un elemento, no libera el espacio (le
            necesitamos para buscar elementos), solamente marcalo para
            limpiar lo de manera amortizada mas tarde.


   2) Hashing Extendible
      Que paso si la tabla es tan grande que no se queda en memoria?

      * Características
        - garantizar un acceso en búsqueda
        - necesita cero espacio en RAM
        - no hay control sobre la utilización del espacio en disco
        - no garantías sobre el percentage de cada pagina utilizado
          en el peor caso, pero en promedio si.

      * Concepto: Simular un B-arbol de altura 2.
        - En cada nodo, maneja los datos con hash
        - guarda un directorio a la raíz
        - utiliza solamente una parte de la valor de hashing para
          elegir el sub-arbol.
        - Maneja el directorio de manera dinámica, extendiéndolo cuando mas
          datos son agregados.

      * Descripción:
        - B: cantidad de objetos en una pagina
        - h: función de hashing de las llaves a [0,2^k-1], para alguno k>0
        - D: la profundidad global, D\leq k
          + la raíz tiene 2^D punteros a las hojas
          + la raíz es indexada por los D primero bits de la valor de hash.
        - d_l: la profundidad local de cada pagina hoja l
          + valor de hash in $l$ van a tener $d_l$ bits in común
          + hay 2^{D-d_l} punteros a la hoja $l$
          + NOTA: siempre d_l \leq D

      * Ejemplo: 
        - B=4, k=6, D=2
        - Directorio:
            00   pagina A  
            01   pagina B  
            10   pagina C  
            11   pagina C  
        - Hojas:
          + Pagina A: d_l=2  (i.e. 2 bits de peso mas signicativos en común)
              000100  
              001000  
              001011  
              001100  
          + Pagina B: d_l=2 (i.e. 2 bits de peso mas signicativos en común)
              010101  
              011100  
                      
                      
          + Pagina C: d_l=1 (i.e. 1 bit de peso mas signicativo en común)
              100100  
              101101  
              110001  
              111100  
      * Operadores:

        - Búsqueda
          + Algoritmo
            - calcular h(x)
            - calcular la hoja (aplicando una mascara)
            - traer la hoja de la memoria externa
            - buscar secuencialmente en la pagina.
          + Ejemplos/Ejercicios: 
            - buscar x tal que h(x)=110001
            - buscar x tal que h(x)=001111

        - Inserción
          + Algoritmo
            - buscar(x), en la hoja H
            - while no hay espacio en H 
              - si d_l=D
                - incrementa D
                - multiplica por dos el tamaño del directorio,
                  duplicando la mejor parte de los punteros;
                - cambia H a la nueva hoja donde se quedaría x, por h(x).
              - si d_l<D 
                - incrementa d_l
                - separa la pagina en dos, clasificando las llaves con
                  el d_l-simo bit de su valor de hashing.
                - cambia H a la nueva hoja donde se quedaría x, por h(x).
            - agrega x en H
          + Ejemplos/Ejercicios:
            - Considera una estructura de hashing vacila, 
            - con B=2 y k=5
            - inserta(x) tq h(x)=01001
            - inserta(x) tq h(x)=00001
            - inserta(x) tq h(x)=01110
            - inserta(x) tq h(x)=11100
            - inserta(x) tq h(x)=10110
            - inserta(x) tq h(x)=01010
            - inserta(x) tq h(x)=11101
            - inserta(x) tq h(x)=01011

        - Remover
          + Algoritmo
            - buscar(x), en la hoja H
            - remover x de H
            - Recursivamente
              - intentar de fusionar H con hora "pareja"
                - que es al mismo nivel d_k que H, y 
                - cual puntero tiene d_l-1 bits de peso mas
                  significativo en común con H.
              - intentar de reducir D si cada hoja tiene mas que un
                puntero.

      * Análisis

        - El directorio siempre es en memoria primaria.
        - Cada búsqueda corresponde exactamente a 1 acceso al disco

        - La complejidad en el peor caso de insertar y remover no es
          tan buena:
          - en el mejor caso, es 1 acceso al disco,
          - pero si la hoja tiene de ser divida o fusionada, se puede
            propagar recursivamente.

        - En promedio y en practica, la complejidad es buena:
          - n / (B\ln 2) \approx 1.44 n/B paginas para n llaves.
          - En practica, cada pagina es 69% llena.


   3) Hashing Lineal    [Litwin, 1980]

      * Características 
        - no garantiza una cantidad de acceso en peor caso
        - no necesita espacio en RAM
        - puede controlar una de las dos cosas siguiente:
          - el espacio utilizado, 
          - o
          - la cantidad promedio de accesos.

      * Mecanismo
        - Dado 
          - un nivel $k$
          - una función de hash $h$ -> [0,M-1], donde M=2^l
        - la dirección de un elemento es 
          - si h(x) mod M < k 
            - cell <- h(x) mod 2M
          - sino 
            - cell <- h(x) mod M
        - Cuando la tabla es (cerca de ser) llena, 
          - incrementa $k$
          - recalcula el hash de cada elemento


      * Variantes, basadas en la definición de "cerca de ser llena"
        - si llenado > 90%, expand()
          - => da bueno espacio utilizado
        - si #promedio de tiempo de acceso > 1+\alpha, expand()
          - => da bueno tiempo promedio 



   4) Elegir $h$ en forma aleatoria

      1. DEFINICIÓN  [Motwani p.216]

          Una familia $H$ de funciones de hashing de  [0,M-1] a [0,N-1]
          se dice *2-universal* si

         $\forall x\neq y \in [0,M-1]  

         \Pr_{h\in H} (h(x)=h(y)) \leq 1/N$

         Idea: Elegir $h$ al azar a dentro de $H$.

      2. DEFINICIÓN

         Dado $h$, definimos $\forall x,y   

         C_{x,y} = 1 si h(x) = h(y)
                   0 sino$

      3. Observación:
         si $h$ es elegido al azar de $H$,

           E(C_{x,y}) = Pr( C_{x,y}=1) \leq 1/N si x\neq y
                                         =  1   sino

      4. DEFINICIÓN

         Dado $h$, definimos    $\forall x

         C_{x,S} = |{y\in S, C_{x,y}=1 }| = \sum_{y\in S}  C_{x,y}$

         $C_{x,S}$ es una cota superior al costo de insertar, buscar y
         borrar $x$ en $S$.

      5. Resultado:

         Si $h$ es universal y $x\in S$

         $E(C_{x,S}) = \sum_{y\in S} E(C_{x,y}) < 1 + |S|/N$

      6. Corolario

         Si elijo $|S|=\Theta(N)$ y un $h$ al azar de una familia
         universal $H$, entonces el costo esperado de todas las
         operaciones es O(1), pero no tenemos garantías de complejidad
         en el peor caso.

      7. Ejemplo de familia Universal

         Consideramos

         H_{P,N} = { h_{a,b}, 1\leq a < p, 0 \leq b < p}

         donde 

         h_{a,b}(x) = ( ( (ax+b) mod p ) mod N )

         p \geq N primo


         - Nota: en practica, es suficiente de tener p primo con grande
           probabilidad.

         - Lema 

           H_{P,N} es universal

         - Prueba

           Queremos mostrar que

           $\forall x\neq y \in [0,M-1]
           \Pr_{h\in H} (h(x)=h(y)) \leq 1/N

           Sean $r\neq s$, $0\leq r,s<p$ 
           (nota que $r\neq s \mod p$)
           Calculemos \Pr (h(x)=h(y)) dados $x\neq y$:
           - \Pr( ax+b \mod p & ay+b=s mod p)
           - si ax+b = r mod p   y ay+b = s mod p,
           - a(x-y) = r-s mod p
           - $a = \frac{r-s}{x-y} mod p$  y $b=r-ax mod p$

           hay solamente un par (a,b) tq $x$ y $y$ son en colisión.

           - Pr(a,b elegidos) = 1/p(p-1)  si $x\neq y$
                                0         si $x=y$


           Cuanta valores de $r$ y $s$ hacen que $h(x)=h(y)$?
           $h(x)=h(y)  \Leftrightarrow  r=s \mod p$

           0                  r                             p
            __________________ _____________________________ 
           <-N-><-N-><-N-><-N-><-N-><-N-><-N-><-N-><-N-><-N->


           Para cada $r$ hay menos que $\lceil p/N\rceil-1$ valores de
           $s\neq r$ ta $r=s \mod N$.

           Entonces, hay $p (\lceil p/N\rceil-1) < p(p-1)/N$ pares
           $(r,s)$ que producen colisiones.

           $\Pr(h(x)=h(y))< p(p-1)/N 1/p(p-1) = 1/N$  \qed

   5) Hashing Perfecto

      1. DEFINICIÓN  (Motwani p.216)

         Una función de hashing h:[0,M-1]->[0,N-1] es *perfecta* para
         $S\subset M$ si no genera colisiones para las llaves en $S$.

      2. Objetivo

         Si se conocen todas las claves de entrada, 
         se puede tener tiempo $O(1)$ *en el peor caso*,
         garantizando que no hay colisiones.

         Pregunta: tal función de hash se puede buscar?
         - deterministicamente (O(N^2)?)
         - al azar (algoritmo de Las Vegas?)

      3. Caso fácil:  espacio $N^2=|S|^2$.

         - Estoy dispuesto a pagar espacio $N^2=|S|^2$.
         - Supongo elija $h\in H$ universal al azar.
         - Cual es la probabilidad que $h$ sea perfecta para $S$?

           $\forall x,y, \Pr( h(x)=h(y) )  \leq 1 / N^2$

           $\Pr( \exist x\neq y \in S, h(x)=h(y)) 
           \leq 
           N(N-1) / 2 \times 1/N^2
           <
           1/2$

         - Un algoritmo de tipo Las Vegas necesita en promedio $O(1)$
           iteraciones.

         - costo esperado $O(|S|)$ 
         - (tecnical a mostrar, pero fácil de mostrar $O(|S|^2)$)

      4. Caso difícil: espacio O(N)=O(|S|)

         1. elijo una $h\in H$ que mapeo a $[0,N-1]$

            - esta $h$ distribuye los |S| elementos en las $N$ celdas
              de alguna manera.

            - llamamos $B_i$ ala cantidad de elementos que caen en la
              celda $i$. Si $\sum B_i^2 > 4n$, vuelvo al paso $1$.

         2. Construyo tablas para cada celda, con funciones
            $h_0,\ldots,h_{N-1}$, usando el "caso fácil"

            Eso ocupa espacio $5n$, con tiempo de construcción de la
            sub tablas $O(n)$.

            Cual es la probabilidad de realizar $\sum B_i^2 < 4n$?

            $\sum B_i^2 = \sum_{x,y} C_{x,y}

            porque B_i^2 = |{(i,j), i,j\in B_i}|
                         = |{(i,j), C_{i,j}=1}|

            entonces 
            E(\sum B_i^2) 
            = E(\sum_{x,y} C_{x,y} )
            = E(\sum_{x=y} C_{x,y}) + E(\sum_{x\neq y} C_{x,y}) 
            =            N          +       N(N-1)/ N
            <2N

            Si el promedio es < 2N
            al menos la mitad de los $h$ tiene $\sum B_i^2<4N$
            Entonces con probabilidad 1/2, E(T)=O(1).

            Nota: es verdad que la mitad son <4N, pero no que la mitad
            son <2N

      5. Conclusión:
         
         Es costoso, pero cuando conocemos las llaves utilizadas,
         podemos calcular la función de hash perfecta para este
         conjunto.

5.7 Algoritmos Aproximados 
===========================





   1) Aproximación de problemas de optimización NP-dificiles

      - Que problemas NP dificiles conocen?
        - colorisacion de grafos
        - ciclo hamiltonian
        - Recubrimiento de Vertices (Vertex Cover)
        - Bin Packing
        - Problema de la Muchila
        - Vendedor viajero (Traveling Salesman)

      - Que hacer cuando se necessita una solucion en tiempo
        polinomial?

        - Consideramos los problemas NP completos de decisión,
          generalmente de optimización.  Si se necesita una solucionó
          en tiempo polinomial, se puede considerar una aproximación.

   2) DEFINICIÓN

      * Dado un problema de minimización, un algoritmo $A$ es un
        *$p(n)$-aproximación* si
        $\forall n \max \frac{ C_A(x) }{ C_{OPT}(x) } \leq p(n)$

      * Dado un problema de maximización, un algoritmo $A$ es un
        *$p(n)$-aproximación* si
        $\forall n \max \frac{ C_{OPT}(x) }{ C_A(x) } \leq p(n)$

       - Notas: 
         1) Aquí consideramos la cualidad de la solución, NO la
            complejidad del algoritmo. Usualmente el problema es
            NP-difícil, y el algoritmo de aproximación es de
            complejidad polinomial.
         2) Las razones estas \geq 1 (elijamos las definiciones para
            eso)
         3) A veces consideramos también C_A(x)-C_{OPT}(x)
            (minimización) y C_{OPT}(x)-C_A(x): eso se llama un
            "esquema de aproximación polinomial" y vamos a verlo mas
            tarde.

   3) Ejemplo: Bin Packing (un problema que es 2-aproximable)

      - DEFINICIÓN

         Dado $n$ valores $x_1, \ldots, x_n$, $0\leq x_i\leq 1/2$,
         cual es la menor cantidad de cajas de tamaño 1 necesarias
         para empaquetarlas?

      - Algoritmo "Greedy"
        - Considerando los $x_i$ en orden,
        - llenar la caja actual todo lo posible,
        - pasar ala siguiente caja.

      - Análisis
        - Este algoritmo tiene complejidad lineal O(n).
        - Greedy da una 2-aproximación.
        - (se puede mostrar facilamente en las instancias donde el
          algoritmo óptima llene completamente todas las cajas.)

      - Además, hay mejores aproximaciones, 
        1. Best-fit tiene performance ratio de 1.7 en el peor caso
        2. [Best-Fit Bin-Packing with Random Order (1997), Kenyon]

           Best-fit is the best known algorithm for on-line
           binpacking, in the sense that no algorithm is known to
           behave better both in the worst case (when Best-fit has
           performance ratio 1.7) and in the average uniform case,
           with items drawn uniformly in the interval [0; 1] (then
           Best-fit has expected wasted space O(n 1=2 (log n) 3=4
           )). In practical applications, Best-fit appears to perform
           within a few percent of optimal. In this paper, in the
           spirit of previous work in computational geometry, we study
           the expected performance ratio, taking the worst-case
           multiset of items L, and assuming that the elements of L
           are inserted in random order, with all permutations equally
           likely. We show a lower bound of 1:08 : : : and an upper
           bound of 1:5 on the random order performance ratio of
           Best-fit. The upper bound contrasts with the result that in
           the worst case, any (deterministic or randomized) on-line
           bin-packing algorithm has performance ratio at least
           1:54 : : :. 1

        3. Un otro paper donde particionan los $x_i$ en tres classes,
           placeando los x_i mas grande primero, buscando el
           placamiento optimal de los $x_i$ promedio, y usando un
           algoritmo greedy para los $x_i$ pequenos. [Karpinski?]

        4. la distribucion de los tamaños de las cajas hacen la
           instancia dificil o facil.


   4) Ejemplo: Recubrimiento de Vertices (Vertex Cover)

      - DEFINICIÓN:

        Dado un grafo $G=(V,E)$, cual es el menor $V'\subseteq V$ tal
        que $V'$ sea un vertex cover de $G$.
        (o sea $V'$ mas pequeño tq $\forall e=(u,v)\in E, u\in V'o v\in V')

      - Algoritmo de aproximación:

        - V'<- \emptyset
        - while E\neq \emptyset
          - sea (u,v) \in E
          - V'\leftarrow V'\cup {u,v}
          - E <- E \ {(x,y), x=u o x=v o y=u o y=v }
        - return V

      - Discusión: es una 2-aproximación o no?

      - LEMA: El algoritmo es una 2-aproximación.

        - PRUEBA:
          + Cada par $u,v$ que la aproximación elige esta conectada,
            entonces u o v estas en cualquier solucionó óptima de
            Vertex Cover.
          + Como se eliminan las aristas incidentes en u y v, los
            siguientes pares que se eligen no tienen intersección con
            el actual, entonces cada 2 nodos que el algoritmo elige,
            uno pertenece a la solución óptima.
          + quod erat demonstrandum, (QED).



   5) Ejemplo: Vendedor viajero (Traveling Salesman)

      - DEFINICIÓN:

        Dado $G=(V,E)$ dirigido y $C:E\rightarrow R^+$ una función de
        costos, encontrar un recorrido que toque cada ciudad una vez, y
        minimice la suma de los costos de las aristas recorridas.

      - LEMA:   Si $c$ satisface la desigualdad triangular
        $\forall x,y,z   c(x,y)+x(y,z) \geq c(x,z)$,
        hay una 2-aproximación.

        - PROOF: 
          - Algoritmo de Aproximación (con desigualdad triangular)
            - construir un arbol cobertor mínimo (MST)
              - se puede hacer en tiempo polinomial, con programación lineal.
              - C_{MST} \leq C_{OPT}
            - producimos un recorrido en profundidad DFS del MST:
              - C_{DFS} = 2C_{MST} \leq 2 C_{OPT}
              - (factor dos porque el camino de vuelta puede ser al
                máximo de tamaño igual al tamaño del camino de ida)
            - eliminamos los nodos repetidos del camino, que no crece
              el costo
              - $C_A\leq 2 C_{OPT}$
          - quod erat demonstradndum, QED.

      - LEMA: Si $c$ no satisface la desigualdad triangular, el
        problema de vendedor viajero no es aproximables en tiempo
        polinomial (a menos que P=NP$).

        - PROOF:   
          - Supongamos que existe una p(n)-aproximación de tiempo
            polinomial.
          - Dado un grafo G=(V,E)
          - Construimos un grafo G'=(V,E') tal que 
            - E'=V^2 (grafo completo), y
            - c(u,v) =  1 si (u,v)\in E
                        n p(n) sino
          - Si no hay un Ciclo Hamiltonian en E,
            - todas las soluciones usan al menos una arista que no es
              en E
            - entonces todas las soluciones tienen costo mas que
              $np(n)$
          - Si hay un Ciclo Hamiltonian en E
            - tenemos una aproximación de una solucionó de costo menos
              que $(n-1)p(n)$                 QED

   6) Ejemplo: Vertex Cover con pesos


      - DEFINICIÓN

        Dado G=(V,E) y c:V->R^+, se quiere un V^*\subseteq V que cubra
        E y que minimice \sum_{v\in V^*} c(v).

      - Este problema es NP Completo.

      - LEMA: Vertex Cover con pesos es 2-aproximable
      - PROOF:
        1. Sea variables x(v)\in {0,1}, \forall v\in V
            - el costo de V^* sera \sum x(v) c(v) 
            - objetivo: \min \sum_{v\in V} x(v) c(v)
              donde 0\leq x(v) \leq 1 \forall v\in V
            - x(u)+x(v)\geq 1 \forall u,v \in E
        2. x(v) \in Z seré programación entera, que es NP Completa
           x(v) \in R seré programación lineal, que es polinomial
           el valor \sum x(v) c(v) que produce el programo lineal
           es inferior a la mejor solución al problema de VC.
        3. Algoritmo
           - resolver el problema de programación lineal
             - nos da x(v_1),\ldots x(v_n)
           - V^* < \emptyset
           - for v\in V
             - si x(v)\geq 1/2
               - V^* <- V^* \cup {v}
           - return V^*
        4. Propiedades
           - V^* es un vertex cover:
             - si \forall(u,v)\in E, x(u)+x(v)\geq 1
             - entonces, x(u)\geq 1/2 o x(v)/geq 1/2
             - entonces, u\in V^* o v\in V^*
           - V^* es una 2-aproximación:
             - c(V^*) = \sum_v y(v) c(v)
               donde y(v) = 1 si x(v) \geq 1/2
                            0 sino
             - entonces y(v) \leq 2 x(v)
             - \sum y(v) c(v) \leq \sum 2 x(v) c(v) \leq 2 OPT
             - C(V^*)         \leq 2 OPT
        5. QED


   7) Definiciones

      * *PTAS*

        Un *esquema de aproximación polinomial* para un problema es un
        algoritmo $A$ que recibe como input una instancia del problema
        y un para-metro $\varepsilon>0$ y produce una $(1+\varepsilon)$
        aproximación. Para todo $\varepsilon$ fijo, el tiempo de $A$
        debe ser polinomial en $n$, el tamaño de la instancia.

        Ejemplos de complejidades:
        - O(n^{2/3})
        - O(\frac{1}{\varepsilon^2} n^2)
        - O(2^\varepsilon n)

      * *FPTAS*

        Un *esquema de aproximación completamente polinomial* es un
        PTAS donde el tiempo del algoritmo es polinomial en $n$ *y en
        1/\varepsilon*.

   8) Problema de la mochila


      * Definición:
        
        Dado 
        - los elementos S={1,\ldots,n} con pesos X_1,\ldots,X_N\geq 0,
        - un tamaño máximo t
        Quiero encontrar $S'\subset S$ 
        - tal que sum(S')\leq t y
        - que maximice sum(S')=\sum_{i\in S'} x_i

      * Solucion exacta, exponencial
        - Dado L={y_1,,\ldots y_m}, definimos L+x={y_1+x,\ldots,y_m+x}
        - Algoritmo:
          - L <- {\emptyset}
          - for i\leftarrow 1 to n
            - L \leftarrow merge (L,L+x_i)
            - prune(L,t)      (remudando las valores > t)
          - return max(L)

      * Solucion aproximada (inspirada del algoritmo exacto)

        - Definimos la operación de *recorte* de una lista L con
          parámetro $\delta$:
          - Dado $y,z \in L$, $z$ *represente* a $y$ si
            - $y /(1+\delta)   \leq   z   \leq y$ 
        - vamos a eliminar de $L$ todos los $y$ que sean representados
          por alguno $z$ no eliminado de $L$.

        - Recortar(L,\delta)
          - Sea L={y_1,\ldots,y_m}
          - L'\leftarrow {y_1}
          - Last \leftarrow y_1
          - for i\leftarrow 2 to m
            - si y_i > Last(1+\delta)
              - L \leftarrow L'.{y_i}
              - Last \leftarrow y_i
          - return L'

        - Algoritmo de Aproximación:
          - L <- {\emptyset}
          - for i\leftarrow 1 to n
            - L \leftarrow merge (L,L+x_i)
            - prune(L,t)      (remudando las valores > t)
            - *L <- recortar(L,\epsilon/2n)*
          - return max(L)

        - El resultado es una $(1+\epsilon)$-aproximación:
          1. retorne una solución valida, tal que 
             - $\sum(S')\leq t$ para algún $S'\subset S$
          2. en el paso $i$, para todo $z\in L_{OPT}$,
             existe un $y\in L_A$ tal que $z$ representa a $y$.
             - Luego de los $n$ pasos, el $z^*$ óptimo en $L_{OPT}$
               tiene un representante $y^*\in L_A$ tal que 
               $z^*/(1+\epsilon/2n)^n \leq y^* \leq z^*
          3. Para mostrar que el algoritmo es una (1+\epsilon)
             aproximación, 
             - hay que mostrar que
               - $z^*/(1+\epsilon) \leq y^*$
             - entonces, debemos mostrar que 
               - $(1+\epsilon/2n)^n \leq 1+\epsilon$
             - Eso se muestra con puro calculo:
               - $(1+\epsilon/2n)^n \leq? 1+\epsilon$
               - e^{n\lg(1+\epsilon/2n) }
               - \leq e^{ n \epsilon /2n }
               - = e^{ \epsilon/2 }
               - \leq? e^{\ln(1+\epsilon)}
               - eso es equivalente a elegir \epsilon tal
                 que \epsilon/2 \leq \ln(1+\epsilon)
               - i.e. cualquier tal que $0<\epsilon\leq 1$
          4. El algoritmo es polinomial (en todos los parámetros)
             - despues de recortar dedos $y_i,y_{i+1}\in L$
               se cumple $y_{i+1}>y_i(1+\delta)$
               y el ultimo elemento es \leq t
             - entonces, la lista contiene 0,1
               y luego a lo mas \lfloor \log_{(1+\delta)} t \rfloor
             - entonces el largo de L en cada iteracion no supera
               $2+ \frac{ \log t }{ \log( 1+\varepsilon/2n) }
             - Nota que \ln(1+x) 
               - = - \ln (1/(1+x))
               - = - \ln ( (1+x-x)/(1+x) )
               - = - \ln ( 1 - x/(1+x) )
               - = - \ln (1+y) \geq -y
               - \geq - (-x/(1+x)) = x/(1+x)
             - Entonces
               - 2+ \frac{ \ln t }{ \ln 1 + \epsilon/2n }
               - \leq 2 + ((1+\epsilon/2n) 2n \ln t )/\epsilon
               - = ( 2n\ln t )/ \epsilon + \ln t + 2
               - = O(n \lg t /\epsilon)
             - Entonces cada iteracion toma O(n \lg t /\epsilon) operaciones
             - Las $n$ iteraciones en total toman 
               - O(n^2 \lg t /\epsilon) operaciones


                 

5.8 Algoritmos paralelos 
=========================

   REFERENCIA: Chap 12 of "Introduction to Algorithms, A Creative Approach",
   Udi Manber, p. 375

   1) Modelos de paralelismo
      * Instrucciones
        - SIMD: Single Instrucción, Múltiple Data
        - MIMD:  Múltiple Instrucción, Múltiple Data
      * Memoria
        - compartida
        - distribuida

      * 2*2 combinaciones posibles:

                   Memoria compartida   Memoria distribuida                                 
           ------+--------------------+----------------------------------------------------
            SIMD   PRAM                 redes de interconexión (weak computer units)        
                                        (hipercubos, meshes, etc...)                        
           ------+--------------------+----------------------------------------------------
            MIMD   Threads              procesamiento distribuido (strong computer units),  
                                        Bulk Synchronous Process, etc...                    

      * En este curso consideramos en particular el modelo PRAM

   2) Modelo PRAM
     * Mucha unidad de CPU, una sola memoria RAM

       cada procesador tiene un identificador único, y puede utilizarlo
       en el programa

     * Ejemplo:
       
       + if p mod 2 = 0 then
         + A[p] += A[p-1]
       + else
         + A[p] += A[p+1]
       + b <- A[p];
       + A[p]<- b;

     * Problema: el resultado no es bien definido, puede tener
       *conflictos* si los procesadores están asinchronos. Las soluciones a
       este problemas dan varios submodelos del modelo PRAM:

       1. EREW Exclusive Read. Exclusive Write

       2. CREW Concurrent Read, Exclusive Write

       3. CRCW Concurrent Read, Concurent Write
          En este caso hay variantes también:
          - todos deben escribir lo mismo
          - arbitrario resultado
          - priorizado
          - alguna f() de lo que se escribe


   3) Como medir el "trade-off" entre recursos (cantidad de
      procesadores) y tiempo?

      * DEFINICIÓN:

        - $T^*(n)$ es el *Tiempo secuencial* del mejor algoritmo no
          paralelo en una entrada de tamaño $n$ (i.e. usando $1$
          procesador).

        - $T_A(n,p)$ es el *Tiempo paralelo* del algoritmo paralelo
          $A$ en una entrada de tamaño $n$ usando $p$ procesadores.

        - El *Speedup* del algoritmo $A$ es definido por

          $S_A(n,p) = \frac{ T^*(n) }{ T_A(n,p) } \leq p$ 

          Un algoritmo es mas efectivo cuando $S(p)=p$, que se llama
          *speedup perfecto*.

        - La *Eficiencia* del algoritmo $A$ es definida por

          $E_A(n,p) = \frac{ S_A(n,p) }{ p }=\frac{T^*(n)}{pT_A(n,p)}$ 

          El caso óptima es cuando $E_A(n,p)=1$, cuando el algoritmo
          paralelo hace la misma cantidad de trabajo que el algoritmo
          secuencial. El objetivo es de *maximizar la eficiencia*.

          (Nota estas definiciones en la pisara, vamos a usarlas después.)

   4) PROBLEMA: Calcular Max(A[1,...,N])

      1. Solucion Secuencial

         * Algoritmo:
           - $m \leftarrow 1$
           - for $i\leftarrow 2$ to $n$
             - if $A[i]>A[m]$ then $m\leftarrow i$
           - return $A[m]$

         * Se puede ver como un arbol de evaluación con una sola rama
           de largo $n$ y $n$ hojas.

         * Complejidad: 
           - tiempo $O(n)$, con $1$ procesador, entonces:
           - $T^*(n)=n$.

      2. Solucion Parallela con $n$ procesadores

         * Algoritmo:
           - $M[p] \leftarrow A[p]$
           - for $l\leftarrow 0$ to $\lceil \lg p \rceil -1$
             - if $p \mod 2^{l+1}=0$ y $ p+2^l<n$
               - $M[p]\leftarrow \max( M[p],M[p+2^l])$
           - if $p=0$
             - $max \leftarrow M[4]$

         * Se puede ver como un arbol balanceado de altura $\lg n$ con
           $n$ hojas.

         * Complejidad: 

           - tiempo $O(\lg n)$ con $n$ procesador, i.e. en nuestra notaciones:
           - $T(n,n) = \lg n$
           - $S(n,n) = \frac{n}{\lg n} $
           - $E(n,n) = \frac{n}{n\lg n} = {1\over\lg n} $

         * Nota: no se puede hacer mas rápido, pero hay mucho
           procesadores poco usados: quizás se puede calcular el max
           en el mismo tiempo, pero usando menos procesadores?

      3. Solucion general con $p$ procesadores

         * Idea:

           - reduce la cantidad de procesadores, y hace "load balancing"
           - utiliza solamente $n/\lg n$ procesadores.
           - Divida el input en $n/\lg n$ grupos, 
           - asigna cada grupo de $\lg n$ elementos a un procesador.
           - En la primera fase, cada procesador encontra el max de su grupo
           - En la segunda fase, utiliza el algoritmo precedente.

         * Complejidad: 

           - tiempo $O(\lg n)$ con $n$ procesador, i.e. en nuestra notaciones:
           - $T(n, {n\over\lg n} ) = 2 \lg n \in O(\lg n)$
             - $T(n,p)= {n\over p} + \lg p$
           - $S(n,p) = \frac{n}{{n\over p} + \lg p} 
             = p ( 1 - \frac {p\lg p}{n+p\lg p})
             \rightarrow p$ si $n\rightarrow \infty$ y p
           - $E(n,p) = \frac{n}{{n\over\lg n}\lg n} = 1/2 $

         * Discusión:
           
           - El parámetro de $\lg n$ procesadores es óptima?

             - Para que? Que significa ser óptima? 
               - en energía
               - en el contexto donde los procesadores libres pueden
                 ser usados para otras tareas.
             - Si, es óptimo para la eficiencia, se puede ver
               estudiando el grafo en función de $p$.

           - Eso es un algoritmo EREW, CREW, o CRCW?

             - EREW (Exclusive Read. Exclusive Write): no dos
               procesadores lean o escriben en la misma cédula al
               mismo tiempo.

           - Nota: 

             - Hay un algoritmo CRCW que puede calcular el max en O(1)
               tiempo en paralelo, ilustrando el poder del modelo
               CRCW (y el costo de las restricciones del modelo EREW)
               [ REFERENCIA: Section 12.3.2 of "Introduction to Algorithms, A
               Creative Approach", Udi Manber, p. 382]]

             




   5) LEMA de Brent

      Si un algoritmo consigue un tiempo T(n,p)=C, 
      entonces consigue tiempo T(n,p/s)= sC \forall s>1
      (bajo algunas condiciones)


   6) DEFINICIÓN "Trabajo"

       - Usando el Lema de Brent, podemos exprimir el rendimiento de
         los algoritmos paralelos con dos medidas:
        
         - $T(n)$, el tiempo del algoritmo paralelo usando cualquier
           cantidad de procesadores que quiere. 

           Nota la diferencia con 
           + $T^*(n)$, el tiempo del mejor algoritmo secuencial, y 
           + $T_A(n,n)$, el tiempo del algoritmo con $n$ procesadores.

         - $W(n)$, la suma del total trabajo ejecutado por todo los
           procesadores.

   7) COROLARIO 

        - Con el lema de Brent podemos obtener:

          - $$T(n,p) = T(n) + \frac{ W(n) }{ p }$$

          - $$E(n,p) = \frac{ T^*(n) }{ pT(n) + W(n) }$$

   8) EJEMPLO

      * Para el calculo del máximo:
        - $T(n) = \lg n$
        - $W(n) = n$

      * Entonces
        - se puede obtener 
          - $T_B(n,p) = \lg n + {n \over p}$
          - $$E(n,p) = \frac{ n }{ p\lg n + n }$$

      * (Nota que eso es solamente una cota superior, nuestro
        algoritmo da un mejor tiempo.)




   9) PROBLEMA: Prefijos en paralelo ("Parallel Prefix")

      * DEFINICIÓN: Problema "Prefijo en Paralelo"

        Dado $x_1,\ldots, x_n$ y un operador asociativo \times
        calcular 
        - $y_1 = x_1$
        - $y_2 = x_1\times x_2$
        - $y_2 = x_1\times x_2 \times x_3$
        - ...
        - $y_n = x_1\times \ldots \times x_n$

      * Solucion Secuencial

        Hay una solucionó obvio en tiempo O(n).

      * Solucion paralela 1

        * Concepto:

          - Hipótesis: sabemos solucionarlo con $n/2$ elementos
          - Caso de base: $n=1$ es simple.
          - Inducción:
            1. recursivamente calculamos en paralelo:
               - todos los prefijos de $\{x_1,\ldots,x_{n/2}\}$ con
                 $n/2$ procesadores.
               - todos los prefijos de $\{x_{n/2},\ldots,x_n\}$ con
                 $n/2$ procesadores.
            2. en paralelo agregamos $x_{n/2}$ a los prefijos de
               $\{x_{n/2},\ldots,x_n\}$


        * ParallelPrefix1(i,j)
          - if $i_p=j_p$
            - return x_{i_p}
          - $m_p \leftarrow \lfloor \frac{i_p + j_p}{2} \rfloor$;
          - if $p\leq m$ then
            - algo( i_p, m_p )
          - else
            - algo(m+1, j_p)
            - y_p \leftarrow y_m . y_p

        * Otra forma del algoritmo (de p. 384 de "Introduction to
          Algorithms, A Creative Approach", Udi Manber):

           + ParallelPrefix1(left,right)
             - if (right-left) = 1
               - x[right] <- x[left] . x[right]
             - else
               - middle <- (left+right-1)/2
               - do in paralel
                 - ParallelPrefix1(left,middle) {assigned to {P_1 to P_{n/2}}}
                 - ParallelPrefix1(middle+1,right) {assigned to {P_{n/2+1} to P_n}}
               - for i <- middle+1 to right do in paralel
                 - x[i] <- x[middle] . x[i]


        * Notas:

          - este solucion *no* es EREW (Exclusive Read&Write), porque
            los procesadores pueden leer $y_m$ al mismo tiempo.
          - este solucionó es CREW (Concurrent Read, Exclusive Write).

         - Complejidad: 

           - $T_{A_1}(n,n) = 1+ T(n/2,n/2) = \lg n$

             (El mejor tiempo en paralelo con cualquier cantidad de
             procesadores.)

           - $W_{A_1}(n) = n + 2 W_{A_1}(n/2) - n\lg n$

           - $T_{A_1}(n,p) = T(n) + W_{A_1}(n)/p = \lg n + (n\lg n)/p$

           - Calculamos $p^*$, la cantidad óptima de procesadores
             para minimizar el tiempo: 
             - $T(n) = W_{A_1}(n) /p^*$
             - $p^* = \frac{ W(n) }{ T(n) } = n$

           - Calculamos la eficiencia

             - $E_{A_1}(n,p^*) 
               = \frac{ T^*(n) }{ p^* T(n)+W(n) }
               = \frac{ n }{ n\lg n } 
               = {1 \over \lg n}$

             - es poco eficiente =(

             - Podríamos tener un algoritmo con 
               - la eficiencia del algoritmo secuencial 
               - el tiempo del algoritmo paralelo?


      * Solucion paralela 2: mismo tiempo, mejor eficiencia

        - Idea:
          
          El concepto es de dividir de manera diferente: par y impar
          (en vez de largo o pequeño).

        - Concepto:
          1. Calcular en paralelo $x_{2i-1}.x_{2i}$ en $x_{2i}$
             para $\forall i, 1\leq i \leq n/2$.
          2. Recursivamente, calcular todos los prefijos de 
             $E=\{ x_2,x_4,\ldots, x_{2i},\ldots, x_n \}$
          3. Calcular en paralelo los prefijos en posiciones impares,
             multiplicando los prefijos de $E$ por una sola valor. 

        - algo2(i,j)

          - for $d\leftarrow 1$ to $(\lg n)-1$
            - if $p=0 \mod 2^{d+1}$
              - if $p+2^d<n$ 
                - $x_{p+2^d} \leftarrow x_p . x_{p+2^d}$
          - for $d\leftarrow 1$ to $(\lg n)-1$
            - if $p=0 \mod 2^{d+1}$
              - if $p-2^d>0$ 
                - $x_{p-2^d} \leftarrow x_{p-2^d}.x_p$

        - visualización 

          0. 
          1. [0,1]
          2. 
          3. [2,3] [0,3]
          4. 
          5. [4,5]
          6. 
          7. [6,7] [4,7] [0,7]
          8. 
          9.  [8,9]
          10.
          11. [10,11] [8,11]
          12. 
          13. [12,13]
          14. 
          15. [14,15] [12,15] [8,15] [0,15]

        - Notas:
          - Este algoritmo es EREW

        - Análisis
          - $T_2(n) = 2\lg n$
          - $W(n) = n + W(n.2) = n$
          - $T_2(n,p) = T(n) + W(n)/p = 2\lg n + \frac{n}{p}$

          - Calculamos la cantidad óptima de procesadores para obtener
            el tiempo óptima:
            - $T_2(n) = W(n) / p^*$ entonces
            - $p^* = {W(n) \over T(n)} = {n\over\lg n}$

          - $E_2(n,p^*) = { T^*(n) \over p^* T(n)+ W(n)} = n/n \in O(1)$




   10) PROBLEMA: Ranking en listas

       1. DEFINICIÓN
          
          - dado una lista, calcula el rango para cada elemento.

          - En el caso de una lista tradicional, no se puede hacer
            mucho mejor que lineal.

          - Consideramos una lista en un arreglo $A$, 
            - donde cada elemento $A[i]$ tiene un puntero al siguiente
              elemento, $N[i]$, y
            - calculamos su rango $R[i]$ en un arreglo $R$.

       2. DoublingRank()
          - $R[p] \leftarrow 0$
          - if $N[p] =  null$
            - R[p] \leftarrow 1
          - for $d\leftarrow 1$ to $\lceil\lg n\rceil$
            - if $N[p]\neq NULL$
              - if $R[N[p]]>0$ 
                - $R[p] \leftarrow R[N[p]]+2^d$
              - $N[p] \leftarrow N[N[p]]$

       3. Análisis

          - T(n) = \lg n
          - W(n) = n + W(n/2) = O(n)
          - T(n,p) = T(n) + W(n)/p = \lg n + n/p
          - p^*
            T(n) = W(n) / p^*
            p n/\lg n
          - E(n,p^*) = \frac{T^*(n)}{ p^* T(n) + W(n) }
                     = \frac{ n }{ n/\lg n \lg n +n} 
                     = \Theta(1)

       4. El algoritmo es EREW o CREW?

          - es EREW si los procesadores están sincronizados, com en RAM
            aquí.
       
           
 

[1] FOOTNOTE DEFINITION NOT FOUND: 2

[2] FOOTNOTE DEFINITION NOT FOUND: 3

[3] FOOTNOTE DEFINITION NOT FOUND: 1

[4] FOOTNOTE DEFINITION NOT FOUND: 0

