mtpTema2

38
Capítulo 2: Esquemas Algorítmicos Fundamentales Capítulo 2: Esquemas algorítmicos fundamentales 1ALGORITMOS DEVORADORES......................................................................................................................................3 1.1DESCRIPCIÓN........................................................................................................................................................................3 1.2DAR LA VUELTA (1)..............................................................................................................................................................5 1.3PROBLEMA DE LA MOCHILA (1)...............................................................................................................................................9 1.4ÁRBOL DE EXPANSIÓN MÍNIMO (ALGORITMO DE PRIM).............................................................................................................11 1.5CAMINOS MÍNIMOS.............................................................................................................................................................13 1.6PLANIFICACIÓN: MINIMIZACIÓN DEL TIEMPO EN EL SISTEMA......................................................................................................17 2DIVIDE Y VENCERÁS......................................................................................................................................................19 2.1ORDENACIÓN......................................................................................................................................................................19 2.2MULTIPLICACIÓN DE ENTEROS GRANDES................................................................................................................................20 2.3ALGORITMOS DE SELECCIÓN Y DE BÚSQUEDA DE LA MEDIANA...................................................................................................22 2.4TORRES DE HANOI..............................................................................................................................................................23 2.5CALENDARIO DE UN CAMPEONATO.........................................................................................................................................25 3ALGORITMOS DE VUELTA ATRÁS.............................................................................................................................29 3.1PROBLEMA DE DAR EL CAMBIO.............................................................................................................................................29 3.2PROBLEMA DE LAS 8 REINAS.................................................................................................................................................31 3.3PROBLEMA DE ATRAVESAR UN LABERINTO.............................................................................................................................33 4PROGRAMACIÓN DINÁMICA.......................................................................................................................................37 4.1CÁLCULO DE LOS N PRIMEROS NÚMEROS PRIMOS......................................................................................................................37  2-1

Transcript of mtpTema2

Page 1: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

Capítulo 2: Esquemas algorítmicos fundamentales

1ALGORITMOS DEVORADORES......................................................................................................................................3

1.1DESCRIPCIÓN........................................................................................................................................................................31.2DAR LA VUELTA (1)..............................................................................................................................................................51.3PROBLEMA DE LA MOCHILA (1)...............................................................................................................................................91.4ÁRBOL DE EXPANSIÓN MÍNIMO (ALGORITMO DE PRIM).............................................................................................................111.5CAMINOS MÍNIMOS.............................................................................................................................................................131.6PLANIFICACIÓN: MINIMIZACIÓN DEL TIEMPO EN EL SISTEMA......................................................................................................17

2DIVIDE Y VENCERÁS......................................................................................................................................................19

2.1ORDENACIÓN......................................................................................................................................................................192.2MULTIPLICACIÓN DE ENTEROS GRANDES................................................................................................................................202.3ALGORITMOS DE SELECCIÓN Y DE BÚSQUEDA DE LA MEDIANA...................................................................................................222.4TORRES DE HANOI..............................................................................................................................................................232.5CALENDARIO DE UN CAMPEONATO.........................................................................................................................................25

3ALGORITMOS DE VUELTA ATRÁS.............................................................................................................................29

3.1PROBLEMA DE DAR EL CAMBIO.............................................................................................................................................293.2PROBLEMA DE LAS 8 REINAS.................................................................................................................................................313.3PROBLEMA DE ATRAVESAR UN LABERINTO.............................................................................................................................33

4PROGRAMACIÓN DINÁMICA.......................................................................................................................................37

4.1CÁLCULO DE LOS N PRIMEROS NÚMEROS PRIMOS......................................................................................................................37

                                                                                                                                         2­1

Page 2: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

                                                                                                                                         2­2

Page 3: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

Capítulo 2

ESQUEMAS ALGORÍTMICOS FUNDAMENTALES

Cuando se estudian los problemas y algoritmos usualmente escogidos para mostrar los mecanismos de un lenguaje algorítmico (ya sea ejecutable o no), puede parecer que el desarrollo de algoritmos es un cajón de sastre en el que se encuentran algunas ideas de uso frecuente y cierta cantidad de soluciones ad hoc, basadas en trucos más o menos ingeniosos.

Sin  embargo,   la   realidad  no  es   así:  muchos  problemas   se  pueden   resolver   con  algoritmos construidos en base a unos pocos modelos, con variantes de escasa importancia. En este capítulo se estudian algunos de esos esquemas algorítmicos fundamentales, su eficiencia y algunas de las técnicas más empleadas para mejorarla. 

1 Algoritmos devoradoresEstos algoritmos toman decisiones basándose en la información que tienen disponible de modo inmediato, sin tener en cuenta los efectos que estas decisiones puedan tener en el futuro. Por tanto, resultan fáciles de inventar, fáciles de implementar y, cuando funcionan, son eficientes. Sin embargo, como el mundo no suele ser tan sencillo, hay muchos problemas que no se pueden resolver correctamente con dicho enfoque.

La estrategia de estos algoritmos es básicamente iterativa, y consiste en una serie de etapas, en cada una de las cuales se consume una parte de los datos y se construye una parte de la solución, parando cuando se hayan consumido totalmente los datos. El nombre de este esquema es muy descriptivo: en cada fase (bocado) se consume una parte de los datos. Se intentará  que la parte consumida sea lo mayor posible, bajo ciertas condiciones.

1.1 DescripciónPor ejemplo, la descomposición de un número  n  en primos puede describirse mediante un esquema devorador. Para facilitar la descripción, consideremos la descomposición de 600 expresada así:

600 = 23 31 52

En cada fase, se elimina un divisor de n cuantas veces sea posible: en la primera se elimina el 2, y se considera el correspondiente cociente, en la segunda el 3, etc. y se finaliza cuando el número no tiene divisores (excepto el 1).

El esquema general puede expresarse así:

entero Resolver (D){

Generar la parte inicial de la solución S (y las condiciones iniciales)mientras (D sin procesar del todo) {

Extraer de D el máximo trozo posible TProcesar T (reduciéndose D)

                                                                                                                                         2­3

Page 4: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

Incorporar el procesado de T a la solución S}

}

La descomposición de  un  número  en   factores  primos   se  puede   implementar   sencillamente siguiendo este esquema:

entero Descomponer (n){

{PreC.: n>1}d←2;mientras n mayor que 1{      Dividir n por d cuantas veces (k) se pueda      Escribir "dk"

      d←d+1;}

}

Implementación en JAVA

public class FactPrimos {static public void descompFactPrimos(int n) {

int i = 2;        // Factor primo inicialint k; // Número de veces factores

System.out.print(n + " = ");while (n > 1) {

k = 0;while ( ( n%i ) == 0 ){

++k;n /= i;

}

if ( k > 0 ) {System.out.print("(" + i + "^" + (k) +") ");

}

++i;}

System.out.println();}

public static void main(String[] args) {descompFactPrimos( 484 );

}}

                                                                                                                                         2­4

Page 5: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

1.2 Dar la vuelta (1).Supongamos que vivimos en un país en el que están disponibles las siguientes monedas:100 pesetas, 25 pesetas, 10 pesetas, 5 pesetas y 1 peseta (con un número ilimitado de cada tipo de moneda). Nuestro problema consiste en diseñar un algoritmo para pagar una cierta cantidad a un cliente, utilizando el menor número posible de monedas. Por ejemplo, si tenemos que pagar 289 pesetas, la mejor solución consiste en dar al cliente 10 monedas: 2 de 100, 3 de 25, 1 de 10 y 4 de 1 peseta. Todos nosotros resolvemos este tipo de problemas todos los días sin pensarlo dos veces, empleando de forma inconsciente un algoritmo voraz: empezamos por nada, y en cada fase vamos añadiendo monedas que ya estén seleccionadas una moneda de la mayor denominación posible, pero que no debe llevarnos más allá de la cantidad que haya que pagar.

Con  los  valores  dados  para   las  monedas  y  disponiendo de  un  suministro  adecuado de  cada denominación, este algoritmo siempre produce una solución óptima para nuestro problema. Sin embargo, con una serie de valores diferente, o si el suministro de alguna de las monedas está limitado, el algoritmo voraz puede no funcionar. Por ejemplo, si el sistema de monedas fuera de 1, 7 y 9 pesetas el cambio de 15 pesetas que ofrece este algoritmo consta de 7 monedas: 1 de 9 y 6 de 1. Mientras que el cambio óptimo requiere tan sólo 3 monedas: 2 de 7 y 1 de 1. Por lo tanto, el algoritmo presentado para el cambio de moneda resultará  correcto  siempre  que  se considere  un  sistema monetario  donde  los  valores  de   las monedas son cada uno múltiplo del anterior. Si no se da esta condición, es necesario recurrir a otros esquemas algorítmicos.

