Problema partición Enunciado A: dados N enteros positivos, N>1, decir si estos pueden dividirse en dos grupos cuya suma sea la misma. Ejemplo: si el conjunto es {1,20,3,9,2,11,4}, una forma de partirlo en dos grupos con suma igual es {20,3,2} y {11,4,9,1}. Dos observaciones interesantes son: 1. Si la suma de todos los números es impar, el problema no tiene solución 2. Si la suma de todos los números es K, un número par, bastará con saber si hay UN subconjunto cuya suma sea K/2. La segunda observación permite reformular el problema de una manera más simple: Enunciado alternativo: dados N enteros positivos, establecer si de estos se puede sacar un subconjunto cuya suma sea K/2. Este enunciado es más simple porque se trata de encontrar un conjunto y no dos. Para lo que sigue en este documento M es K/2 y el conjunto de enteros positivos es A={k0,k1,k2,...,kN-1} Los algoritmos solución se pueden diseñar de varias formas. A continuación de presentan algunas: Solución exhaustiva o de fuerza bruta Básicamente consiste en generar todos los subconjuntos posibles y verificar si alguno de ellos cumple que sus elementos suman M. Se usará una cadena binaria b0,b1,b2,...,bN-1 para representar cada uno de los subconjuntos de A. La convención de la representación es que si b j es 1, entonces k j está en el subconjunto representado. La siguiente tabla ilustra la biyección entre cadenas binarias de longitud 3 y subconjuntos de A={9,7,2}. Cadena binaria 000 001 010 011 100 101 110 111 Subconjunto representado ∅ {2} {7} {7,2} {9} {9,2} {9,7} {9,7,2} La generación exhaustiva de los subconjuntos de A se reduce a generar todas las cadenas binarias de longitud |A|. La verificación de uno cualquiera de estos subconjuntos consiste simplemente en establecer si la suma de sus elementos es M. Los algoritmos correspondientes son los siguientes: 1 PROCEDIMIENTO generar(ENT_SAL elegidos: arreglo [] de entero; ENT N: entero) (* OBJ: calcular el siguiente subconjunto para el problema PARTICIÓN, es decir, la siguiente cadena binaria PRE: elegidos[0..N-1] es un vector de ceros y unos. Hay por lo menos un cero POS: elegidos CODIFICA el siguiente subconjunto *) VARIABLES k:entero k ← N-1; MQ (elegidos[k]=1) haga // recorre la cadena de derecha a izquierda ... elegidos[k] ← 0 // mientras haya unos k ← k-1 FinMQ elegidos[k] ← 1; //... una vez encuentra un 0,lo cambia a 1 FinPROCEDIMIENTO FUNCION verificar(ENT cifras: arreglo [] de entero; ENT N,meta: entero; ENT elegidos: arreglo [] de entero):booleano (* OBJ: verificar si el SUBCONJUNTO codificado en elegidos[0..N-1] es solución PRE: cifras[k]>0, para todo k tal que 0<=k<N. POS: retorna True, si el SUBCONJUNTO codificado es solución. Retorna False, en otro caso *) VARIABLES suma,i:entero suma ← 0 i ← 0 MQ (i<N) haga SI(elegidos[i]=1) suma ← suma + cifras[i] FinSI i ← i+1 FinMQ Devolver (suma=meta) FinFUNCION Ahora, el algoritmo principal que soluciona el problema es el siguiente: FUNCION solEx(ENT cifras: arreglo [] de entero; ENT N,meta: entero; ENT_SAL elegidos: arreglo [] de entero):booleano (* OBJ: obtener la solución del problema PARTICION por método exhaustivo PRE: cifras[k]>0, para todo todo k tal que 0<=k<N. POS: retorna True, si encuentra una solución. Retorna False en otro caso *) VARIABLES cont, posibles: entero exito: booleano elegidos ← 0 //inicializa el vector elegidos en cero. exito ← False cont ← 1 posibles ← 2N MQ (¬exito ∧ cont<posibles) generar(elegidos,N) exito ← verificar(cifras,N,meta,elegidos) cont ← cont+1 FinMQ Devolver exito FinFUNCION Note que, en caso de haber solución, este algoritmo deja en elegidos[0..N-1] la codificación de una partición apropiada. 2 Solución recursiva (divide y vencerás) Básicamente consiste en expresar la solución del problema original como una combinación de las soluciones de algunos problemas más pequeños, de la misma naturaleza del original. El problema original habla de saber si hay un subconjunto de A={k0,k1, k2,...,kN-1} que sume un número objetivo M. La búsqueda de la solución se puede pensar como una secuencia de decisiones D0 D1 ...DN-1, tal que en la decisión D j se resuelve si el número k j se incluye o no en el subconjunto buscado. La siguiente figura muestra cómo cambiarían las dos variables del problema después de tomar la decisión D0. {k0,k1, k2,...,kN-1} , M k0 va en la solución {k1, k2,...,kN-1} , M-k0 k0 no va en la solución {k1, k2,...,kN-1} , M El rectángulo de la parte superior de la figura muestra las variables del problema en su estado inicial: el conjunto de elementos sobre los cuales no se ha tomado ninguna decisión y el número objetivo inicial, M. Los dos rectángulos de la parte inferior muestran lo que puede suceder después de tomar decisión sobre k0. En el rectángulo de la izquierda están las variables del problema después de resolver que k0 está en el conjunto solución. En el rectángulo de la derecha están las variables del problema después de resolver que k0 no está en el conjunto solución. Note que CUALQUIER solución potencial incluye a k0 en el subconjunto o no lo incluye. (no existen otras opciones para k0.) Los tres rectángulos muestran estados del mismo problema. Además, los dos de la parte inferior representan instancias “más pequeñas” porque el conjunto de números es más pequeño y/o el número objetivo es menor. Observe que si se resuelven los dos problemas que pueden resultar de tomar la decisión D0, sería fácil usar estas dos soluciones para dar respuesta al problema original: bastaría calcular la disyunción de las soluciones obtenidas. Ahora, las dos soluciones necesarias se pueden obtener recursivamente dado que los dos subproblemas son de la misma naturaleza del original, es decir son versiones o instancias más pequeñas del original Si se establece que partir(A,S) expresa si hay o no un subconjunto de A cuya suma es S, la siguiente definición recursiva formaliza la discusión anterior partir({k0,k1, k2,...,kN-1},M) ≡ partir({k1,k2,...,kN-1},M-k0) ∨ partir({k1,k2,...,kN-1},M) 3 Enseguida se da la definición recursiva completa, incluidos los casos de base de la recursión. Además, esta se refiere a una decisión cualquiera Dj (0≤j<N), y el conjunto se reemplaza por el índice del elemento sobre el cual se toma decisión. partir(j,S) expresa si hay o no un subconjunto de {kj,kj+1,...,kN-1} cuya suma es S partir(j,S) partir(j,S) partir(j,S) partir(j,S) ≡ ≡ ≡ ≡ False si j=N [caso en el que el conjunto queda vacío] True si j<N y S=0 [caso en el que el número objetivo es 0] partir(j+1,S) si j<N y kj>S partir(j+1,S-kj) ∨ partir(j+1,S) si j<N y kj<S La anterior definición se traduce de manera directa en un algoritmo recursivo. FUNCION partir(ENT cifras: arreglo [] de entero; ENT j,N,meta: entero; ENT_SAL elegidos: arreglo [] de entero): booleano (* OBJ: obtener la solución del problema PARTICION por método recursivo PRE: 0<=j<=N. cifras[k]>0, para todo 0<=k<N. meta>=0. POS: retorna True si hay solución. Retorna False en otro caso. Si hay solución, elegidos[0..N-1] CODIFICA un subconjunto solución mediante una cadena binaria *) VARIABLES temp: booleano SI(j=N) (* no hay más números en el conjunto *) Devolver False FinSI SI (meta=0) Devolver True FinSI SI (cifras[j]>meta) (* el número sobre el cual se está decidiendo es mayor que el objetivo *) Devolver partir(cifras,j+1,N,meta,elegidos) FinSI elegidos[j] ← 1 (* registra que el número cifras[j] va en el subconjunto solución *) temp ← partir(cifras,j+1,N,meta-cifras[j],elegidos) SI (temp) Devolver True FinSI elegidos[j] ← 0 (* registra que el número cifras[j] no va en el subconjunto solución *) Devolver partir(cifras,j+1,N,meta,elegidos) FinFUNCION Note que, en caso de haber solución, este algoritmo deja en elegidos[0..N-1] la codificación de una partición apropiada. POR HACER: i) estimar la complejidad temporal de este algoritmo y compararlo contra la del algoritmo exhaustivo, y ii) verificar si este algoritmo repite o hace cálculos innecesarios. Solución de programación dinámica Básicamente consiste en diseñar un proceso iterativo que calcule la ecuación recursiva obtenida, y lo haga de una manera más rápida. La ecuación en cuestión es: partir(j,S) expresa si hay o no un subconjunto de {kj,kj+1,...,kN-1} cuya suma es S 4 partir(j,S) partir(j,S) partir(j,S) partir(j,S) ≡ ≡ ≡ ≡ False si j=N True si j<N y S=0 partir(j+1,S) si j<N y kj>S partir(j+1,S-kj) ∨ partir(j+1,S) si j<N y kj<S Normalmente, la técnica de programación dinámica requiere la definición de estructuras de datos adicionales que sirvan para almacenar algunos resultados intermedios con el objeto de evitar que se repitan cálculos o que se hagan algunos innecesarios. Recuerde que el problema se trata de saber si hay un subconjunto de A={k0,k1, k2,...,kN-1} cuyos elementos sumen un número objetivo M. Lo primero que se recomienda, para obtener una solución de programación dinámica es hacer el diagrama de necesidades (o invariante) para la recurrencia partir(j,S): M S S-kj 0 j j+1 N En este caso, el plano cartesiano ilustra que el valor de la función booleana (recurrente) partir en (j,S) se puede calcular fácilmente si se tiene calculada la recurrencia en los puntos (j+1,S) y (j+1,S-kj). Dicho de otra forma, para calcularla en el punto (j,S) se necesita o se requiere tenerla calculada en los puntos (j+1,S) y (j+1,S-kj). Ahora, si los cálculos se guardan en una gran matriz de dimensión (M+1)x(N+1), de tipo booleano, entonces esta debería llenarse de derecha a izquierda y de abajo hacia arriba, como se ilustra enseguida, con excepción de la primera fila y la última columna: 0 M 0 0 S 0 0 0 S-kj 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 0 0 j j+1 N 5 Si se sigue el orden de cálculo mencionado, en el momento en que se quiere calcular el valor de la celda (S,j) de la matriz (marcada con líneas horizontales) ya estarán calculadas las celdas necesarias (casillas rayadas diagonalmente), también estarán calculadas todas las casillas sombreadas, y faltarán por calcular las casillas blancas. El resultado estará en la casilla (M,0), dado que allí estará el valor de partir(0,M) que, según la definición, dice si hay o no un subconjunto de {k0,k1,...,kN-1} cuya suma es M. El siguiente algoritmo define la matriz y la llena en el orden establecido arriba. FUNCION solProgDin(ENT cifras: arreglo [] de entero; ENT N,M: entero): booleano (* OBJ: obtener la solucion del problema PARTICION por programación dinámica PRE: cifras[k]>0, para todo k>=0 y k<N. N<MAX y M<MAX POS: retorna True, si encuentra una solución. retorna False en otro caso *) VARIABLES mat: matriz[MAX][MAX] de booleano s,j: entero PARA s←0 HASTA M HACER mat[s][N] ← False FinPARA PARA j←0 HASTA N HACER mat[0][j] ← True FinPARA PARA(j←N-1 HASTA 0 HACER PARA s←1 HASTA M HACER SI (cifras[j]>s) mat[s][j] ← mat[s][j+1] FinSI SI (cifras[j]≤s) mat[s][j] ← mat[s][j+1] ∨ mat[s-cifras[j]][j+1] FinSI FinPARA FinPARA Devolver mat[M][0] FinFUNCION POR HACER: i) estimar la complejidad temporal de este algoritmo y compararlo contra la del algoritmo exhaustivo y la del algoritmo recursivo, ii) El anterior algoritmo es mejorable en espacio: se puede usar un par de vectores de tamaño M+1, en vez de una matriz, y iii) rehaga este algoritmo para que, durante el llenado de la matriz, también se vaya haciendo el registro de una solución (arreglo elegidos) 6