Problema partición

Anuncio
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
Descargar