El algoritmo se puede formalizar de la siguiente forma:

Conjunto_de_monedas devolverCambio (entero n){

/*     Da el cambio de n unidades utilizando el menor número posible de monedas.    En C se especifican las monedas disponibles*/C ← {100,25,10,5,1}S ←∅ // S es un conjunto que contendrá la solucións  ← 0  // Es la suma de los elementos de Smientras ( s != n ){

x ← el mayor elemento de C tal que s + x <= nsi no existe ese entonces devolver “No encuentro la solución”S ← S ∪ {una moneda de valor x}s ← s + x

}devolver S

}

Generalmente, los algoritmos voraces y los problemas que éstos resuelven se caracterizan por la mayoría de las siguientes propiedades:

• Tenemos que resolver algún problema de forma óptima. Para construir la solución disponemos de un conjunto de candidatos (las monedas disponibles).

• A medida que avanza el algoritmo, vamos acumulando dos conjuntos. Uno contiene los candidatos que ya han sido considerados y seleccionados,  mientras que el  otro contiene candidatos que han sido considerados y rechazados.

                                                                                                                                         2­5

Page 6: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

• Existe una función que comprueba si un cierto conjunto de candidatos constituye una solución del problema, ignorando si es o no óptima por el momento. (Por ejemplo ¿suman las monedas seleccionadas la cantidad que hay que pagar?).

• Hay una segunda función que comprueba si un cierto conjunto de candidatos es factible, esto es, si es posible o no completar el conjunto añadiendo otros candidatos para obtener al menos una solución del problema. Una vez más, no nos preocupa aquí si esto es óptimo o no. Normalmente, se espera que el problema tenga al menos una solución que sea posible obtener empleando candidatos del conjunto que estaba disponible inicialmente.

• Hay otra función más, la función de selección, que indica en cualquier momento cuál es el más prometedor de los candidatos restantes, que no han sido seleccionados ni rechazados.

• Por último, existe una función objetivo que da el valor de la solución que hemos hallado (número de monedas utilizadas  para  dar   la  vuelta).  A  diferencia  de   las   tres   funciones  mencionadas  anteriormente,   la   función objetivo no aparece explícitamente en el algoritmo voraz.

Los   algoritmos   voraces   avanzan   paso   a   paso.   Inicialmente,   el   conjunto   de   elementos seleccionados está vacío. Entonces, en cada paso se considera añadir a este conjunto el mejor candidato sin considerar los restantes, estando guiada nuestra elección por la función de selección. Si el conjunto ampliado  de  candidatos   seleccionados  ya  no   fuera   factible,   rechazamos  el   candidato  que  estamos considerando en ese momento. Sin embargo, si el conjunto aumentado sigue siendo factible, entonces añadimos el candidato actual al conjunto de candidatos seleccionados, en donde pasará a estar desde ahora en adelante. Cada vez que se amplía el conjunto de candidatos seleccionados, comprobamos si éste constituye ahora una solución para nuestro problema.

Conjunto voraz(C:conjunto){

{C es el conjunto de candidatos}S←∅ {Construimos la solución en el conjunto S}mientras ( C != ∅ y no solución( s ) )

x ← seleccionar ( C )C ← C \ {x}Si factible ( S ∪ {x} ) S ← S  ∪ {x}

si solucion(S) devolver Ssino devolver "no hay soluciones"

}

Está  clara   la   razón por   la  cual   tales  algoritmos se denominan “voraces”:  en  cada  paso,  el procedimiento selecciona el  mejor  bocado que pueda  tragar,  sin  preocuparse por  el   futuro.  Nunca cambia de opinión: una vez que un candidato se ha incluido en la solución queda allí para siempre; una vez que se excluye un candidato de la solución nunca vuelve a ser considerado.

Volviendo por un momento al ejemplo de dar cambio, lo que sigue es una forma de adecuar las características generales de los algoritmos voraces a las características particulares de este problema.

1. Los candidatos son un conjunto de monedas, que representan en nuestro ejemplo 100, 25, 10, 5, 1 pesetas, con tantas monedas de cada valor que nunca las agotamos. (Sin embargo, el conjunto de candidatos debe ser finito.)

2. La función de solución comprueba si el valor de las monedas seleccionadas hasta el momento es exactamente el valor que hay que pagar.

3. Un conjunto de monedas será factible si su valor total no sobrepasa la cantidad que haya que pagar.

                                                                                                                                         2­6

Page 7: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

4. La función de selección toma la moneda de valor más alto que quede en el conjunto de candidatos.

5. La función objetivo cuenta el número de monedas utilizadas en la solución.

Está   claro   que   es   más   eficiente   rechazar   todas   las   monedas   restantes   de   100   pesetas   por ejemplo, cuando el valor restante que hay que pagar cae por debajo de ese valor. El uso de la división entera para calcular cuántas monedas de un cierto valor hay que tomar también es más eficiente que actuar por sustracciones sucesivas. Si se adopta cualquiera de estas tácticas, entonces podemos evitar la condición consistente en que el conjunto de monedas debe ser finito.

                                                                                                                                         2­7

Page 8: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

Implementación en JAVA

public class Cambio {public static int[] darCambio(int obj, int[] C) {// C debe estar ordenado de mayor a menor

int [] S    = new int[C.length];int parcial = 0;int i       = 0;int k;

// Mientras no se haya cumplido el objetivo// con las monedas disponibleswhile ( parcial < obj

       && i < C.length ) {       

// buscar una moneda que “quepa”k = obj – parcial;if ( i < C.length

         &&  C[i] <= k ){ 

// se encontró una monedak /= C[i]; // ¿cuantas de este tipo?S[i] += k;parcial += C[i]*k;

}

++i;}

if ( parcial < obj ) {             S[0] = ­1;      // Marca “no hay solución”   }

return S;}

public static void main(String[] args) {int[] C = {100, 50, 25, 5, 1};int[] resultado;

resultado = darCambio( 77, C );

if ( resultado[0] > ­1 ) {for (int i = 0; i < resultado.length; ++i)     if ( resultado[i] > 0 ) {

System.out.println( resultado[i] + " de " + C[i] );     }

}else System.out.println( "No hay solución" );

}}

                                                                                                                                         2­8

Page 9: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

1.3 Problema de la mochila (1)Se desea llenar una mochila hasta un volumen máximo V, y para ello se dispone de n objetos, 

en cantidades limitadas v1, ..., vn y cuyos valores por unidad de volumen son p1, ..., pn, respectivamente. Puede seleccionarse de cada objeto una cantidad cualquiera ci ∈ R con tal de que ci <= vi. El problema 

consiste en determinar las cantidades c1, ..., cn que llenan la mochila maximizando el valor   ∑i= 1

n

vipi

total.Este problema puede resolverse fácilmente seleccionando, sucesivamente, el objeto de mayor 

valor por unidad de volumen que quede y en la máxima cantidad posible hasta agotar el mismo. Este paso se repetirá hasta completar la mochila o agotar todos los objetos. Por lo tanto, se trata claramente de un esquema voraz.

Si pensamos en el anterior problema, pero añadiendo la condición (más realista) de un número de monedas   limitado para cada  tipo,   tendremos exactamente el  mismo algoritmo presentado aquí. Dicho de otra forma, este algoritmo es el mismo que el anterior con el control de número de objetos de cada volumen añadido, además de un pequeño bucle auxiliar (bajo el comentario “buscar el primer objeto que cabe”) que se introduce por eficiencia y que es también aplicable al mencionado anterior algoritmo.

Implementación en JAVA (incluye entrada/salida)

public class Mochila {        public static int[] llenarMochila(int V, int[] v, int[] n)    /*        IMPORTANTE: v debe estar ordenado de mayor a menor        V es el volumen total a llenar        v[] son los volúmenes de los objetos        n[] es un vector paralelo con las cantidades    */    {        int [] S    = new int[v.length];        int parcial = 0;        int i       = 0;        int k;            // Mientras no se haya cumplido el objetivo        // ... o agotado los objetos ...        while ( parcial < V             && i < v.length )        {            // buscar el primer objeto que cabe            while( i < v.length              && ( ( parcial + v[i] ) > V ) ) {                    ++i;            }                            // ¿se encontró un objeto?            if ( i < v.length )            {                 if ( n[i] > 0 ) {

