Sunteți pe pagina 1din 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
Capítulo 2: Esquemas Algorítmicos Fundamentales

                                                                                                                                         2­2
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 devoradores
Estos 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ón
Por 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 T
Procesar T (reduciéndose D)

                                                                                                                                         2­3
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 inicial
int 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
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ón
s  ← 0  // Es la suma de los elementos de S
mientras ( s != n )
{
x ← el mayor elemento de C tal que s + x <= n
si 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
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 S
sino 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
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
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 disponibles
while ( parcial < obj
       && i < C.length ) 
{       
// buscar una moneda que “quepa”
k = obj – parcial;
if ( i < C.length
         &&  C[i] <= k )

// se encontró una moneda
k /= 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
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 
n
consiste en determinar las cantidades c1, ..., cn  que llenan la mochila maximizando el valor   ∑ vipi
i= 1
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
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
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 coste
repetir
Seleccionar un nuevo tramo
hasta 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
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
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
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ícitamente
para (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(n 2). 
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
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
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
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
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 
n
=  ∑  tiempo en el sistema para el cliente i)
i= 1
Supongamos por ejemplo que tenemos tres clientes, con t1 = 5, t2 = 10 y t3 = 3. Existen seis órdenes 
de servicio posibles:

Orden T
123: 5 + (5+10) + (5+10+3) = 38
132: 5 + (5+3) + (5+3+10) = 31
213: 10 + (10+5) + (10+5+3) = 43
231: 10 + (10+3) + (10+3+5) = 41
312: 3 + (3+5) + (3+5+10) = 29
321: 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.

El algoritmo voraz es óptimo

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
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ón
Como   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
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á ordenado
sino
Dividir v en dos subvectores A y B
Ordena A y B usando Merge Sort
Mezclar 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á ordenado
sino
Dividir v en dos bloques A y B
Con todos los elementos de A menores que los de B
Ordenar A y B usando Quick sort
Devolver 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:
s
• u = 10  w+x
s s s
• v = 10  y+z con 0 <= x < 10  , 0 < = z < 10  y s = n/2
• Por tanto w e y tienen n/2 cifras
• El producto es:

                                                                                                                                         2­20
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 = wy
q = xz 
r = (w+x) (y+z) 
102s p + 10s (r­p­q) + q

                                                                                                                                         2­21
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
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
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
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
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
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
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
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ón
repetir
{
Seleccionar nuevo paso hacia la solución
si válido 
{
Anotar el paso
si (no completada solución)
EnsayarSolucion a partir del nuevo paso
si (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
Capítulo 2: Esquemas Algorítmicos Fundamentales

grande y así sucesivamente.

Implementación en JAVA
public class DarCambioVueltaAtras {
private int[] C;     // Conjunto de Monedas
private int[] D;     // Conjunto de cantidades de monedas
private 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 principal
while ( ( i < C.length )
    &&  ( !objetivo ) ) 
{
// Hay suficientes monedas 
if ( D[i] > 0 ) 
                  {
// El siguiente paso es esa moneda
paso = cambio ­ C[i];
­­D[i]; // Queda una menos

// Hacer el paso
if ( paso == 0 ) {
// Es el fín !
objetivo = true;
++S[i];
}
else {
objetivo = devCambio( paso );

                                                                                                                                         2­30
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 algoritmo
if ( !calc.devCambio( 21000 )) 
System.out.println( "No hay solución" );
else {
        S = calc.getSolucion();

// Visualizar el resultado
for (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
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
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
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ón
devuelve verdadero
sino {
//desplazarse en vertical u horizontal
// por las otras casillas
si (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
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áculos
if ( L[posicionX][posicionY] != libre ) {
toret = false;
}
else
{
        // Marca como parte del camino
        L[posicionX][posicionY] = camino;

        // Quizás ya es la solución
if ( ( 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
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
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
Capítulo 2: Esquemas Algorítmicos Fundamentales

  
  public static void main (String  args[])
  { 
// Buscar los 100 primeros números primos
buscaPrimos( 100 );
  }
}

                                                                                                                                         2­38

S-ar putea să vă placă și