                                                                                                                                         2­9

Page 10: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

                    k = (V­parcial) / v[i];  // ¿cuántos de este tipo?                                    if ( k > n[i] ) {                        k = n[i];                    }                                    S[i] += k;                    parcial += v[i] * k;                }            } else {                S[0] = ­1; // Marca “no hay solución”                parcial = V; // Fin de bucle            }

            ++i;        }

        return S;    }

    public static void main(String[] args) {        int[] v = {100, 50, 25, 5, 1};        int[] n = { 0,   0,  2, 5, 5};        int[] resultado;                resultado = llenarMochila( 77, v, n );                if ( resultado[0] > ­1 )        {            for (int i = 0; i < resultado.length; ++i) {                 if ( resultado[i] > 0 ) {                     System.out.println( resultado[i] + " de " + v[i] );                 }            }        } else System.out.println( "No hay solución" );    }}

                                                                                                                                         2­10

Page 11: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

1.4 Árbol de expansión mínimo (Algoritmo de Prim).Consideremos  un  mapa  de  carreteras,   con  dos   tipos  de   componentes:   las   ciudades   (nodos)  y   las carreteras que   las  unen.  Cada  tramo de  carreteras   (arco)  está   señalado con su  longitud.  Se desea implantar un tendido eléctrico siguiendo los trazos de las carreteras de manera que conecte todas las ciudades y que la longitud total sea mínima.

Otra forma de plantear el problema es la siguiente: “un viajante debe recorrer una serie de ciudades   interconectadas   entre   sí,   de   manera   que   recorra   todas   ellas   con   el   menor   número   de kilómetros posible”.

Una forma de lograrlo consiste en:

Empezar con el tramo de menor costerepetir

Seleccionar un nuevo tramohasta que esté completa una red que conecte todas las ciudades

donde cada nuevo  tramo que se selecciona es el de menor longitud entre los no redundantes (es decir, que da acceso a una ciudad nueva).

Un razonamiento sencillo nos permite deducir que un árbol de expansión cualquiera (el mínimo en particular)   para un mapa de  n  ciudades tiene  n­1  tramos. Por lo tanto, es posible simplificar la condición de terminación que controla el algoritmo anterior.

La representación del grafo se realizará mediante una matriz n x n. La casilla i,  j guardará el valor del arco entre las ciudades i y j. En el caso de no existir arcos, o de tratarse de la diagonal de la matriz (casillas  i,  i), se elegirá un valor que denotará la falta de arco. Este valor especial puede ser cualquiera (por ejemplo, ­1), pero si escogiendo un valor apropiado es posible simplificar mucho el algoritmo. Así, un valor de 999999 será muy útil, dado que se escogerá en cada paso el arco con menor valor.

Implementación en JAVA

public class Prim {    private static final int NOHAYENLACE = 999999;

    // Comprueba si el elemento "i" está en el vector "v"    // Sirve para saber si se ha visitado ya un nodo    static private boolean estaEn(int[] v, int i)     {

int j;            for (j = 0; j < v.length; ++j) {                if ( v[j] == i ) {                    break;                }

}

            return ( j < v.length );    }

   

                                                                                                                                         2­11

Page 12: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

    public static int[] caminoMinimo(int[][] L, int salida)    {        int numnodos = L.length;        int S[]      = new int[numnodos];        int lcamino  = 0;        int menor    = 0;        int nodoact  = salida;            S[0]         = salida;

        while ( lcamino < ( numnodos – 1 ) )   {

            // Encontrar el camino mínimo al siguiente nodo            for (int i = 1; i < numnodos; ++i)

{    // El más pequeño, pero no es posible repetir nodos !!

                if ( L[nodoact][i] < L[nodoact][menor]                  && !estaEn( S, i ) )                  {

menor = i;    }}

            // El nodo siguiente es el destino del camino mínimo anterior            ++lcamino;            nodoact = menor;            S[lcamino] = nodoact;        }                return S;    }

    public static void main(String args[]) {        /* El grafo será            n0 a n1 30 n0 a n2 40 n1 a n3 60            n1 a n2 10 n2 a n3 50 n0 a n3 80        */

        // Vector de soluciones        int sol[];

        // Crear un grafo de 4 vértices representado por una matriz        int g[][] = new int [4][4];

        // Inicializar la matriz representando el grafo        for(int i = 0; i < 4; ++i) {            for(int j = 0; j < 4; ++j) {                g[i][j] = NOHAYENLACE;            }        }

        // Rellenar el grafo con las aristas        g[0][1] = 30; g[0][2] = 15;        g[0][3] = 80; g[1][3] = 60;        g[1][2] = 10; g[2][3] = 50;        g[2][1] = 10;

                                                                                                                                         2­12

Page 13: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

        // Encontrar el camino mínimo (se empieza desde el nodo 0)        sol = caminoMinimo( g, 0 );

        // Mostrar el camino        System.out.print( "Solución: " );        for(int i = 0; i < 4; ++i) // n­1 aristas entre n nodos                System.out.print( sol[i] + " " );

        System.out.println();    }}

1.5 Caminos Mínimos.Consideremos un grafo dirigido G = <N,A> en donde N es el conjunto de nodos de G, y A es el conjunto de aristas dirigidas. Cada arista posee una longitud no negativa. Se toma uno de los nodos como nodo origen. El problema consiste en determinar la longitud del camino mínimo que va desde el origen hasta cada uno de todos lo demás nodos del grafo. Podríamos considerar el coste de una arista, en lugar de mencionar su longitud, y se podría plantear el problema consistente en determinar la ruta más barata desde el origen hasta cada uno de todos los demás nodos.

Este problema se puede resolver mediante un algoritmo voraz que recibe frecuentemente el nombre de algoritmo de Dijkstra. El algoritmo utiliza dos conjuntos de nodos, S y C. En todo momento, el conjunto S contiene aquellos nodos que ya han sido seleccionados; como veremos, la distancia mínima desde el origen ya es conocida para todos los nodos de S. El conjunto C contiene todos los demás nodos, cuya distancia mínima desde el  origen todavía no es conocida, y que son candidatos a ser seleccionados en alguna etapa posterior. Por tanto, tenemos la propiedad invariante N = S  ∪ C. En primer momento, S contiene nada más el origen en sí; cuando se detiene el algoritmo, S contiene todos los nodos del grafo y nuestro problema está resuelto. En cada paso seleccionamos aquel nodo de C cuya distancia al origen sea mínima, y se lo añadimos a S.

Diremos que un camino desde el  origen hasta  algún otro nodo es especial  si   todos   los nodos intermedios a lo largo del camino pertenecen a S. En cada fase del algoritmo, hay una matriz D que contiene la longitud del camino especial más corta que va hasta cada nodo del grafo. En el momento en que se desea añadir un nuevo nodo v a S, el camino especial más corto hasta v es también el más corto de   los   caminos   posibles   hasta   v.   Cuando   se   detiene   el   algoritmo,   todos   los   nodos   del   grafo   se encuentran en S, y por tanto todos los caminos desde el origen hasta algún otro nodo son especiales. Consiguientemente, los valores que hay en D dan la solución del problema de caminos mínimos.

Por   sencillez   suponemos  que   los  nodos  de  G están numerados  desde  0  hasta  n,  así  que  N = {0,1,...,n}.   Podemos   suponer   sin   pérdida   de   generalidad   que   el  nodo   cero  es   el   nodo   origen. Supongamos también que la matriz L da la longitud de todas las aristas dirigidas: L[i][j] >= 0 si la arista (i,j) ∈ A, y L[i][j] = ∞ en caso  contrario. Véase a continuación el algoritmo:

                                                                                                                                         2­13

Page 14: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

int [] Dijkstra (int [][] L){

int [] D // Posiciones desde 0 hasta n

{Iniciación}C ← {1,2,...,n}  // S = N \ C sólo existe implícitamentepara (i ← 1, a n)

D[i] = L[0][i]

{bucle voraz}repetir n­2 veces {

v  ← algún elemento de C que minimiza D[v]C ← C \ {v} //e implícitamente S ← S ∪ {v}para cada w ∈ C hacer {

D[w] ← min(D[w], D[v] + L[v][w])}

}

devolver D } 

Análisis del algoritmo: Supongamos que el algoritmo de Dijkstra se aplica a un grafo que posee n nodos y a aristas. Utilizando la representación sugerida hasta el momento, este caso se da en la forma de una matriz L[n][n]. La iniciación requiere un tiempo de O(n). En una implementación directa, la selección   de  v  dentro   del   bucle   (repetir)   requiere   examinar   todos   los   elementos   de   C,   así   que examinaremos n­1, n­2,...,2 valores de D en las sucesivas iteraciones, dando un tiempo total de O(n2). El bucle para  interno realiza n­2,  n­3,...,1   iteraciones  dando también un tiempo total  de O(n2).  El tiempo requerido para esta versión del algoritmo es por tanto de O(n2).

                                                                                                                                         2­14

Page 15: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

Implementación en JAVA

class DikstraGrafo{        // Indica que no hay arista entre dos nodos    public static final int NOHAYENLACE = 9999;        /**     * dijstra() ­ El algoritmo que halla los caminos mínimos     *              desde el origen, dado un grafo     *                   * @param L El grafo representado como una matriz     * @return Un vector de enteros, cada posición "i" es      *         el coste del camino mínimo desde el nodo 0     *         hasta el nodo "i".    */    public static int[] dijkstra(int[][] L)    {        // Nodos del grafo procesados, se considera        // que el nodo 0 es desde el que se parte (ya procesado)        boolean [] C = {false, true, true, true, true};                     // En D se almacenan los caminos mínimos desde el nodo 0        int [] D = new int[C.length];                 // Inicializar la distancia mínima desde el nodo 0        // hasta el resto de nodos        D[0]= 0;        for (int i = 1; i < C.length; ++i) {            D[i] = L [0][i];         }            // Preparar la visualización paso a paso        System.out.println( "      Paso        v         C           D" );        System.out.print( "Inicializacion    ­­     " );        visualizar( D, C );                // Nodo que posee el menor camino en cada pasada            int v = 0;        int j;                // Repetir n­2 veces            for (int i = 0; i < ( C.length – 2 ); ++i)        {            // Buscar el primer nodo del que todavía             // no se ha calculado el camino mínimo            j = 1;            while ( !C[j] ) {                j++;            }                            v = j;                             // Continuar búsqueda del nodo con la distancia más pequeña            for (++j; j < C.length; ++j) {               //Nodo con la distancia más pequeña               if ( C[j]

                                                                                                                                         2­15

Page 16: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

               && D[v] > D[j] )               {                   v = j;                }            }                               // v nodo con el camino mínimo ya calculado                           C[v] = false;                          // Se calcula el mínimo camino para los nodos             // que todavía no se han visitado            for (j = 1; j < C.length; ++j)             {                if ( C[j]                  && ( D[j] > (D[v] + L[v][j] ) ) )                {                    D[j] = (D[v]+ L[v][j]);                }            }                 //Utilizando el nuevo nodo que se acaba de visitar            System.out.print( "        "                             + i                             + "          "                            + v                             + "     "  )

;            visualizar( D, C );        }                return D;    }            // Muestra por pantalla el contenido de los nodos    // que quedan por visitar     // y sus caminos mínimos    // Permite comprobar la ejecución del algoritmo paso a paso     static void visualizar (int[] caminoMinimo, boolean[] visitados)     {         System.out.print( "{" );            for (int i = 0; i < visitados.length; ++i)         {            if ( visitados[i] ) {                System.out.print( i + " " );              }         }

         System.out.print( "}" );         System.out.print( "      " );         System.out.print( "[" );              for (int i = 1; i < caminoMinimo.length; ++i)         {            System.out.print( caminoMinimo[i]+" " );         }

                                                                                                                                         2­16

Page 17: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

         System.out.print( "]" );         System.out.println();    }        // Asigna en L el valor de las aristas, cuando una arista no existe    // le asigna el máximo valor NOHAYENLACE    public static void main(String[] args)    {       // Solución con los caminos mínimos       int[] D;               // L es la representación del grafo       int [][] L; 

       // Crear un grafo a resolver 5x5       L = new int [5][5];              // Inicializarlo con los valores adecuados       for (int i = 0; i< L.length; ++i) {           for (int j = 0; j < L[i].length; ++j) {               L[i][j] = NOHAYENLACE;

     } }

                      // Crear las aristas       L[0][1]= 50; // "Coste" de ir de nodo "0" a nodo "1"       L[0][2]= 30;       L[0][3]= 100;       L[0][4]= 10;       L[2][1]= 5;       L[3][1]= 20;       L[3][2]= 50;       L[4][3]= 10;              // Resolver       D = dijkstra( L );              // Mostrar       System.out.print( "\n\nSolución: {" );

       for (int i = 0; i < D.length; ++i)       {            System.out.print( D[i]+" " );       }       

       System.out.println( "}\n" );    }}

1.6 Planificación: Minimización del tiempo en el sistema.El  problema   consiste   en  minimizar   el   tiempo   medio  que   invierte   cada   tarea   en   el   sistema.  Este problema se puede resolver empleando un algoritmo voraz.

Un único servidor, como por ejemplo un procesador, un surtidor de gasolina, o un cajero de un banco, tiene que dar servicio a n clientes. El tiempo requerido por cada cliente se conoce de antemano: el cliente i requerirá un tiempo ti para 1<= i  <= n. Deseamos minimizar el tiempo invertido por cada 

                                                                                                                                         2­17

Page 18: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

cliente en el sistema. Dado que el número n de clientes está predeterminado, esto equivale a minimizar el tiempo total invertido en el sistema por todos los clientes. En otras palabras, deseamos minimizar T 

=  ∑i= 1

n

tiempo en el sistema para el cliente i)

Supongamos por ejemplo que tenemos tres clientes, con t1 = 5, t2 = 10 y t3 = 3. Existen seis órdenes de servicio posibles:

Orden T123: 5 + (5+10) + (5+10+3) = 38132: 5 + (5+3) + (5+3+10) = 31213: 10 + (10+5) + (10+5+3) = 43231: 10 + (10+3) + (10+3+5) = 41312: 3 + (3+5) + (3+5+10) = 29321: 3 + (3+10) + (3+10+5) = 34

En el primer caso, se sirve inmediatamente al cliente 1, el cliente 2 espera mientras se sirve al cliente 1 y entonces le llega el turno, y el cliente 3 espera mientras se sirve a los clientes 1 y 2, y se le sirve en último lugar; el tiempo total invertido en el sistema por los tres clientes es 38. Los cálculos para los demás casos son similares.

En este  caso   la  planificación óptima  se  obtiene  cuando  se   sirve  a   los   tres   clientes  por  orden creciente de tiempos de servicio: el cliente 3, que necesita el menor tiempo, es servido en primer lugar, mientras que el cliente 2, que necesita mayor tiempo, es servido en último lugar. Al servir a los clientes por orden decreciente de tiempos de servicio se obtiene la peor planificación.

Para dar plausibilidad a la idea de que puede ser óptimo planificar los clientes por orden creciente de tiempo de servicio, imaginemos un algoritmo voraz que construye la planificación óptima elemento a elemento. Supongamos que después de planificar el servicio para los clientes i1, i2, ..., im se añade un cliente j. El incremento del tiempo T en esta fase es igual a la suma de los tiempos de servicio para los clientes i1 hasta im (porque es lo que tiene que esperar el cliente j antes de recibir servicio) más tj, que es el tiempo necesario para servir al cliente j. Para minimizar esto, dado que un algoritmo voraz nunca reconsidera sus decisiones anteriores, lo único que podemos hacer es minimizar tj. Nuestro algoritmo voraz, por tanto, es bastante sencillo: en cada paso se añade al final de la planificación al cliente que requiera el menor servicio de entre los restantes.

Demostración: En la figura 3.1, se comparan dos planificaciones  P  y  P’, se observa que los  a­1 primeros clientes salen del sistema exactamente al mismo tiempo en ambas planificaciones. Lo mismo sucede para los n­b últimos clientes. Ahora el cliente a sale cuando antes salía el cliente b, mientras que el cliente b sale antes que salía el cliente a, porque tb < ta. Finalmente, los clientes que son servidos en  las posiciones desde  a+1  hasta  b+1  también salen antes del sistema, por  la misma razón. Por consiguiente, P’ es mejor que P en conjunto.

1 .. a­1      a a+1 .. b­1         b b+1 .. n

1 .. a­1 b a+1 .. b­1      a b+1 .. n

                                                                                                                                         2­18

El algoritmo voraz es óptimo

Page 19: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

Figura 3.1 Intercambio de dos clientes.De esta manera, se puede optimizar toda planificación en la que se sirva a un cliente antes que a 

otro que requiera menos servicio. Las únicas planificaciones que quedan son aquellas que se obtienen poniendo a los clientes por orden no decreciente de tiempo de servicio. Todas estas planificaciones son claramente equivalentes, y por tanto todas son óptimas.

La implementación de este algoritmo, en esencia, lo único que necesita es ordenar los clientes por orden de tiempo no decreciente de servicio, lo cual requiere un tiempo que está en O(n log n).

2 Divide y Vencerás.La idea básica de este esquema consiste dividir el problema en problemas más pequeños, hasta que estos son triviales. Entonces, se resuelven y se recombinan con las subdivisiones para presentar la solución. Puede esquematizarse en los siguientes pasos:

1. Dado un problema P, con datos D, si los datos permiten una solución directa, se ofrece ésta;2. En caso contrario, se siguen las siguientes fases:

a) Se dividen los datos D en varios conjuntos de datos más pequeños, Di.b) Se   resuelven   los   problemas   P(Di)   parciales,   sobre   los   conjuntos   de   datos   Di, 

recursivamente.c) Se combinan las soluciones parciales, resultando así la solución final.

Esquema genérico:

ts divide_y_venceras (tx x){

ts s1,s2,...,sk;tx x1,..,xk;si x es suficientemente simple devuelve solucion_simple (x)sino {

descomponer x en x1,..,xk;para (i ← 1, a k) {

s[i] ← divide_y_venceras(xi);}devuelve combinar (s1,s2,...,sk);

}}

2.1 OrdenaciónComo ejemplo,   está   que  el  problema  de  ordenar  un  vector   admite  dos   soluciones   siguiendo  este esquema: los algoritmos Merge Sort y Quick Sort. algoritmos que se tratan en el tema de algoritmos de ordenación y búsqueda.

                                                                                                                                         2­19

Page 20: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

El primer nivel de diseño de Merge Sort puede expresarse así:

si v es de  tamaño 1 entonces v ya está ordenadosino

Dividir v en dos subvectores A y BOrdena A y B usando Merge SortMezclar las ordenaciones de A y B para generar el vector ordenado.

El siguiente algoritmo, Quick Sort, resuelve el mismo problema siguiendo también la estrategia de divide y vencerás:

si v es de tamaño 1 entonces v ya está ordenadosino

Dividir v en dos bloques A y BCon todos los elementos de A menores que los de BOrdenar A y B usando Quick sortDevolver v ya ordenado como concatenación de las ordenaciones de A y de B.

Para que el esquema algorítmico divide y vencerás sea eficiente es necesario que el tamaño de los subproblemas obtenidos sea similar. 

2.2 Multiplicación de Enteros grandes.Como ya es conocido,  el   tamaño de cualquier  tipo de dato numérico está  sujeto a un rango. Así, normalmente los tipos de datos numéricos ofrecidos en los lenguajes de programación se basan en los que soporta directamente el hardware. Por ejemplo, los enteros sin signo en máquinas de 16 bits para el lenguaje  C   soportan  65536  valores   distintos,  mientras   que   en  máquinas   de  32  bits   este  valor   se incrementa hasta más allá de los cuatro mil millones. Aunque este limite sea bastante grande, no deja de ser un límite, es decir, no es posible manejar directamente por hardware números cuyo valor sea de por ejemplo cien mil millones o incluso billones.

El   coste   de   realizar   las   operaciones   elementales   de   suma   y   multiplicación,   es   razonable considerarlo constante si los operandos son directamente manipulables por el hardware, es decir, no son  muy grandes.  Si   se  necesitan  enteros  muy grandes,  y  hay  que   implementar  por   software   las operaciones, el coste dependerá de los algoritmos que se desarrollen.

Las   operaciones   de   suma   y   resta   pueden   hacerse   en   tiempo   lineal.   Operaciones   de multiplicación   y   división   entera   por   potencias   positivas   de   10,   pueden   hacerse   en   tiempo   lineal (desplazamiento de las cifras). Sin embargo, la operación de multiplicación con el algoritmo clásico supone un tiempo cuadrático.

A continuación se propone una técnica divide y vencerás para la multiplicación de enteros muy grandes:

• Sean u y v dos enteros de n cifras.• Se descomponen en mitades de igual tamaño:

• u = 10s w+x• v = 10s y+z con 0 <= x < 10s , 0 < = z < 10s y s = n/2

• Por tanto w e y tienen n/2 cifras• El producto es:

                                                                                                                                         2­20

Page 21: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

• uv = 102s wy + 10s (wz+xy)+xz     

         Abundando en lo anterior, es posible presentar un pseudocódigo más detallado, que se presenta a continuación. Nótese que para poder implementarlo es imprescindible desarrollar el TDA GranEntero, el   cuál   además   debe  ofrecer   las  operaciones   citadas   anteriormente,  multiplicación  y  división  por potencias de diez, suma, y resta. Este TDA deberia implementarse como una clase, que guardara en una cadena el entero en cuestión como atributo. Cada operación se implementaría como un método que actúa sobre el entero.

En el siguiente pseudocódigo se asume que tal clase existe y funciona tal cuál ha sido descrita. Por simplicidad, no se incluye la citada clase, y por tanto, no se incluye la implementación en lenguaje Java.

GranEntero mult (GranEntero u, GranEntero v){

GranEntero w,x,y,z;Entero s;

n = max( tamaño( u ), tamaño( v ) );

si n es pequeño {devuelve multclasica ( u , v );

}sino {

s = n/2;w = u / 10s ;x = u % 10s;y = v / 10s;z = v % 10s; devuelve mult( w,y )* 102s + ( mult ( w,z ) + mult( x,y ) )* 10s + mult( x, z );

}}

El coste temporal supone para sumas, multiplicaciones por potencias de 10 y divisiones por potencias de 10 un tiempo lineal.  Operación módulo de una potencia de 10 tiempo lineal (pueden hacerse con una división, una multiplicación y una resta). Si se supone que n es una potencia de 2, t(n)  = 4 t(n/2) + O(n). Por lo tanto, el tiempo de ejecución es de O(n2).

La  mejora   se   consigue   si   conseguimos   reducir   el   cálculo  a  menos  de  4  multiplicaciones. Teniendo en cuenta que: r = (w+x) (y+z) = wy + (wz + xy) + xz ... se puede reescribir el algoritmo de la siguiente forma:

Considerando p = wyq = xz r = (w+x) (y+z) 102s p + 10s (r­p­q) + q

                                                                                                                                         2­21

Page 22: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

GranEntero mult2 (GranEntero u, GranEntero v){

GranEntero w,x,y,z,r,p,q;Entero s;

n = max( tamaño( u ), tamaño( v ) );

si n es pequeño {devuelve multclasica ( u,v );

}sino {

s = n/2;w = u / 10s ;x = u % 10s;y = v / 10s;z = v % 10s;r = mult2(w+x,y+z);p = mult2(w,y);q = mult2(x,z);

devuelve p* 102s + (r-p-q)* 10s + q;

}}  El número de sumas (contando restas como si fueran sumas) es mayor que el algoritmo divide y 

vencerás original.  ¿Merece la pena efectuar cuatro sumas más para ahorrar una multiplicación? La respuesta es negativa cuando se multiplican números pequeños. Sin embargo, merece la pena cuando los números que hay que multiplicar son grandes, el tiempo requerido para la sumas es despreciable frente al tiempo que requiere una sola multiplicación.

El tiempo de t(n) = 3t(n/2) + g(n) cuando n es par y suficientemente grande tendría un tiempo de ejecución de  O(nlg3).  Lg 3  ≈  1.585  es menor que 2, este algoritmo puede multiplicar dos enteros grandes mucho más deprisa que el algoritmo clásico de multiplicación, y cuanto mayor sea  n, más merecerá la pena esta mejora.

2.3 Algoritmos de selección y de búsqueda de la mediana.Dado un vector T de  n  enteros, la mediana de T es un elemento  m  de T que tiene tantos elementos menores como mayores que él en T. Un problema más general es encontrar el k­ésimo menor elemento. Obviamente, se puede realizar ordenando el vector, pero es de esperar que selección sea un proceso más rápido, al solicitarse menor información. Haciendo un pequeño cambio en el algoritmo quicksort, se puede resolver el problema de selección en tiempo lineal, en promedio. Llamamos a este algoritmo selección rápida. Los pasos a realizar son los siguientes:

1. Si el número de elementos en S es 1, entonces presumiblemente k también es 1, por lo que se puede devolver el único elemento en S.

2. En otro caso, elegir un elemento v de S como pivote:1. Hacer una partición de S – {v} en I y D, exactamente igual a como se hacía en el 

quicksort.2. Si  k  es menor o igual que el número de elementos en I, entonces el elemento que 

estamos   buscando   debe   estar   en   I,   por   lo   que   se   llama   recursivamente   a 

                                                                                                                                         2­22

Page 23: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

seleccionRapida(   I,  k  ).  Si  k  es  exactamente  igual  a  uno más  que el  número de elementos I, entonces el pivote es el  k­ésimo  menor elemento y se puede devolver como respuesta. En otro caso, el  k­ésimo  menor elemento estará  en D. De nuevo podemos hacer una llamada recursiva y devolver el resultado obtenido.

2.4 Torres de Hanoi.El de las Torres de Hanoi es un juego matemático consistente en mover unos discos de una torre a otra. La leyenda cuenta que existe un templo (llamado  Benares), bajo la bóveda que marca el centro del mundo, donde hay tres varillas de diamante creadas por Dios al crear el mundo, colocando 64 discos de oro en la primera de ellas. Unos monjes mueven los discos a razón de uno por día, y, el día en que tengan todos los discos en la tercera varilla, el mundo terminará. Como se comprobará a continuación, en realidad 64 discos son suficientes para muchos años.

En este juego, se trata de pasar un número de discos (típicamente, con tres existe una dificultad suficiente como para plantearlo como un pasatiempo), de un poste de origen (el primero, más a la izquierda) a un poste de destino (el tercero, a la derecha), utilizando como poste auxiliar el del medio. Sólo se puede mover un disco de cada vez, y nunca poner un disco sobre un segundo que sea de menor diámetro que el primero. Así, al comienzo del juego todos los discos apilados en el primero (el de la izquierda). Cada disco se asienta sobre otro de mayor diámetro, de manera que tomados desde la base hacia arriba, su tamaño es decreciente. El objetivo, como ya se ha dicho, es mover uno a uno los discos desde el poste A (origen) al poste C (destino) utilizando el poste B como auxiliar, para lo cuál se puede emplear una técnica divide y vencerás, como se explica a continuación.

Vamos a plantear la solución de tal forma que el problema vaya dividiendo en problemas más pequeños, y a cada uno de ellos aplicarles la misma solución. Se puede expresar así:

• El problema de mover n discos de A a C consiste en:

mover los n­1 discos superiores de A a B

mover el disco n de A a C

mover los n­1 discos de B a C

Un problema de tamaño n ha sido transformado en un problema de tamaño n­1. A su vez cada problema de tamaño n­1 se transforma en otro de tamaño n­2 (empleando el poste libre como auxiliar).

• El problema de mover los n­1 discos de A a B consiste en:

mover los n­2 discos superiores de A a C

mover el disco n­1 de A a B

mover los n­2 discos de C a B

De este modo se va progresando, reduciendo cada vez un nivel de dificultad del problema hasta que sólo haya que mover un único disco. La técnica consiste en ir intercambiando la finalidad de los postes, origen, destino y auxiliar. La condición de terminación es que el número de discos sea 1. Cada acción  de  mover   un  disco   realiza   los  mismos  pasos,  por   lo   que  puede   ser   expresada  de  manera recursiva. Así, podemos establecer un pseudocódigo como el siguiente:

                                                                                                                                         2­23

Page 24: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

hanoi(int numDiscos, origen, auxiliar, destino)

{

if ( numDiscos == 1 ) {

mover el disco de origen a destino;

}

hanoi( numDiscos – 1, origen, destino, auxiliar );

mover el disco número numDiscos de origen a destino;

hanoi( numDiscos – 1, auxiliar, origen, destino );

}

Si  el  algoritmo  se   invoca  con  hanoi(  3,  A,  B,  C  ),   se  puede  observar  cómo el  mismo va cambiando las funcionalidades (origen, auxiliar y destino) para los postes (A, B, y C), resolviendo el problema correctamente (también para cualquier otro número de discos).

El número de movimientos aumenta según va aumentando el número de discos, de manera exponencial. De hecho, se puede decir que para n discos, el número de movimientos será 2n – 1.

Número de discos Movimientos

3 7

4 15

5 31

10 1023

64 5,05E+016

En realidad, la leyenda descrita es una pura y absoluta invención. Este juego fue inventado por el matemático francés  Edouard Lucas  en 1883. En aquella época, Francia estaba en plena campaña militar por el sudeste asiático, y es bastante posible que el sitio a la ciudad de Hanoi le inspirase en la creación del nombre para el juego.

Implementación en JAVA

class Torres_de_Hanoi {  static long int cont = 0;

  static void movimiento (int n, char A, char C)  {   System.out.println ("Mover disco " + n + " de " + A + " a " + C);   ++cont;  }

 

                                                                                                                                         2­24

Page 25: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

  static void hanoi (int n,char A, char B, char C)  {    if ( n == 1 ) {

movimiento (n,A,C);    }    else {  

hanoi( n­1, A, C, B );      movimiento( n, A, C );      hanoi( n­1, B, A, C );    }  } 

  public static void main (String [] args) {    int  n = 10;    char A = 'A';    char B = 'B';    char C = 'C';

    hanoi( n, A, B, C );

    System.out.println( "Total movimientos " + cont );  }  } 

2.5 Calendario de un campeonato.Se desea organizar los enfrentamientos en forma de liga para n participantes (asumimos n = 2K). Los enfrentamientos son en días consecutivos, y cada día cada jugador sólo juega un partido.

Cada participante debe saber el orden en el que se enfrenta a los  n­1  restantes. Por tanto, la solución puede representarse por una matriz n x (n­1). El elemento (i,j), 0 <= i <= n, 0 <= j <= n­1, contiene el número del participante contra el que el participante i­ésimo compite el día j­ésimo.

Solución por fuerza bruta:

1. Se obtiene para cada participante i el conjunto P(i) de todas las permutaciones posibles del resto de los participantes {0..n}\{i}.

2. Se completan las filas de la matriz de todas las formas posibles, incluyen en cada fila i algún elemento de P(i).

3. Se elige cualquiera de las matrices resultantes en la que toda columna j, contiene números distintos (nadie puede competir el mismo día contra dos participantes).

Cada conjunto  P(i)  consta de  (n­1)!  Elementos. La matriz tiene  n  filas. Por lo tanto, hay  n! formas distintas de rellenar la matriz. ¡Es demasiado costoso!

Una solución mediante la técnica divide y vencerás.

1. Caso básico: n = 2, basta con una competición.

2. Caso a dividir n = 2K , con k > 1.

a) Se elaboran independientemente dos subcalendarios de   2K­1 participantes, uno para los participantes 0 .. 2K­1 , y otro para los participantes 2K­1 + 1 .. 2K .

                                                                                                                                         2­25

Page 26: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

b) Falta   elaborar   las   competiciones   cruzadas   entre   los   participantes   de   numeración inferior y los de numeración superior.

I. Se completa primero la parte de los participantes de numeración inferior:

• 1er.   Participante:   compite   días   sucesivos   con   los participantes   de   numeración   superior   en   orden creciente.

• 2º participante: toma la misma secuencia y realiza una permutación cíclica de un participante.

• Se   repite   el   proceso   par   todos   los   participantes   de numeración inferior.

II. Para la numeración superior se hace lo análogo con los de  la inferior.

Implementación en JAVA

public class Campeonato{ private int participantes; private int[][] Calendario;  // Método para visualizar el calendario, se muestra en pantalla // en forma de matriz de n x (n­1), siendo las filas los jugadores // y las columnas los días de la competición

 void Visualizar() {    System.out.println( "Filas = jugadores, Columnas = días competición\n" );   System.out.print( "  " );

   for (int i = 0; i < participantes ­1; ++i) {   System.out.print(" "+i+" ");   }

   System.out.println();   System.out.println( " ­­­­­­­­­­­­­­­­­­­­­­­" );     for (int i = 0; i < participantes; ++i)   {     System.out.print( i + "|" ); 

     for (int j = 0; j < ( participantes – 1 ); ++j) {        System.out.print( " " + Calendario[i][j] + " " );     }

     System.out.println();    } } 

                                                                                                                                         2­26

Page 27: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

 void establecerCalendario(int[][] Calen, int inicio, int fin) {   if ( inicio == fin – 1 ) { 

Calen[inicio][0] = fin;      Calen[fin][0]    = inicio;   }   else {       int medio = ( inicio + fin ) /2;       establecerCalendario( Calen, inicio, medio );       establecerCalendario( Calen, medio+1, fin );

       //Enfrentamiento entre los equipos inferiores y superiores,  //se le pasa cual es el

       //equipo inicial, el equipo final, día de comienzo, día de fin //y cual es el equipo

       //que inicia el enfrentamiento

 completarCalendario( Calen,                      inicio,                      medio,                       ( fin­inicio )/2,                      ( fin­inicio ) ­ 1,                      medio + 1 );  completarCalendario( Calen,                       medio + 1,                       fin,                       ( fin ­ inicio ) / 2,                      ( fin­inicio ) ­ 1,                      inicio 

       );    }                      }  void completarCalendario(int[][] Calen,

  int Equipoinf, int Equiposup,  int diainicio, int diafin, int Equipoinicio)

 {

  // El primer participante de numeración inferior  // participa en orden creciente  // con los participantes  // de numeración superior, y el primer participante de numeración   // superior   participa en orden creciente  // con los participantes de numeración inferior.

  for (int i = diainicio; i <= diafin; ++i) {    Calen[Equipoinf][i] = Equipoinicio++;  }     // Para el resto de los participantes    for (int i = Equipoinf + 1; i <= Equiposup; ++i)  {   // El primer día se enfrenta al participante    // con el que se enfrentó el participante

                                                                                                                                         2­27

Page 28: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

   // anterior el último día      Calen[i][diainicio] = Calen[i­1][diafin];     // El resto de los días se enfrenta al participante    // que se enfrentó el día anterior   // contra el participante anterior

   for (int j = diainicio + 1; j <= diafin; ++j) {        Calen[i][j] = Calen [i­1][j­1];   }  } }}

class Ppal { public static void main(String  args[]) {

Campeonato Obj = new Campeonato();

Obj.participantes = 8;Obj.Calendario    = new int[Obj.participantes][Obj.participantes­1];Obj.establecerCalendario( Obj.Calendario, 0, Obj.participantes ­ 1 );Obj.Visualizar();

 }}

                                                                                                                                         2­28

Page 29: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

3 Algoritmos de vuelta atrás.Los   algoritmos   de   vuelta   atrás   (también   conocidos   como   de  backtracking)   hacen   una   búsqueda sistemática de todas las posibilidades, sin dejar ninguna por considerar. Cuando intenta una solución que no lleva a ningún sitio, retrocede deshaciendo el último paso, e intentando una nueva variante desde esa posición (es normalmente de naturaleza recursiva).

El proceso general de los algoritmos de vuelta atrás se contempla como un método de prueba y búsqueda, que gradualmente construye tareas básicas y las inspecciona para determinar si conducen a la solución del problema. Si una tarea no conduce a la solución, prueba con otra tarea básica. Es  una prueba sistemática  hasta   llegar  a   la  solución,  o  bien  determinar  que  no hay solución por  haberse agotado todas las opciones que probar.

La característica principal de los algoritmos de vuelta atrás es intentar realizar pasos que se acercan cada vez más a la solución completa. Cada paso es anotado, borrándose tal anotación si se determina que no conduce a la solución, esta acción constituye la vuelta atrás. Cuando se produce una vuelta atrás  se ensaya otro paso (otro movimiento).  En definitiva,  se prueba sistemáticamente con todas las opciones posibles hasta encontrar una solución, o bien agotar todas las posibilidades sin llegar a la solución.

Esquema general de este método:

EnsayarSolucion{

Inicializar cuenta de opciones de selecciónrepetir{

Seleccionar nuevo paso hacia la soluciónsi válido {

Anotar el pasosi (no completada solución)

EnsayarSolucion a partir del nuevo pasosi (no alcanza solución completa)

Borrar anotación}

} hasta (completada solución) o (no más opciones)}

Este esquema puede tener  variaciones.  En cualquier caso siempre habrá  que adaptarlo a  la casuística del problema a resolver.

3.1 Problema de dar el Cambio.Supongamos que el cajero sólo tiene billetes de 2000, 5000 y 10000 y nos debe dar 21000 pts. 

Con una estrategia voraz nunca llegaría a la solución correcta (daría 2 de 10000 y no podría dar el resto), mientras que con vuelta atrás podemos ir dando billetes (empezando por el mayor valor posible) y si llegamos a una combinación sin solución, volvemos atrás intentándolo con el segundo billete más 

                                                                                                                                         2­29

Page 30: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

grande y así sucesivamente.

Implementación en JAVA

public class DarCambioVueltaAtras {private int[] C;     // Conjunto de Monedasprivate int[] D;     // Conjunto de cantidades de monedasprivate int[] S;     // Conjunto de soluciones

   public int[] getSolucion() {     return S;   }

   public DarCambioVueltaAtras(int[] c, int[] d) {     C = c;     D = d;   }

public boolean devCambio(int cambio) {int i = 0;int paso;boolean objetivo = false;

// Inicializar el conjunto resultado S.      // Sus campos deben estar a cero (Java ya lo hace)

S = new int[C.length];

// Llegar al primer C[i] válido según “cambio”while ( ( i  < C.length ) 

              &&    ( C[i] > cambio ) ){

++i;}

// Bucle principalwhile ( ( i < C.length )    &&  ( !objetivo ) ) {

// Hay suficientes monedas if ( D[i] > 0 ) 

                  {// El siguiente paso es esa monedapaso = cambio ­ C[i];­­D[i]; // Queda una menos

// Hacer el pasoif ( paso == 0 ) {

// Es el fín !objetivo = true;++S[i];

}else {

objetivo = devCambio( paso );

                                                                                                                                         2­30

Page 31: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

if ( objetivo ) {++S[i];

}else ++D[i]; // Al final no la usamos

}

}++i;

}

return objetivo;

}}

class Ppal {static public void main(String[] args) {

     // Preparar los conjuntos     int[] C = { 10000, 5000, 2000 };  // Monedas     int[] D = {     0,    1,   10 };  // Cantidades de monedas     int[] S;                 // Solución

     // Preparar la ejecución     DarCambioVueltaAtras calc = new DarCambioVueltaAtras( C, D );

// Llamar al algoritmoif ( !calc.devCambio( 21000 )) 

System.out.println( "No hay solución" );else {

        S = calc.getSolucion();

// Visualizar el resultadofor (int i = 0; i < S.length; ++i) {     if ( S[i] > 0 ) {

System.out.println( S[i] + " de " + C[i] );                       }

}}

}

3.2 Problema de las 8 reinas.El problema consiste en colocar ocho reinas en un tablero de ajedrez sin que se den jaque (dos 

reinas se dan jaque si comparten fila, columna o diagonal).  Puesto que no puede haber más de una reina por fila, podemos replantear el problema como "colocar una reina en cada fila del tablero de forma que no se den jaque". En este caso, para ver si dos reinas se dan jaque basta con ver si comparten columna o diagonal. Por lo tanto, toda solución del problema se puede representar como una 8­tupla (X0,...,X7) en la que  Xi  es la columna en la que se coloca la reina que está en la fila i del tablero.

1. Representación de la información:a) Debe permitir interpretar fácilmente la solución:b) X vector [0..n­1] de enteros.  X[i] almacena la columna de una reina en la fila i­

ésima.2. Evaluación: 

                                                                                                                                         2­31

Page 32: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

a) Se utilizará  un método buenSitio que devuelva el valor verdad si la k­ésima reina se puede colocar en el valor almacenado en X[k], es decir, si está en distinta columna y diagonal que las k­1 reinas anteriores.

b) Dos reinas están en la misma diagonal  sii tienen el mismo valor de “fila + columna”, mientras que están en la diagonal sii tienen el mismo valor de “fila­columna”.(f1 –c1 = f2 – c2) ∨ (f1 + c1 = f2 + c2)⇔ (c1 – c2 = f1 – f2) ∨ (c1 – c2 = f2 – f1)⇔ |c1 – c2|= |f1 – f2|

Implementación en JAVA

class Reinas{                        // Devuelve verdad si y sólo si se puede colocar una reina en la fila j // y columna A[j], habiendo sido colocadas ya las j­1 reinas anteriores static public boolean buenSitio (int j, int[] R) {    // ¿Es amenaza colocar la reina j en A[j], con las anteriores ?   for(int i = 0; i < j; ++i) {   {        if ( ( R[i] == R[j] )          || ( Math.abs( R[i]­R[j] ) == Math.abs( i­j ) ) )         {           break;        }   }   return ( i == j ); }  static public boolean colocarReinas (int j, int[] R) {     boolean toret = false;

  // Comprobar con todas las columnas  for (int i = 0; i < R.length; ++i)  {      // Colocar la reina j en la columna i      R[j] = i;           if ( buenSitio( j, R ) )      {            // Si j es N­1 he colocado todas las reinas            if ( j == R.length ­1 ) {                toret = true;

    break;            }            else {                // Hacer el siguiente paso recursivamente                // (colocar la siguiente reina)                if ( colocarReinas( j + 1, R ) ) {                    toret = true;

  break;                }

                                                                                                                                         2­32

Page 33: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

           }      }   }      return toret;} public static void main (String  args[]){      // En Reinas[i] se almacena la columna donde está i      int[] reinas = new int[8];          if ( colocarReinas( 0, reinas ) )      {

for (int i= 0; i < R.length; ++i) {         System.out.println( "  Reina " + i 

      + “ en la fila “ + i      + ", columna " + R[i] 

                  );}

      }      else System.out.println( "No hay solución\n" ); }}

3.3 Problema de Atravesar un Laberinto.Nos encontramos en un entrada de un laberinto y debemos intentar atravesarlo. Dentro del algoritmo nos encontraremos con muros que no podremos atravesar, sino que habrá que rodear, lo que hará que nos   encontremos   a   veces   en   un   callejón   sin   salida.   Es   necesario   tener   en   cuenta   las   siguientes condiciones:

1. Representación: Array N x N, de casillas marcadas como libre u ocupada por una pared.2. Es posible pasar de una casilla a otra moviéndose solamente en vertical u horizontal.3. El problema se soluciona cuando desde la casilla (0,0) se llega a las casilla (n­1, n­1).

Para resolver este problema se diseñará un algoritmo de búsqueda con retroceso de forma que se marcará en la misma matriz del laberinto un camino solución si existe.

Si por un camino recorrido se  llega a una casilla desde la que es  imposible encontrar una solución, hay que volver atrás y buscar otro camino.

Además hay que marcar las casillas por donde ya se ha pasado para evitar meterse varias veces por el mismo callejón sin salida, dar vueltas alrededor de columnas....

El pseudocódigo de dicho algoritmo podría ser el siguiente:

/*PosicionX e PosicionY, indican la casilla en la que estoy dentro de la matriz Laberinto.En una posición del laberinto pueden almacenarse los valores Libre,

que indica que una casilla está Libre, Pared quiere decir que esa casilla hay una pared, Camino quiere decir que esa casilla forma parte del camino que se está recorriendo,

e Imposible quiere decir que esa casilla no conduce a la solución*/

                                                                                                                                         2­33

Page 34: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

boolean ensayar (posicionX, posicionY, Matriz Laberinto) { si ((posicionX < 0) o (posicionX > n-1) o (posicionY < 0) o (posicionY > n-1)) {

devuelve falso } sino {

si (Laberinto[posicionX][posicionY] es distinto de “Libre”)devuelve falso

sino { Laberinto[posicionX][posicionY] es igual a “Camino”;

si ((posicionX es igual a n-1) y (posicionY es igual a n-1))//Se ha encontrado la solucióndevuelve verdadero

sino {//desplazarse en vertical u horizontal// por las otras casillassi (no Ensayar(posicionX+1, posicionY, Laberinto) si (no Ensayar(posicionX, posicionY+1, Laberinto) si (no Ensayar(posicionX-1, posicionY, Laberinto) si (no Ensayar(posicionX,posicionY-1, Laberinto) {

Laberinto[posiconX][posicionY] <- “Imposible” devolver falso;

}devolver verdadero;

} } }}

Implementación en JAVA

public class Laberinto {

  private char L[][];

  public static final char camino    = 'c';  public static final char obstaculo = 'X';  public static final char imposible = '*';  public static final char libre     = ' ';    public laberinto(char[][] matrLab)  {     L = matrLab;  }

  public char[][] getLaberinto()  {   return L;  }

  

                                                                                                                                         2­34

Page 35: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

 public void visualizaLaberinto() {   for (int i = 0; i<L.length; ++i) {     for(int j = 0; j<L.length; ++j) {        System.out.print(L[i][j] + " ");

}

     System.out.println();   }  }

  public boolean ensayar(int posicionX, int posicionY)   {   int n = L.length; // Tamaño del laberinto n x n   boolean toret;

   // estamos dentro del laberinto ?if ( ( posicionX < 0 )

       || ( posicionX > n­1 )     || ( posicionY < 0 )     || ( posicionY > n­1 ) )

{toret = false;

}else{

     // No se puede pasar por encima de los obstáculosif ( L[posicionX][posicionY] != libre ) {

toret = false;}else{

        // Marca como parte del camino        L[posicionX][posicionY] = camino;

        // Quizás ya es la soluciónif ( ( posicionX == n­1 ) 

          && ( posicionY == n­1 ) ){

toret = true; //Se ha encontrado la solución}else {

           // en principio, hay solución           toret = true;

//hay que desplazarse en vertical            //u horizontal por las otras casillas

if ( !ensayar( posicionX+1, posicionY ) ) if ( !ensayar( posicionX, posicionY+1 ) )  if ( !ensayar( posicionX­1, posicionY ) )   if ( !ensayar( posicionX, posicionY­1 ) ) {

   L[posicionX][posicionY] = imposible;   toret = false;   }

}}

   }  

                                                                                                                                         2­35

Page 36: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

   return toret;  }}

class Ppal {  public static void main(String args[]) {     boolean haysolucion;

     // Crear un laberinto     char lab[][] = {

// Fila 1{ laberinto.libre,  laberinto.libre,  laberinto.libre,  laberinto.libre},// Fila 2{ laberinto.obstaculo,  laberinto.obstaculo,

                    laberinto.libre,                    laberinto.obstaculo

},// Fila 3

  { laberinto.libre,    laberinto.libre,

  laberinto.libre,   laberinto.obstaculo},// Fila 4

  { laberinto.libre,  laberinto.obstaculo,  laberinto.libre,   laberinto.libre}

        };         Laberinto calcLab = new Laberinto( lab );

      // Mostrar problema inicial      calcLab.visualizaLaberinto();

     // Encontrar la solución     if ( calcLab.ensayar( 0, 0 ) )        System.out.println( "Se encontró una solución\n\n" );        else  System.out.println( "No se encontró una solución\n\n" );

     // Visualizar solución (o lo que sea)        calcLab.visualizaLaberinto();  }}

                                                                                                                                         2­36

Page 37: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

4 Programación dinámica.La idea de la técnica "divide y vencerás" es llevada al extremo en la programación dinámica. Si en el proceso de resolución del problema resolvemos un subproblema, almacenamos la solución, por si esta puede ser necesaria de nuevo para la resolución del problema. Estas soluciones parciales de todos los subproblemas   se   almacenan   en   una   tabla   sin   tener   en   cuenta   si   van   a   ser   realmente   necesarias posteriormente en la solución total.

Con el uso de esta tabla se evitan hacer cálculos idénticos reiteradamente, mejorando así la eficiencia en la obtención de la solución.

4.1 Cálculo de los n primeros números primos.Este problema se puede resolver empleando un esquema de programación dinámica. Se almacena cada número primo encontrado en una tabla y se divide cada nuevo número sólo por los que hay en la tabla (y no por todos los menores que él) para saber si es primo:

Implementación en JAVA

class PrimosDinamica{

 public static void buscaPrimos(int n) {     int[] Tabla     =  new  int[n];  //  Se  almacenan los números  primos ya calculados   Tabla[0]     = 2;          // Primer número primo   int nPrimos  = 1;          // Contador de números primos

   // Mientras no encuentre 100 números primos    for (int i = 3; nPrimos < n; ++i) {     int j = 0;        // Mientras que los números primos que están almacenados      // proporcionen un resto distinto de 0 ...

while ( ( j < nPrimos )     && ( i % Tabla[j] != 0 ) ){     ++j;}

// Se han comprobado todos los números primos e i no es// divisible por ninguno de ellos.if ( j == nPrimos ){     Tabla[nPrimos] = i;     ++nPrimos;}

   }

   // Se visualiza el resultado   for (int i = 0; i < n; ++i) {     System.out.print( "Numero primo :" + Tabla[i] + "       " );   }  }

                                                                                                                                         2­37

Page 38: mtpTema2

Capítulo 2: Esquemas Algorítmicos Fundamentales

    public static void main (String  args[])  { 

// Buscar los 100 primeros números primosbuscaPrimos( 100 );

  }}

                                                                                                                                         2­38