Icpc 1
Icpc 1
Icpc 1
Programación
Competitiva
3 Estructura de datos 1 53
3.1 Introducción .................................................................... 53
3.1.1 ¿Qué es una estructura de datos? . . . . . . . . . . . . . . . . . . . 53
3.1.2 Genericidad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
4 Programación modular 75
4.1 Introduccion .................................................................... 75
4.1.1 ¿Qué es la Programación Modular? . . . . . . . . . . . . . . . . . 75
5 Matemáticas 83
5.1 Introducción .................................................................... 83
5.2 Series o sucesiones ....................................................... 83
5.2.1 Tipos de Series . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
5.2.1.1 Series aritméticas . . . . . . . . . . . . . . . . . . . . . . . 83
5.2.1.2 Series geométricas . . . . . . . . . . . . . . . . . . . . . . 84
5.2.1.3 Series Especiales . . . . . . . . . . . . . . . . . . . . . . . 84
6 Algoritmos Basicos 95
6.1 Algoritmos de Ordenanmiento .................................. 95
6.1.1 Ordenamiento Rapido . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
6.1.1.1 Descripcion del Algoritomo . . . . . . . . . . . . . . . . 95
6.1.2 Ordenamiento por mezcla . . . . . . . . . . . . . . . . . . . . . . . . . . 96
6.1.2.1 Descripcion del Algoritomo . . . . . . . . . . . . . . . . 96
6.1.3 Ordenamiento por Monticulos . . . . . . . . . . . . . . . . . . . . . . . 97
6.1.3.1 Descripcion del Algoritmo . . . . . . . . . . . . . . . . . 97
6.1.4 Ordenamiento por Cuentas . . . . . . . . . . . . . . . . . . . . . . . . . 98
6.1.4.1 Descripcion del Algortimo . . . . . . . . . . . . . . . . . 98
8 Grafos 115
8.1 Introducción .................................................................. 115
8.1.1 ¿Qué es un Grafo? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
8.1.2 ¿Para que sirven los grafos? . . . . . . . . . . . . . . . . . . . . . . . . 115
8.2 Representaciones de un grafo ................................ 116
8.2.1 Matriz de adyacencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
8.2.2 Lista de adyacencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
8.2.3 Lista de arcos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
8.3 Recorrido de un grafo ................................................ 120
8.3.1 BFS (Breadth First Search) . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
8.3.2 DFS (Depth First Search) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 122
9 Algoritmos ++ 127
9.1 Árbol de expansión mínima ..................................... 127
9.1.1 Algoritmo de Kruskal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127
9.2 Camino más corto ...................................................... 129
9.3 Puntos de Articulación y Puentes ........................... 131
9.4 Componentes Conexas ............................................. 132
1.1 Introducción
1.1.1 ¿Qué es un Lenguaje de Programación?
Antes de hablar de C++, es necesario explicar que un lenguaje de programación es una herramienta
que nos permite comunicarnos e instruir a la computadora para que realice una tarea específica. Cada
lenguaje de programación posee una sintaxis y un léxico particular, es decir, forma de escribirse que
es diferente en cada uno por la forma que fue creado y por la forma que trabaja su compilador para
revisar, acomodar y reservar el mismo programa en memoria.
Existen muchos lenguajes de programación de entre los que se destacan los siguientes:
• C
• C++
• Basic
• Ada
• Java
• Pascal
• Python
• Fortran
• Smalltalk
2. Un compilador de C++
• Windows MingW (GCC para Windows) o MSVC (compilador de Microsoft con versión
gratuita)
• Linux (u otros UNIX): g++
• Mac (con el compilador Xcode)
3. Un editor cualquiera de texto, o mejor un entorno de desarrollo (IDE)
• Windows:
– Microsoft Visual C++ (conocido por sus siglas MSVC). Incluye compilador y posee
una versión gratuita (versión express)
– Bloc de notas (no recomendado)
– Editor Notepad++
– DevCpp (incluye MingW - en desuso, no recomendado, incluye también un compi-
lador)
– Code::Blocks
• Linux (o re-compilación en UNIX):
– Gedit
– Kate
– KDevelop
– Code::Blocks
– SciTE
– GVim
• Mac:
– Xcode (con el compilador trae una IDE para poder programar)
4. Tiempo para practicar
5. Paciencia
Adicional
• Inglés (Recomendado)
• Estar familiarizado con C u otro lenguaje derivado (PHP, Python, etc).
Es recomendable tener conocimientos de C, debido a que C++ es una mejora de C, tener los
conocimientos sobre este te permitirá avanzar más rápido y comprender aún mas. También, hay
que recordar que C++, admite C, por lo que se puede programar (reutilizar), funciones de C que se
puedan usar en C++.
Aunque no es obligación aprender C, es recomendable tener nociones sobre la programación
orientada a objetos en el caso de no tener conocimientos previos de programación estructurada.
Asimismo, muchos programadores recomiendan no saber C para saber C++, por ser el primero de
ellos un lenguaje imperativo o procedimental y el segundo un lenguaje de programación orientado a
objetos.
1.1.5 Ejemplo
10 // Este tipo de líneas de código que comienzan por '//' son comentarios
11 // El compilador los omite, y sirven para ayudar a otros programadores o
12 // a uno mismo en caso de volver a revisar el código
13 // Es una práctica sana poner comentarios donde se necesiten,
14
15 std :: cout << " Hola Mundo " << std :: endl ;
16
17 // Mostrar por std::cout el mensaje Hola Mundo y comienza una nueva línea
18
19 return 0;
20
21 // se devuelve un 0.
22 //que en este caso quiere decir que la salida se ha efectuado con éxito.
23 }
Mediante simple inspección, el código parece enorme, pero el compilador lo único que leerá para la
creación del programa es lo siguiente:
Como se puede observar, este código y el original no difieren en mucho salvo en los saltos de línea y
que los comentarios, de los que se detallan posteriormente, están omitidos y tan sólo ha quedado "el
esqueleto" del código legible para el compilador. Para el compilador, todo lo demás, sobra.
Aquí otro ejemplo
Para hacer el código más corto debemos incluir using namespace std con lo cual ya no necesitaremos
incluir cada vez que lo necesitemos, como en los objetos cout y cin, que representan el flujo de salida
estándar (típicamente la pantalla o una ventana de texto) y el flujo de entrada estándar (típicamente
el teclado).
Se vería de la siguiente manera:
4
5 int main ()
6 {
7 cout << " Hola Mundo " << endl ;
8 cout << " Hello World " << endl ;
9 cout << " Hallo Welt " << endl ;
10 return 0;
11 }
Los pasos siguientes son para una compilación en GNU o sistema operativo Unix, para generar el
ejecutable del programa se compila con g++ de la siguiente forma:
Para poder ver los resultados del programa en acción, se ejecuta el programa de la siguiente forma:
1 |./ hola
1 Hola Mundo
1.2.2 Sintaxis
Sintaxis es la forma correcta en que se deben escribir las instrucciones para el computador en un
lenguaje de programación específico. C++ hereda la sintaxis de C estándar, es decir, la mayoría de
programas escritos para el C estándar pueden ser compilados en C++.
1.2.2.3 Comentarios
Existen dos modos básicos para comentar en C++:
// Comentan solo una línea de código
/*Comentario*/ Comentan estrofas de código
A continuación un ejemplo:
Los valores dependen de la arquitectura utilizada. Los mostrados son los que generalmente se
encuentran en una máquina típica de arquitectura 32 bits.
El Modificador unsigned
1.2 Lo mas básico 21
El modificador unsigned es utilizado únicamente con los enteros, su utilización permite utilizar en
los enteros únicamente la parte positiva,
1 int a ;
2 // Almacena valores entre -2,147,483,648 a 2,147,483,647
3 unsigned int a ;
4 // Almacena valores entre 0 a 4,294,967,295
A diferencia de las constantes declaradas con la palabra const los símbolos definidos con #define no
ocupan espacio en la memoria del código ejecutable resultante.
El tipo de la variable o constante puede ser cualquiera de los listados en Tipos primitivos, o bien de
un tipo definido por el usuario.
Las constantes son usadas a menudo con un doble propósito, el primero es con el fin de hacer más
legible el código del programa, es decir, si se tiene (por ejemplo) la constante numérica 3.1416, esta
representa al número pi, entonces podemos hacer declaraciones tales como:
1 # define pi 3.1416
En este caso podremos usar la palabra pi en cualquier parte del programa y el compilador se encargará
de cambiar dicho símbolo por 3.1416 o bien,
1 const pi = 3.1416;
En este otro caso podremos usar la palabra pi en cualquier parte del programa y el compilador se
encargará de cambiar dicho símbolo por una referencia a la constante pi guardada en la memoria.
Para el manejo de cin y cout se necesita también el uso de los operadores de direccionamiento
< < y > >. Los operadores de direccionamiento son los encargados de manipular el flujo de datos
desde o hacia el dispositivo referenciado por un stream específico. El operador de direccionamiento
para salidas es una pareja de símbolos de "menor que" < <, y el operador de direccionamiento
para entradas es una pareja de símbolos de "mayor que" > >. Los operadores de direccionamiento
se colocan entre dos operandos, el primero es el Stream y el segundo es una variable o constante
que proporciona o recibe los datos de la operación. Por ejemplo, en el siguiente programa y en la
instrucción cout < < "Entre su nombre: "; la constante "Entre su nombre: " es la fuente o quien
proporciona los datos para el objeto cout. Mientras que en la instrucción cin > > nombre la variable
nombre es el destino o quien recibe los datos provenientes del objeto cin.
14 }
Observe que si en una misma línea de comando se desea leer o escribir sobre varios campos a la vez,
no es necesario nombrar más de una vez al stream. Ejemplos:
Consola:
2 Ahora estoy fraccionado en el codigo , pero en la consola me muestro como una unica
frase .
3 Un gran texto puede ocupar muchas lineas .
4 Pero eso no frena al programador a que todo se pueda poner en una unica linea de
codigo y que
5 el programa , al ejecutarse , lo situe como el programador quiso
Identificador Salida
%c Carácter o entero pequeño
%s Tira de caracteres
%d, %i Entero decimal
%u Entero decimal sin signo
%lld Entero largo
%llu Entero largo sin signo
%f Número de punto flotante en notación decimal
%lf Número de punto flotante en notación decimal con doble presición
%o Entero octal sin signo
%x Entero hexadecimal sin signo
Veamos ahora cómo se utilizan. El programa siguiente emplea algunos de los identificadores que
acabamos de ver:
La salida es:
1 printf ( " Los % d jovenes tomaron % d helados .\ n " , numero , numero *2) ;
Control sería la frase entre comillas (después de todo, es una tira de caracteres), y numero y
numero*2 serían los items; en este caso, los valores de dos variables.
También se debe hablar de modificadores de especificaciones de conversión en printf(), estos son
apéndices que se agregan a los especificadores de conversión básicos para modificar la salida. Se
colocan entre el símbolo % y el carácter que define el tipo de conversión. A continuación se da una
lista de los símbolos que está permitido emplear. Si se utiliza más de un modificador en el mismo
sitio, el orden en que se indican deberá ser el mismo que aparece en la tabla. Tenga presente que no
todas las combinaciones son posibles.
- El ítem correspondiente se comenzará a escribir empezando en el extremo izquierdo del
campo que tenga asignado. Normalmente se escribe el ítem de forma que acabe a la
derecha del campo.
Ejemplo: %-10d
número Anchura mínima del campo. En el caso de que la cantidad a imprimir (o la tira de
caracteres) no quepa en el lugar asignado, se usará automáticamente un campo mayor
Ejemplo: %4d
.número Precisión. En tipos flotantes es la cantidad de cifras que se han de imprimir a la derecha
del punto (es decir, el número de decimales). En el caso de tiras, es el máximo número de
caracteres que se ha de imprimir.
Ejemplo: %4.2f (dos decimales en un campo de cuatro caracteres de ancho)
Veamos ahora un ejemplo:
5 int main ()
6 {
7 printf ( " Modificador ' - '\ n " ) ;
8 printf ( " /% -10 d /\ n " , 365) ;
9 printf ( " /% -6 d /\ n " , 365) ;
10 printf ( " /% -2 d / " , 365) ;
11 printf ( " en este caso no sirve de nada el modificador \ n " ) ;
12 printf ( " Modificador ' numero '\ n " ) ;
13 printf ( " /%2 d / " , 365) ;
14 printf ( " en este caso tampoco hace efecto el modificador \ n " ) ;
15 printf ( " /%6 d /\ n " , 365) ;
16 printf ( " /%10 d /\ n " , 365) ;
17 printf ( " Modificador '. numero '\ n " ) ;
18 printf ( " /%.1 f /\ n " , 365.897) ;
19 printf ( " /%.6 f /\ n " , 365.897) ;
20 printf ( " /%.3 s /\ n " , " AsDfGhJkL " ) ;
21 printf ( " /%.100 s / " , " AsDfGhJkL " ) ;
22 printf ( " No se rellena con 0 ' s como en los ejemplos anteriores \ n
");
23 return 0;
24 }
La salida es:
1 Modificador '-'
2 /365 /
3 /365 /
4 /365/ en este caso no sirve de nada el modificador
5 Modificador ' numero '
6 /365/ en este caso tampoco hace efecto el modificador
7 / 365/
8 / 365/
9 Modificador '. numero '
10 /365.9/
11 /365.897000/
12 / AsD /
13 / AsDfGhJkL / No se rellena con 0 's como en los ejemplos anteriores
Para el uso de scanf() se utilizan los mismo identificadores que en printf(), y al igual que printf(),
scanf() emplea una tira de caracteres de control y una lista de argumentos. La mayor diferencia
entre ambas está en esta última; printf() utiliza en sus listas nombres de variables, constantes y
expresiones; scanf() usa punteros a variable. Afortunadamente no se necesita saber mucho de
punteros para emplear esta expresión; se trata simplemente de seguir las dos reglas que se dan a
continuación:
• Si se desea leer un valor perteneciente a cualquier de los tipos básicos coloque el nombre de la
variable precedido por un &.
• Si lo que desea es leer una variable de tipo tira de caracteres, no use &.
Otro detalle a tomar en cuenta es que scanf() considera que dos ítems de entrada son diferentes
cuando están separados por blancos, tabulados o espacios. Va encajando cada especificador de
conversión con su campo correspondiente, ignorando los blancos intermedios. La única excepción
es la especificación %c, que lee el siguiente caracter, sea blando o no.
El siguiente programa es válido:
1.2 Lo mas básico 27
1.2.6 Operadores
El C++ está lleno de operadores. Presentamos aquí una tabla de los mismos indicando el rango
de prioridad de cada uno, y cómo se ejecutan. A continuación comentaremos brevemente los
operadores.
Operadores Sentido
( ) { } -> . I-D
! ~++ – - (tipo) * & sizeof(todos unarios) D-I
*/% I-D
+- I-D
<<>> I-D
< <= > >= I-D
== != I-D
& I-D
^ I-D
| I-D
&& (and) I-D
|| (or) I-D
?: (Operador Condicional Ternario) I-D
= += -= *= /= %= D-I
, I-D
& Operador de Dirección Cuando va seguido por el nombre de una variable, entrega la
dirección de dicha variable &abc es la dirección de la variable
abc.
* Operador de Indirección Cuando va seguido por un puntero, entrega el valor almacenado
en la dirección apuntada por él
1 abc = 22;
2 def = & abc ; // puntero a abc
3 val = * def
. El operador de pertenencia (punto) se utiliza junto con el nombre de la estructura o unión, para
especificar un miembro de las mismas. Si tenemos una estructura cuyo nombre es nombre,
y miembro es un miembro especificado por el patrón de la estructura, nombre.miembro
identifica dicho miembro de la estructura. El operador de pertenencia puede utilizarse de la
misma forma en uniones. Ejemplo:
1 struct {
2 int codigo ;
3 float precio ;
4 } articulo ;
5 articulo . codigo = 1265;
1 struct {
2 int codigo ;
3 float precio ;
4 } articulo , * ptrstr ;
5 ptrstr = & articulo ;
6 ptrstr - > codigo = 3451;
De este modo se asigna un valor al miembro código de la estructura articulo. Las tres
expresiones siguientes son equivalentes:
ptrstr->código articulo.código (*ptrstr).codigo
30 CAPÍTULO 1. I NTRODUCCIÓN A LA PROGRAMACIÓN EN C ++
Operadores Lógicos
Estos son cuatro operadores que funcionan con datos de clase entera, incluyendo char. Se dice que
son operadores que trabajan "bit a bit" porque pueden operar cada bit independientemente del bit
situado a su izquierda o derecha.
~ El complemento a uno o negación en bits es un operador unario, cambia todos los 1 a 0 y
los 0 a 1. Así:
~(10011010) == 01100101
& El and de bits es un operador que hace la comparación bit por bit entre dos operandos. Para
cada posición de bit, el bit resultante es 1 únicamente cuando ambos bits de los operandos
sean 1. En terminología lógica diríamos que el resultado es cierto si, y sólo si, los dos bit
que actúan como operandos lo son también. Por tanto
porque todas las posiciones de bits con excepción del bit número 6 tenían un valor 1 en, por
lo menos, uno de los operandos.
^ El or exclusivo de bits es un operador binario que realiza una comparación bit por bit entre
dos operandos. Para cada posición de bit, el resultante es 1 si alguno de los operandos
contiene un 1; pero no cuando lo contienen ambos a la vez. En terminología, el resultado es
cierto si lo es el bit u otro operando, pero no si lo son ambos. Por ejemplo:
Observe que el bit de posición 0 tenía valor 1 en ambos operandos; por tanto, el bit resultante
ha sido 0.
<< El desplazamiento a la izquierda es un operador que desplaza los bits del operando
izquierdo a la izquierda el número de sitios indicando por el operados de su derecha.
Las posiciones vacantes se rellenan con ceros, y los bits que superan el límite del byte
se pierden. Así:
(10001010)«2 == (1000101000)
(10001010)»2 == (00100010)
1.2.6.8 Misceláneas
sizeof Devuelve el tamaño, en bytes, del operando situado a su derecha. El operando
puede ser un especificador de tipo, en cuyo caso se emplean paréntesis; por ejemplo,
sizeof(float). Puede ser también el nombre de una variable con concreta o de un array,
en cuyo caso no se emplean paréntesis: sizeof foto
(tipo) Operador de moldeado, convierte el valor que vaya a continuación en el tipo especifi-
cado por la palabra clave encerrada entre los paréntesis. Por ejemplo, (float)9 convierte
el entero 9 en el número de punto flotante 9.0.
, El operador coma une dos expresiones en una, garantizando que se evalúa en primer
lugar la expresión situada a la izquierda, una aplicación típica es la inclusión de más
información de más información en la expresión de control de un bucle for:
Las sentencias de decisión o también llamadas de control de flujo son estructuras de control que
realizan una pregunta la cual retorna verdadero o falso (evalúa una condición) y selecciona la
siguiente instrucción a ejecutar dependiendo la respuesta o resultado.
1.3.1.1 Sentencia if
La instrucción if es, por excelencia, la más utilizada para construir estructuras de control de flujo.
Sintaxis
Primera Forma
Ahora bien, la sintaxis utilizada en la programación de C++ es la siguiente:
1 if ( condicion )
2 {
3 Set de instrucciones
4 }
siendo condición el lugar donde se pondrá la condición que se tiene que cumplir para que sea
verdadera la sentencia y así proceder a realizar el set de instrucciones o código contenido dentro de
la sentencia.
Segunda Forma
Ahora veremos la misma sintaxis pero ahora le añadiremos la parte Falsa de la sentencia:
1 if ( condicion )
2 {
3 Set de instrucciones //Parte VERDADERA
4 }
5 else
6 {
7 Set de instrucciones 2 //Parte FALSA
8 }
La forma mostrada anteriormente muestra la unión de la parte verdadera con la nueva secuencia la
cual es la parte falsa de la sentencia de decisión if.
La palabra Else indica al lenguaje que de lo contrario al no ser verdadera o no se cumpla la parte
verdadera entonces realizara el set de instrucciones 2.
Ejemplos De Sentencias If
Ejemplo 1:
1.3 Estructuras de Control 33
1 if ( numero == 0) //La condicion indica que tiene que ser igual a Cero
2 {
3 cout < < " El Numero Ingresado es Igual a Cero " ;
4 }
Ejemplo 2:
1 if ( numero > 0) // la condicion indica que tiene que ser mayor a Cero
2 {
3 cout < < " El Numero Ingresado es Mayor a Cero " ;
4 }
Ejemplo 3:
1 if ( numero < 0) // la condicion indica que tiene que ser menor a Cero
2 {
3 cout < < " El Numero Ingresado es Menor a Cero " ;
4 }
Ahora uniremos todos estos ejemplos para formar un solo programa mediante la utilización de la
sentencia else e introduciremos el hecho de que se puede escribir en este espacio una sentencia if ya
que podemos ingresar cualquier tipo de código dentro de la sentencia escrita después de un else.
Ejemplo 4:
1 if ( numero == 0) //La condicion indica que tiene que ser igual a Cero
2 {
3 cout < < " El Numero Ingresado es Igual a Cero " ;
4 }
5 else
6 {
7 if ( numero > 0) // la condicion indica que tiene que ser mayor a Cero
8 {
9 cout < < " El Numero Ingresado es Mayor a Cero " ;
10 }
11 else
12 {
13 if ( numero < 0) // la condicion indica que tiene que ser menor a Cero
14 {
15 cout < < " El Numero Ingresado es Menor a Cero " ;
16 }
17 }
18 }
sentencia default son opcionales. La sentencia switch es muy útil en los casos de presentación de
menús.
Sintaxis
1 switch ( condicion )
2 {
3 case primer_caso :
4 bloque de instrucciones 1
5 break ;
6
7 case segundo_caso :
8 bloque de instrucciones 2
9 break ;
10
11 case caso_n :
12 bloque de instrucciones n
13 break ;
14
15 default : bloque de instrucciones por defecto
16 }
Ejemplo 1
1 switch ( numero )
2 {
3 case 0: cout << " numero es cero " ;
4 }
Ejemplo 2
1 switch ( opcion )
2 {
3 case 0: cout << " Su opcion es cero " ; break ;
4 case 1: cout << " Su opcion es uno " ; break ;
5 case 2: cout << " Su opcion es dos " ;
6 }
Ejemplo 3
1 switch ( opcion )
2 {
3 case 1: cout << " Su opcion es 1 " ; break ;
4 case 2: cout << " Su opcion es 2 " ; break ;
5 case 3: cout << " Su opcion es 3 " ; break ;
1.3 Estructuras de Control 35
Sintaxis
En donde, condición es la expresión que se evalúa, proceso 1 es la tarea a realizar en el caso de que
la evaluación resulte verdadera, y proceso 2 es la tarea a realizar en el caso de que la evaluación
resulte falsa.
Ejemplo 1
1 int edad ;
2 cout << " Cual es tu edad : " ;
3 cin >> edad ;
4 //Usando el operador condicional ternario
5 cout << (( edad <18) ? " Eres joven " : " Ya tienes la mayoria de edad " ) ;
6 //Usando if else
7 if ( edad < 18)
8 cout << " Eres joven aun " ;
9 else
10 cout << " Ya tienes la mayoria de edad " ;
Ejemplo 2
Vamos a suponer que deseamos escribir una función que opere sobre dos valores numéricos y que la
misma ha de regresar 1 (true) en caso de que el primer valor pasado sea igual al segundo valor; en
caso contrario la función debe retornar 0 (false).
Las Sentencias de Iteración o Ciclos son estructuras de control que repiten la ejecución de un grupo
de instrucciones. Básicamente, una sentencia de iteración es una estructura de control condicional,
ya que dentro de la misma se repite la ejecución de una o más instrucciones mientras que una a
condición específica se cumpla. Muchas veces tenemos que repetir un número definido o indefinido
de veces un grupo de instrucciones por lo que en estos casos utilizamos este tipo de sentencias.
En C++ los ciclos o bucles se construyen por medio de las sentencias for, while y do - while. La
sentencia for es útil para los casos en donde se conoce de antemano el número de veces que una o
más sentencias han de repetirse. Por otro lado, la sentencia while es útil en aquellos casos en donde
no se conoce de antemano el número de veces que una o más sentencias se tienen que repetir.
Donde contador es una variable numérica, final es la condición que se evalúa para finalizar el ciclo
(puede ser independiente del contador) e incremento es el valor que se suma o resta al contador.
Hay que tener en cuenta que el for evalúa la condición de finalización igual que el while, es decir,
mientras esta se cumpla continuaran las repeticiones.
Ejemplo 1
Esto indica que el contador i inicia desde 1 y continuará iterando mientras i sea menor o igual a 10
(en este caso llegará hasta 10) e i++ realiza la suma por unidad lo que hace que el for y el contador
se sumen repitiendo 10 veces Hola Mundo en pantalla.
Ejemplo 2
Este ejemplo hace lo mismo que el primero, salvo que el contador se inicializa a 10 en lugar de 1; y
por ello cambia la condición que se evalúa así como que el contador se decrementa en lugar de ser
incrementado.
La condición también puede ser independiente del contador:
Ejemplo 3
1.3 Estructuras de Control 37
1 int j = 20;
2 for ( int i =0; j >0; i ++) {
3 cout < < " Hola " <<i < < " - " <<j < < endl ;
4 j - -;
5 }
En este ejemplo las iteraciones continuaran mientras i sea mayor que 0, sin tener en cuenta el valor
que pueda tener i.
1 while ( condicion )
2 {
3 codigo a Repetir
4 }
Ejemplo 1
1 int contador = 0;
2
3 while ( contador <=10)
4 {
5 contador = contador +1;
6 cout < < " Hola Mundo " ;
7 }
El contador indica que hasta que este llegue a el total de 10 entonces se detendrá y ya no se realizará
el código contenido dentro de la sentencia while, de lo contrario mientras el contador sea menor a
10 entonces el código contenido se ejecutará desplegando hasta 10 veces Hola Mundo en pantalla.
La sentencia do es usada generalmente en cooperación con while para garantizar que una o más
instrucciones se ejecuten al menos una vez. Por ejemplo, en la siguiente construcción no se ejecuta
nada dentro del ciclo while, el hecho es que el contador inicialmente vale cero y la condición para
que se ejecute lo que está dentro del while es "mientras el contador sea mayor que diez". Es evidente
que a la primera evaluación hecha por while la condición deja de cumplirse.
1 int contador = 0;
2
38 CAPÍTULO 1. I NTRODUCCIÓN A LA PROGRAMACIÓN EN C ++
1 int contador = 0;
2 do
3 {
4 contador ++;
5 cout < < " Hola Mundo " ;
6 }
7 while ( contador > 10) ;
Observe cómo en el caso de do la condición es evaluada al final en lugar de al principio del bloque
de instrucciones y, por lo tanto, el código que le sigue al do se ejecuta al menos la primera vez.
1.3.3.1 Break
La sentencia break se usa para forzar un salto hacia el final de un ciclo controlado por for o por
while.
Ejemplo
En el siguiente fragmento de código la sentencia break cierra el ciclo for cuando la variable i es igual
a 5.
1 0 1 2 3 4
1.3.3.2 Continue
La sentencia continue se usa para ignorar una iteración dentro de un ciclo controlado por for o por
while.
Ejemplo
1.3 Estructuras de Control 39
1 0 1 2 3 4 6 7 8 9
Break
1 int i = 0;
2 while (i <10) {
3 if ( i == 5) break ;
4 cout << i << " " ;
5 i ++;
6 }
Continue
1 int i = -1;
2 while (i <10) {
3 i ++;
4 if ( i == 5) continue ;
5 cout << i << " " ;
6 }
2. Introducción a los jueces virtuales de progra
Existen algunos otros errores que son menos comunes, por lo cual no son mencionados acá.
Una vez que se ha leído un problema, e implementado(programado) un código que resuelva los casos
de ejemplo, y se este seguro que dará para cualquier caso se puede enviar el código; usualmente lo
único que se debe hacer para enviar un código es darle al boton ’Submit/Enviar’ (la mayoría de los
jueces están en inglés así que por lo general el botón dirá ’Submit’) del juez, a continuación se debe
elegir el lenguaje en el que se enviará la solución, y pegar el código, finalmente terminar el envío con
algún botón que diga "submit" (o algún equivalente). E.g se enviará un código al juez online UVA:
En la parte superior derecha se puede ver el botón submit, mediante un clic accederemos a la ventana
de envío:
2.2 ¿Cómo enviar un problema a un juez virtual? 43
En esta pestaña elegiremos el lenguaje (en otros jueces es posible que se tenga una lista más extensa
de lenguajes para elegir).
En el recuadro grande nosotros podremos pegar nuestro código con un simple copiar-pegar, la otra
opción es darle clic al botón de seleccionar archivo, en cuyo caso tendremos que buscar la carpeta
donde se encuentra ubicado nuestro código y seleccionarlo (nótese que algunos jueces solo permiten
esta última opción):
A continuación darle clic al botón submit, y solo nos resta esperar a ver que veredicto nos dio, para
esto debemos buscar la opción "my submissions" o algún equivalente del juez que estemos usando:
44 CAPÍTULO 2. I NTRODUCCIÓN A LOS JUECES VIRTUALES DE PROGRAMACIÓN
En esa pestaña podremos ver el veredicto Uno no debe preocuparse si accede a un nuevo juez en
general todos son muy parecidos y se puede hallar las opciones para enviar y ver los resultados de
manera intuitiva. Jueces de este tipo:
Nota: Para lenguajes como java que utilizan una clase siempre, existen normas dentro de cada juez
sobre como debe llamarse tu clase, eso aparece específicado en las reglas del juez.
Sin embargo existen algunos jueces que evalúan de manera más especial, en primera tenemos a los
brindados para las competencias de Google Code Jam, y Facebook Hacker Cup que nos proporcionan
un archivo de entrada, y nosotros debemos enviar nuestro archivo de salida, y una copia de nuestro
código.
Por ejemplo en codejam:
2.2 ¿Cómo enviar un problema a un juez virtual? 45
Esas 2 líneas con freopen son las que nos permiten leer y escribir archivos, en el primer parámetro
va el nombre de nuestro archivo, en el segundo "r" si es el archivo de entrada y "w" si es el de salida,
en el tercero de igual manera stdin para entrada y stdout para salida. Para poder leer el archivo este
debe estar ubicado en la misma carpeta que nuestro código. Otra alternativa para leer y escribir el
archivo desd consola (en Windows o en Linux) es:
Finalmente solo resta enviar el archivo "misalida.txt" (o el nombre que tenga asignado ese archivo) y
una copia del código si es que pide. A continuación nos dará uno de los veredictos mencionados
anteriormente.
46 CAPÍTULO 2. I NTRODUCCIÓN A LOS JUECES VIRTUALES DE PROGRAMACIÓN
Otro juez con un formato especial es TopCoder: Para empezar deberemos descargar la arena de:
https://community.topcoder.com/contest/arena/ContestAppletProd.jnlp
Esta aplicación nos permetirá competir y practicar en topcoder.
Nota. A la fecha de realización de este capítulo existe una versión web en beta de la arena de
topcoder, tiene muchos errores, se cuelga sin razón aparente, solo se puede usar java, etc. Es mucho
mejor descargar la arena.
Para acceder a un problema (en práctica) iremos a la pestaña practice room, lo cual nos abrirá nuevas
pestañas, seleccionamos alguna de estas (ususalmente las SRM son las mejores para practicar),
seleccionamos uno de los concursos dentro de la pestaña y le damos clic:
En la parte superior derecha inicialmente se elige el lenguaje que se usará para resolver el problema
(esto es importante algunas partes del enunciado cambian en función del lenguaje elegido). El cuadro
de arriba nos muestra la descripción del problema, límites de memoria y tiempo, casos de prueba. Y
además a diferencia de otros jueces EN ESTE JUEZ NO EXISTEN ENTRADAS NI SALIDAS solo
se debe implementar una función dentro de una clase. Por ejemplo este código resuelve el problema
de 200 puntos del SRM 144 DIV 2:
4 public :
5 string whatTime ( int seconds ) {
6 int h = seconds /3600;
7 seconds %=3600;
8 int minutos = seconds /60;
9 seconds %=60;
10 string s = " " ;
11 s += to_string ( h ) ;
12 s += " : " ;
13 s += to_string ( minutos ) ;
14 s += " : " ;
15 s += to_string ( seconds ) ;
16 return s ;
17 }
18 };
Este código debe estar en la ventana inferior de la pestaña. El formato en C++ es el mismo para
todos los códigos solo cambian los nombres de la clase y la función.
Como se puede ver el código no cuenta con una función main() esto hace que algunos se asusten con
este juez al no poder probar su código; sin embargo el código puede ser compilado y corrido con las
opciones de que aparecen en la parte inferior de la pestaña:
Con compile hacemos correr nuestro código, con batch test podremos verificar que nuestro código
sirve para los casos de ejemplo, y con submit podremos enviar nuestra solución. Otra opción
para probar nuestro código (si es que uno no esta muy acostumbrado a trabajar en esta arena, o
simplemente no le gusta) es crear su propio main y llamar a nuestra clase ahí con nuestras entradas.
e.g:
Ese es un main para el problema citado anteriormente llamamos a la clase del problema en una
variable, y a continuación podemos mostrar el resultado para un cierto dato de entrada, en este caso
el problema te daba un número entero entonces el único parámetro a ingresar en nuestra función es
ese entero, Nota que también se puede puede pedir datos por teclado y mandarlos a la función.
La última opción es usar algún plugin que genere automáticamente la clase, la función y permita
probar los casos de prueba directamente en tu editor/IDE favorito e.g Kawigi, File z editor; sin
embargo no sé hablara sobre el funcionamiento de los plugins en este libro (por ahora).
simplemente se debe enviar el código y este será evaluado, otros en los que se debe enviar un archivo
de entrada y salida, y finalmente problemas con un estilo muy parecido a topcoder.
Otra particularidad de este juez es que evalúa por subtareas, una subtarea es esencialmente una parte
pequeña del problema, e.g resolver el problema para un límite más pequeño, o con alguna restricción
especial que facilita el problema al resolver todas las subtareas se otorga el puntaje máximo del
problema (equivalente a tener un Accepted). Cada subtarea tiene su propio puntaje asignado, toda
esta información debería ser explicada en el enunciado del problema.
Una vez enviado un problema se puede saber que puntaje dio (cuantas subtareas logró resolver el
código):
En este caso tenemos 2 envíos para el problema, al presionar el botón "Play!" podremos averiguar el
puntaje que nos dio el envío (como el envío de abajo que da un puntaje 60/100), como advertencia
en algunos concursos el número de veces que se puede usar el botón "Play!" es limitado, y no debe
usarse en todos los envíos. Además de saber que puntaje dio el código al presionar "Play!" podremos
ver detalles extra de nuestro envío presionando el botón details de nuestro envío:
50 CAPÍTULO 2. I NTRODUCCIÓN A LOS JUECES VIRTUALES DE PROGRAMACIÓN
En este caso vemos que la primera y última subtarea fueron resueltas de manera correcta, mientras
que las dos del medio fallaron.
Como consejo de parte del autor: Si sabes como resolver alguna subtarea de manera sencilla pues
hazlo, no trates de obsesionarte en querer todo el problema de golpe. También recuerda en este
tipo de problemas cada subtarea tiene sus propias condiciones y es posible que alguna condición
arruine tus otras subtareas, así que se ingenioso no importa si tienes un "if" que contiene una solución
distinta para cada subtarea si esto te da puntajes, al final el que más puntos acumule es el ganador.
Finalmente para los problemas " al estilo de TopCoder" por lo general en el enunciado nos hablaran
sobre una grader (archivo que no debemos tocar) y se nos pedirá implementar alguna función o
funciones que resuelvan el problema (cuyos nombres son dados en el problema) e.g "usted recibirá
debe implementar la función suma que recibe 2 números enteros, esta función debe retornar la suma
de los dos enteros a y b, además usted debe incluir "sumas.h" (el grader) a su código ". Entonces se
debería hacer algo como esto:
Al hacer esto el problema estaría resuelto, para probar nuestro código en el juez se debe ir a la
pestaña testing ubicada en la parte izquierda del juez (barra de opciones):
2.2 ¿Cómo enviar un problema a un juez virtual? 51
Una vez ahí se abrirá una ventana nueva, en la que elegiremos el problema y subiremos nuestros
archivos:
Deberemos subir nuestro código y el archivo de entrada, también podemos subirlos comprimidos
en un zip, una vez enviados estos podremos descargar el archivo de salida generado para nuestro
código, dando clic al botón "Download", este archivo puede ser abierto con un bloc de notas. Al
igual que el caso de Topcoder si es que no es agradable trabajar con la interfaz del juez el usuario es
libre de crear su propia función main para poder probar su código; pero no debe olvidarse borrar esta
(comentar) a la hora del envío.
52 CAPÍTULO 2. I NTRODUCCIÓN A LOS JUECES VIRTUALES DE PROGRAMACIÓN
3.1 Introducción
En este capítulo veremos más de C++, el uso de sus librerías. C++ provee de librerías muy útiles
que nos harán de mucha ayuda, en este capítulo veremos cómo utilizarlas y como trabajan en el
interior.
3.1.2 Genericidad
Una clase genérica encapsula a una estructura cuyo comportamiento es independiente del tipo de las
componentes, esto favorece la reusabilidad. Lo cual implica que estas clases pueden llegar a ser de
cualquier tipo de dato incluso de otra clase genérica o no.
Booleano Está estructura es la más simple solo ocupa un byte puede llegar a almacenar 1 (verdad)
o 0 (falso).
Carácter Está estructura llega a almacenar un carácter en general como números, letras y
símbolos, ocupa en memoria 8 bits, desde -128 a 127.
Entero Está estructura puede almacenar a un entero desde -21474836482147483647, ocupa
en memoria 32 bits.
Punto Está estructura almacena números reales desde 1.2e-308 a 3.4e-38, ocupa en memoria
Flotante 32 bits.
Entero Está estructura almacena también enteros pero con más capacidad desde -
Largo 9223372036854775808 a 9223372036854775807, ocupa en memoria 64 bits.
Double Está estructura almacena también reales como los float pero con más capacidad desde
2.2e-308 a 3.4e-38, ocupa en memoria 64 bits.
Índice 0 1 2 3 4 5 6 7 8 9
int A[10] 445 123 523 -6546 867 0 -7890 23 3453 190
Este es un arreglo de caracteres de tamaño [3].
Índice 0 1 2
char A[3] ‘f’ ‘0’ ‘?’
3.2.2.2 Matrices
Está estructura almacena varios elementos del mismo tipo en forma planar.
Este es una matriz de doubles de tamaño [5][4].
Índices 0 1 2 3 4
0 5.345 1.0 5.0 -546.114 7.124
double M[8][4] 1 12.12 -45.7 -4.8 56.5 7.4
2 13.3 4.12 -79.3 13.51 45.13
3 45.1 6.41 1.1315 2.5 3.12343
3.2.2.3 Pares
Está estructura almacena solo dos elementos del mismo tipo o diferente tipo.
Este es un par de <caracter, entero>.
3.3.1.1 Vector
Para la utilización de está estructura es necesario añadir en la cabecera del programa #include
<vector>, gracias a esto ya podemos utilizar la estructura de otro modo no. Vector es una clase
genérica y está pensada para operar con arreglos unidimensionales de datos del mismo tipo.
3.3.1.1.1 Creación
Para la creación de un vector se le debe mandar el tipo de dato. De la siguiente manera:
Otra manera de creación puede realizarse mandando al constructor el tamaño inicial del vector:
De está manera se reservara 8 espacios de memoria de tipo entero decimal para la utilización de
estos se debe primero llenar de lo contrario retornara datos basura si se llega a acceder a uno de ellos.
.begin() Está función retorna el iterador del primer elemento, si el vector está vacío retorna
.end().
.end() Está función retorna el iterador del elemento siguiente al último elemento del vector
3.3.1.1.2 Iteradores
Estos son punteros que son utilizados para el acceso directo a sus elementos.
3.3.1.1.3 Acceso
El acceso al vector se puede hacer directamente mediante los corchetes [ índice ] como si fuera
un array estático de la siguiente manera, siempre y cuando el vector tenga creado ese espacio de
memoria.
Existe también dos funciones auxiliares para el acceso al vector estas son .front() que retorna el
primer elemento del vector y .back() que retorna al último elemento del vector, el uso es el siguiente.
3.3.1.1.4 Ilustración
Índice 0 1 2 3 4
vector<int> vec(5) 445 123 523 -6546 867
Acceso .front(), vec[0] vec[1] vec[2] vec[3] .back(), vec[4]
Salida:
1 v1 : 100 200
2 v1 luego de . clear () :
3 v1 luego de . insert ( v1 . begin () , 2, 159) :159 159
4 v1 luego de . erase ( v1 . begin () ) :159
5 v1 luego de . push_back (753) :159 753
6 v1 luego de . pop_back () :159
7 v1 luego de . resize (4) :159 0 0 0
60 CAPÍTULO 3. E STRUCTURA DE DATOS 1
8 v2 : 123
9 SWAP
10 v1 : 123
11 v2 : 159 0 0 0
3.3.1.2 Pila
Para la utilización de está estructura es necesario añadir en la cabecera del programa #include
<stack>, gracias a esto ya podemos utilizar la estructura de otro modo no.
Una pila es una estructura de datos en la que el modo de acceso a sus elementos es de tipo LIFO (del
inglés Last In First Out, último en entrar, primero en salir) que permite almacenar y recuperar datos.
Para el manejo de los datos se cuenta con dos operaciones básicas: apilar, que coloca un objeto en
la pila, y su operación inversa, des-apilar (retirar), que retira el último elemento apilado. La clase
stack es genérica.
Las pilas suelen emplearse en los siguientes contextos:
• Evaluación de expresiones en notación postfija (notación polaca inversa).
• Reconocedores sintácticos de lenguajes independientes del contexto
• Implementación de recursividad.
3.3.1.2.1 Creación
Para la creación de una pila se le debe mandar el tipo de dato. De la siguiente manera:
De está manera se copiará los elementos de la pila stc1 a la nueva pila stc2.
3.3.1.2.2 Acceso
En cada momento sólo se tiene acceso a la parte superior de la pila, es decir, al último objeto apilado
(denominado TOS, Top Of Stack en inglés). La función .top() la obtención de este elemento, que
cuando es retirado de la pila permite el acceso al siguiente (apilado con anterioridad), que pasa a ser
el nuevo TOS.
3.3.1.2.3 Ilustración
stack<int> stc 445 123 523 -6546 867 <- TOS
Acceso .top()
.empty() Está función retorna verdad si la pila está vacía y retorna falso si es que por lo menos
tiene un elemento como tos.
.size() Está función retorna cuantos elementos tiene la pila, pero sin embargo no se puede
acceder a ellas por lo que no es muy usual el uso de está función.
A continuación un ejemplo de manejo.
3.3.1.3 Cola
Para la utilización de está estructura es necesario añadir en la cabecera del programa #include
<queue>, gracias a esto ya podemos utilizar la estructura de otro modo no. La clase queue es
genérica.
Una cola es una estructura de datos en la que el modo de acceso a sus elementos es de tipo FIFO
(del inglés First In First Out, primero en entrar, primero en salir) que permite almacenar y recuperar
datos.Así, los elementos sólo se pueden eliminar por el principio y sólo se pueden añadir por el final
de la cola.
3.3.1.3.1 Creación
Para la creación de una cola se le debe mandar el tipo de dato. De la siguiente manera:
62 CAPÍTULO 3. E STRUCTURA DE DATOS 1
De está manera se copiará los elementos de la cola que1 a la nueva cola que2.
3.3.1.3.2 Acceso
En una cola solo podemos acceder al primero o bien al último elemento de la estructura, para esto
tenemos dos funciones, .front() que nos permite acceder al primer elemento y .back() que nos
permite acceder al último elemento.
3.3.1.3.3 Ilustración
queue<int> que 445 123 523 -6546 867
Acceso .front() .back()
3.3.2.1.1 Creación
Para la creación de una cola prioridad se le debe mandar el tipo de dato, si el dato es cualquier tipo
de entero decimal, la prioridad sera el valor del mismo elemento.
Otra manera de creación puede realizarse mandándole el tipo de dato, vector del mismo dato y el
comparador:
64 CAPÍTULO 3. E STRUCTURA DE DATOS 1
1 priority_queue < int , vector < int > , greater < int > > pqg ;
3.3.2.1.2 Acceso
En una cola de prioridad solo podemos acceder a la raíz del heap con la función .top() está función
si es constante ya que siempre se sabe dónde está la raíz del heap.
3.3.2.1.3 Ilustración
3.3.2.2 Conjunto
Conjunto es un contenedor asociativo que contiene un conjunto ordenado de objetos únicos. La orde-
nación se realiza utilizando la función de comparación. Los conjuntos se implementan normalmente
como árboles rojo-negro, por lo que sus funciones principales (búsqueda, eliminación e inserción)
tienen complejidad logarítmica O(log2 N).
3.3.2.2.1 Creación
Para la creación de un conjunto se debe mandar el tipo de dato siempre y cuando tenga un comparador
por defecto, los datos primitivos si llevan comparadores definidos por defecto.
Si se desearía crear un conjunto de otro tipo de datos que no sean primitivos de c++ se debe
implementar una función booleana que acepta dos variables del tipo de dato deseado. Para la
creación de este tipo de conjuntos se debe mandar aparte del Tipo_de_Dato lo siguiente, bool
66 CAPÍTULO 3. E STRUCTURA DE DATOS 1
3.3.2.2.2 Iteradores
Estos son punteros que se utilizan para acceder a los elementos, un itereador soporta el operador ++,
lo cual significa que podemos usarlo en sentencias de iteración(for).
.begin() Está función retorna el iterador del primer elemento, si el conjunto está vacio retorna
.end().
.end() Está función retorna el iterador del elemento siguiente al último elemento del conjunto
3.3.2.2.3 Acceso
El acceso a está estructura se debe hacer con iteradores, o a ciertos datos por medio de funciones.
3.3 Estructuras Dinámicas 67
3.3.2.2.4 Ilustración
.clear() Está función elimina todos los elemtos del conjunto logrando así que el
tamaño (.size()) sea 0
.insert(pos) Está función inserta un elemento al conjunto si este ya existe lo ignorara,
valor debe ser un tipo de dato aceptado por el conjunto. La complejidad de
está función es O(log2 N).
.erase(inicio, fin) Está función elimina elemtos desde una posición inicial (inicio) hasta otra
posición (fin), inicio y fin deben ser iteradores. La complejidad es lineal
.erase(valor) Está función busca el elemnto y lo elimina, valor debe ser un tipo de dato
aceptado por el conjunto. La complejidad de está función es O(log2 N).
23 else
24 cout < < " 100 no esta en el conjunto \ n " ;
25 //Resultado: 100 no está en el conjunto
26 if ( conj . lower_bound (80) != conj . upper_bound (80) )
27 cout < < " 80 esta en el conjunto \ n " ;
28 else
29 cout < < " 80 no esta en el conjunto \ n " ;
30 //Resultado: 80 está en el conjunto
31 if ( conj . lower_bound (70) == conj . upper_bound (70) )
32 cout < < " 70 no esta en el conjunto \ n " ;
33 else
34 cout < < " 70 esta en el conjunto \ n " ;
35 //Resultado: 70 no está en el conjunto
36 return 0;
37 }
3.3.2.3 Mapa
Mapa es un contenedor asociativo que contiene pares de llaves y valores, las llave son únicas más no
así los valores. La ordenación se realiza utilizando la función de comparación. Las aplicaciones se
implementan normalmente como árboles rojo-negro, por lo que sus funciones principales (búsqueda,
eliminación e inserción) tienen complejidad logarítmica O(log2 N).
3.3.2.3.1 Creación
Para la creación de un mapa se debe mandar el tipo de dato de las llaves y el tipo de dato de los
valores, siempre y cuando el tipo de dato de las llaves tenga un comparador por defecto, los datos
primitivos sí llevan comparadores definidos por defecto.
Si se desearía crear un conjunto de otro tipo de datos que no sean primitivos de c++ se debe
implementar una función booleana que acepta dos variables del tipo de dato deseado. Para la
creación de este tipo de conjuntos se debe mandar aparte del Tipo_de_Dato lo siguiente, bool
(*)(Tipo_de_Dato , Tipo_de_Dato) y en el constructor mandarle el comparador que llegaría a ser
la función que vimos hace un momento. Quedaría de la siguiente manera:
14 map < vector < int > , int , bool (*) ( vector < int > , vector < int >) >
aplvec ( comvec ) ;
15 return 0;
16 }
3.3.2.3.2 Iteradores
Estos son punteros utilizados para acceder a los elementos un itereador soporta el operador ++, lo
cual significa que podemos usarlo en sentencias de iteración for.
.begin() Está función retorna el iterador apuntando al primer elemento, si la aplicación está
vacía retorna .end().
.end() Está función retorna el iterador apuntando al elemento siguiente al último elemento
de la aplicación.
3.3.2.3.3 Acceso
Un mapa tiene la ventaja de acceder a los valores directamente mediante los corchetes [ llave ] y
llaves, como si fuera un vector. Si se accede a un elemento mediante la llave y el corchete que no
existe, se crea automáticamente asignando 0’s como valores.
También se puede acceder a las llaves y valores mediante iteradores de la siguiente manera:
3.3.2.3.4 Ilustración
.clear() Está función elimina todos los elemtos de la aplicación logrando así
que el tamaño (.size()) sea 0
.insert(make_pair(llave, Está función inserta un par de elementos la llave y el valor designado
valor)) a la llave, llave y valor debe ser un tipo de dato aceptado por la
aplicación. La complejidad de está función es O(log2 N).
.erase(inicio, fin) Está función elimina elemtos desde una posición inicial (inicio) hasta
otra posición (fin), inicio y fin deben ser iteradores. La complejidad
es lineal
.erase(llave) Está función busca el elemnto y lo elimina, llave debe ser un tipo de
dato aceptado por la aplicación. La complejidad de está función es
O(log2 N).
72 CAPÍTULO 3. E STRUCTURA DE DATOS 1
4.1 Introduccion
4.1.1 ¿Qué es la Programación Modular?
La programacion modular es un paradigma (modelo) de programacion, que consiste en dividir un
problema en subproblemas con el fin de simplificarlo.
Aplicando la Programacion Modular podemos llevar problemas grandes y tediosos, a pequeños
subproblemas, y estos a su ves en otros, hasta poder resolverlos facilmente con un lenguaje de
programacion, a esta tecnica la llamaremos divide y venceras.
Un modulo es cada una de las partes del programa que resuelve un subproblema en las que se dividio
el problema original.[4]
4.2 Modulos
4.2.1 Concepto de Modulo
Un Modulo es una segmento de codigo separado del bloque principal, que puede ser invocado en
cualquier momento desde este o desde otro modulo.
4.2.3 Ejemplos
El siguiente ejemplo muestra una subrutina que tiene un parametro de entrada (x) del cual calcula su
factorial y lo devuelve como parametro de salida.
El factorial de un numero n se define como el producto de todos los numeros desde 1 hasta n, y se
simboliza n!.
5! = 1x2x3x4x5 = 120
76 CAPÍTULO 4. P ROGRAMACIÓN MODULAR
4.3 Recursividad
4.3.1 Que es recursvidad?
La recursividad es una tecnica de programacion en la que un modulo hace una llamada a si mismo
con el fin de resolver el problema. La llamada a si mismo se conoce como llamada recursiva.
Dicho formalmente, un algoritmo se dice recursivo si calcula instancias de un problema en función
de otras instancias del mismo problema hasta llegar a un caso base, que suele ser una instancia
pequeña del problema, cuya respuesta generalmente está dada en el algoritmo y no es necesario
calcularla.[5]
Aun asi la definicion puede ser confusa, por eso ahora presentaremos 2 ejemplos que ilustran la
recursividad:
4.3.2 Ejemplos
• Al igual que en el primer ejemplo tendremos una subrutina o funcion que calcule el factorial
de un numero, en este caso lo hara de manera recursiva.
9 if ( n ==1) return 1;
10 /*
11 Si el valor es diferente de 1 , entonces es un valor que
12 necesitamos calcular , por la definicion de factorial
13 podemos apreciar que , por ejemplo : 5!=5 x4 ! y que a su
14 vez 4!=4*3! y asi sucesivamente , esto es recursividad
15 ya que esta funcion se llama asi misma para calcular el
16 siguiente factorial .
17 */
18 return n * fact (n -1) ;
19 }
20 int main () {
21 int n ;
22 cin > > n ;
23 cout < < fact ( n ) << endl ;
24 return 0;
25 }
4.4.2.1 NOT
El NOT bit a bit, es una operación unaria que realiza la negación lógica en cada bit, invirtiendo los
bits del número, de tal manera que los ceros se convierten en 1 y viceversa.
a ∼a
0 1
1 0
Ejemplo:
∼ 1010
0101
4.4.2.2 AND
El AND bit a bit, toma dos números enteros y realiza la operación AND lógica en cada par
correspondiente de bits. El resultado en cada posición es 1 si el bit correspondiente de los dos
operandos es 1, y 0 de lo contrario. La operacion AND se representa con el signo ”&”.
a b a&b
0 0 0
0 1 0
1 0 0
1 1 1
Ejemplo:
& 10101
01011
00001
4.4 Operaciones de bits 79
4.4.2.3 OR
Una operación OR de bit a bit, toma dos números enteros y realiza la operación OR inclusivo en
cada par correspondiente de bits. El resultado en cada posición es 0 si el bit correspondiente de los
dos operandos es 0, y 1 de lo contrario. La operacion OR se representa con el signo ”|”.
a b a|b
0 0 1
0 1 1
1 0 1
1 1 0
Ejemplo:
| 00101
01001
01101
4.4.2.4 XOR
El XOR bit a bit, toma dos números enteros y realiza la operación OR exclusivo en cada par
correspondiente de bits. El resultado en cada posición es 1 si el par de bits son diferentes y 0 si el
par de bits son iguales. La operacion XOR se representa con el signo "^".
a b aˆb
0 0 0
0 1 1
1 0 1
1 1 0
Ejemplo:
^ 00101
01001
01100
4.4.4 Aplicaciones
Con todas las operaciones anteriormente vistas podemos hacer muchas cosas interesantes a la hora de
programar, ahora les mostraremos algunas de las mas importante aplicaciones de estas operaciones:
17 = 10001
1 << 4 = 10000
& 10001
10000
10000
15 = 1111
1 << 1 = 10
∼ 10 = ...11101
| 1111
1101
1101
21 = 10011
1 << 3 = 1000
& 10101
01000
11101
29 = 11101
29 >> 1 = 1110 = 14
29 << 1 = 111010 = 58
1 << 5 = 100000 = 25 = 32
0 000
1 001
2 010
3 011
4 100
5 101
6 110
7 111
Los subconjuntos seran cada numero en ese rango, y los elementos estan representados por cada
columna, entonces si el bit esta encendido quiere decir que el elemento se toma para ese subconjunto,
por le contrario si el bit esta apagado este se omite en este subconjunto.
82 CAPÍTULO 4. P ROGRAMACIÓN MODULAR
4.5.1 Ejemplo
Para ilustrar este algoritmo usaremos el siguiente problema: Se nos dare un entero n, seguido de n
enteros que representan la longitud de unas barras de metal, seguidamente se son dara un entero a
la longitud de una barra que necesitamos, lo que se quiere saber es si juntando varias de las barras
podemos formar una de longitud a. Aca tenemos el codigo en C++:
5.1 Introducción
La aparición de los problemas de matemáticas relacionados con las competencias de programación
no es sorprendente ya que las Ciencias de la Computación esta profundamente enraizada en las
Matemáticas.
Si queremos generar una lista de números primos en rango de [0..N], existe un mejor algoritmo que
probar si cada numero en el rango es primo o no. Este algoritmo es llamado "Criba de Eratóstenes"
inventado por Eratóstenes de Alexandria. Esto funciona de la siguiente manera.
Primero, hacemos que todos los números en el rango sean ‘probablemente primos’, pero hacemos que
los números 0 y 1 no sean primos. Luego, tomamos al 2 como primo marcamos todos los múltiplos de
2 empezando por 2 + 2 = 4, 6, 8, 10, ... hasta que el múltiplo sea mas grande que N. Luego tomamos
el siguiente numero no marcado como primo que en este caso seria el 3 y marcamos todos los
múltiplos de 3 empezando por 3 + 3 = 6, 9, 12, ... Luego tomamos al el siguiente numero no marcado
que en este caso seria el 5 y marcamos todos los múltiplos de 5 empezando por 5 + 5 = 10, 15, 20, ...
y así sucesivamente. Después de esto cualquier numero no marcado dentro del rango [0..N] sera
primo.
Dado que i es un primo que divide a j, se tiene que si i es el máximo factor primo de j este será el
último en pasar por j y si no entonces existirá otro posible i máximo que pase por j.
También tenemos que al final del procedimiento C[X] es el primo máximo que divide a X el cual lo
podemos obtener desde ahora en un tiempo de ejecución O(1).
Ahora tenemos un X > 1 y deseamos encontrar el conjunto de factores primos que siguen la
definición:
Definimos la función f p(x) para todo x > 1 la cual nos devuelve el conjunto de factores primos y
multiplicidad de x ordenado ascendentemente.
Pero tenemos que Pk = C[x] definido como el primo más grande que divide a X, por lo tanto:
a
f p(X) = {P1a1 , P2a2 , P3a3 , ..., Pk−1
k−1
} ∪ {C[x]ak }
88 CAPÍTULO 5. M ATEMÁTICAS
a
Podemos tomar un numero Y = P1a1 /timesP2a2 /timesP3a2 /times.../timesPk−1
k−1
aunque aun no conoce-
mos la multiplicidad de Pk sabemos que si el siguiente primo máximo que divide a Y es igual a Pk
o sea que Pk = Pk−1 podemos decir que la multiplicidad de Pk es 2 y continuar haciendo el mismo
proceso hasta que sean diferentes.
Entonces cuando Pk no sea igual a Pk−1 :
X = Y × Pkak
Y = PXak = C[x]
X
ak
k
Ahora dado que Y es el producto de todos los factores primos de X excepto por el mayor entonces
podemos hacer lo siguiente:
a
f p(Y ) = {P1a1 , P2a2 , P3a3 , ..., Pk−1
k−1
}
f p(X) = f p(Y ) ∪ {C[x] k } a
reemplazando:
X ak
f p(X) = f p( C[x]ak ) ∪ {C[x] }
Desde luego f p(Y ) será un conjunto vacio si X es un numero primo, por lo que tendremos la igualdad
siguiente:
(
{} si X ≤ 1
f p(X) = a
f p(Y ) ∪ {C[x] } si X > 1
X
Donde Y = C[x] ak y a es la multiplicidad de C[X].
El código lee un X por entrada e imprimirá los factores primos y su multiplicidad de X de forma
ordenada.
19 Y = Y / C [ Y ];
20 }
21 fp ( Y ) ;
22 cout < < C [ X ] < < " ^ " <<a < < endl ;
23 }
24 int main () {
25 int x ;
26 iniciar_criba () ;
27 while ( cin > > x ) {
28 fp ( x ) ;
29 }
30 return 0;
31 }
Aunque no podemos encontrar una función específica que describa el tiempo de ejecución, debido a
la característica impredecible de los números primos, podemos estar seguros que O(log2 X) describe
el peor tiempo de ejecución para un todos los X en el rango de 1 a N. Es decir que el máximo tiempo
de ejecución dado un X será en un tiempo proporcional a log2 X o mucho menor en un buen caso.
Para verlo de una manera más simple volvemos a nuestra definición:
A simple vista vemos que el número de factores primos está estrechamente relacionado al valor de
los mismos. Mientras más grandes son los factores menor será la multiplicidad da cada factor. Dado
este hecho el peor de los casos es uno en el que la suma de la multiplicidad de los factores es más
grande por tanto los factores tienen que ser mínimos.
Si X tiene la forma de 2M , o sea está conformado de M factores primos iguales a 2 entonces:
2k = X
log2 2k = log2 X
M = log2 X
Por lo que tenemos que en el peor de los casos tenemos O(log2 X) en un peor caso de prueba ya
que nuestro algoritmo tiene un tiempo de ejecución en función al número de factores primos y su
multiplicidad.
Este método es lento para un problema en el cual la cantidad de consultas del MCD es muy grande,
existe un método mas rápido llamado Algoritmo de Euclides.
El Algoritmo de Euclides itera sobre dos números hasta que encontremos un resto igual a 0. Por
ejemplo, supongamos que queremos encontrar el MCD de 2336 y 1314. Empezamos expresando el
numero mas grande (2336) en términos del numero pequeño (1314) mas un resto:
El ultimo resto distinto de cero es el MCD. Así el MCD de 2336 y 1314 es 146. Este código
algoritmo puede ser fácilmente codificado como una función recursiva (mirar abajo).
El MCD esta muy relacionado con el Mínimo Común Múltiplo (mcm). El mcm de dos números
enteros (a, b) denotado por mcm(a, b), es definido como el numero entero mas pequeño l tal que a | l
y b | l.
Ej: mcm(4, 8) = 8, mcm(10, 5) = 10, mcm(20, 12)60.
Usando el Algoritmo de Euclides también podemos encontrar el mcm (mirar abajo).
5.7 Exponenciación binaria modulo m 91
a0 = 1
ab = a(c+d) = ac × ad , si b = c + d
(ab )c = abc
Ahora resolver el problema a "fuerza bruta" es fácil ya que lo único que debemos hacer es realizar
una multiplicación b veces lo que nos daría una solución en O(b). Sin embargo es posible optimizar
esto y obtener el resultado en O(log2 b).
Definimos una función que calculara POW (a, b) = ab
b b
Sabemos que POW (a, b) = a2 2 = (a2 ) 2 = POW (a × a, b2 )
Tendríamos un problema si b es un número impar así que haremos lo siguiente:
c = b − 1 como b es impar entonces c es par.
POW (a, b) = ab−1+1 = ac × a = POW (a × a, 2c ) × a = POW (a × a, d b2 e) × a
Donde dxe es la parte entera de x.
También podemos deducir fácilmente que d b2 e = b−12 si b es un número entero impar.
Entonces ya tenemos descrita nuestra función recursiva POW .
1
si b = 0
POW (a, b) = POW (a × a, d b2 e) × a si b es impar
POW (a × a, b2 ) si b es par
Sin embargo, como sabemos, la función exponencial crece mucho y no podemos guardar ese número
en variables de tipo "int" o "long long" por lo que aplicaremos modulo a la función POW .
1
si b = 0
POW (a, b)%m = POW (((a%m × a%m)%m, d b2 e) × a%m)%m si b es impar
POW ((a%m × a%m)%m, b2 ) si b es par
Como podemos ver el tiempo de ejecución está en función de b (exponente) y no de a (base) por
tanto sabemos que b se dividirá en 2 hasta que esta llegue a 1 y de esta manera realizara k procesos.
1 = 2bk
2k = b
k = log2 b
Por lo tanto este algoritmo tiene un tiempo O(log2 b) el cual es totalmente eficiente.
6. Algoritmos Basicos
20
21 if ( first < last ) {
22 pivotElement = pivot (a , first , last ) ;
23 quickSort (a , first , pivotElement -1) ;
24 quickSort (a , pivotElement +1 , last ) ;
25 }
26 }
27 void swap ( int & a , int & b ) {
28 int temp = a ;
29 a = b;
30 b = temp ;
31 }
de un montículo a partir del conjunto de elmentos de entrada, y después, una fase de extracción
sucesiva de la cima del montículo. La implementación del almacén de datos en el heap, pese a ser
conceptualmente un árbol, puede realizarse en un vector de forma fácil. Cada nodo tiene dos hijos y
por tanto, un nodo situado en la posición i del vector, tendrá a sus hijos en las posiciones 2 x i, y 2 x
i +1 suponiendo que el primer elemento del vector tiene un índice = 1. Es decir, la cima ocupa la
posición inicial del vector y sus dos hijos la posición segunda y tercera, y así, sucesivamente. Por
tanto, en la fase de ordenación, el intercambio ocurre entre el primer elemento del vector (la raíz
o cima del árbol, que es el mayor elemento del mismo) y el último elemento del vector que es la
hoja más a la derecha en el último nivel. El árbol pierde una hoja y por tanto reduce su tamaño en
un elemento. El vector definitivo y ordenado, empieza a construirse por el final y termina por el
principio.
6.2.2 Ejemplo
El ejemplo mas comun de Busqueda Binaria es el de buscar un elemento x dentro de un arreglo de
nuemeros ordenados. Usando la definicion anterior veremos como podemos aplicar busqueda binaria
sobre este tipo de funciones.
Supongamos que tenemos el siguiente arreglo de numeros ordenados:
0 1 2 3 4 5 6 7 8 9 10 11
0 4 9 11 13 29 51 52 64 71 77 99
Y se quiere saber si en el se enuentra el numero 13, bien pues lo que haremos sera escribir la funcion
binaria:
6.2 Busqueda Binaria 99
(
True : x ≤ 13
F(x)
False : x > 13
Si tomamos la recta real como los posibles valores de x en nuestra funcion tenemos que todos los
valores desde −∞ hasta 13 son verdad y todos los valores desde 13 hasta +∞ son falsos. Lo que
queremos encontrar es el punto donde la funcion cambia de estado.
Para esto necesitamos don valores: a un valor siempre verdadero para la funcion y b un valor siempre
falso para la funcion. En nuestro caso podemos tomar la "pocision" −1 del arreglo como a ya que
sabes que sera menor a todos los elementos del arreglo y tomar la "pocision" 12 como b por que sera
mayor a cualquier elemento del arreglo.
a = −1 b = 12
A partir de este momento lo que haremos es tomar el elemento medio entre estos y ver que sucede
con la funcion, si dicho elemento nos devuelve verdad este sera nuestro nuevo a y si por el contrario
es falso sera nuestro nuevo b, asi sucesivamente hasta que la distancia entre a y b sea 1, pues en
ese momento habremos encontrado el punto de imflexion de la funcion. Es decir a sera un valor
verdarero y b uno falso, por lo tanto el elemento que buscabamos se encuentra en a.
EL codigo en C++ es el siguiente:
7.1 Introducción
En este capitulo estudiaremos estructura de datos más avanzadas las cuales no las prevee C++, por
lo que estaremos oblidados a implementarlas. En este capitulo abarcaremos tres estrcuturas:
• Arboles (no lo implemetaremos)
• Unión Find
• Segment Tree
7.2 Arboles
A los arboles lo veremos de manera más teorica, por lo que no nos enfocaremos como implementarlo
como en las demas estructuras, ya que tenemos en C++ una implementación de arboles Rojos y
Negros implementada (set).
Primero empezemos definiendo algunos terminos:
Nodo En estructuras de datos definiremos un nodo como la unidad fundamental de un árbol,
también podemos llamarlo vértice.
Arco Es un enlace, conección o unión entres dos nodos.
Un arbol es un conjunto de nodos y arcos, un nodo puede tener entre uno o dos arcos, una ilustración
puede ser la siguiente.
Si volteamos la imagen se logra ver un arbol como en la vida real. Ahora vamos a definir algunos
elementos fundamentales de un arbol:
102 L ECTURE 7. E STRUCTURA DE DATOS ++
Nodo Hijo Es aquel nodo que desciende de otro, en la imagen el nodo 6 es el hijo del
nodo 1.
Nodo Padre Es aquel nodo del que un nodo hijo desciende, en la imagen el nodo 4 es el
padre de los nodos 1 y 8.
Nodos Hermanos Son aquellos nodos que comparten el mismo padre, en la imagen los nodos
2 y 5 comparten el nodo padre 6.
Nodo Raíz Es el nodo superior del árbol, cada arbol solo tiene una raiz, en la imagen es
el nodo 4.
Nodo Hoja Es aquel nodo que no tienen hijos, en la imagen son los nodos 2, 5, 7 y 9.
Nodo Rama Aunque esta definición apenas la usaremos, son los nodos que no son hoja
ni raiz, en la imagen son los nodos 1, 8, 6 y 3.
Tambien podemos hablar de otros conceptos que definen las caracteristicas de un árbol, que más
adelanten nos seran de mucha ayuda:
Orden Es el número potencial de hijos que puede tener cada elemento de árbol. De este
modo, diremos que un árbol en el que cada nodo puede apuntar a otros dos es de orden
dos, si puede apuntar a tres será de orden tres, etc.
Grado Es ell número de hijos que tiene el elemento con más hijos dentro del árbol. En el
árbol del ejemplo, el grado es dos, ya que los nodos 4, 1 y 6 tienen dos hijos, y no
existen nodos con más de dos hijos.
Nivel Se define para cada nodo del árbol como la distancia a la raíz, medida en nodos. El
nivel de la raíz es cero y el de sus hijos uno. Así sucesivamente. En la imagen, el nodo
8 tiene nivel 1, el nodo 7 tiene nivel 2, y el nodo 9 nivel 3.
Altura La altura de un árbol se define como el nivel del nodo de mayor nivel. Como cada
nodo de un árbol puede considerarse a su vez como la raíz de un árbol, también
podemos hablar de altura de ramas. El árbol de la imagen tiene altura 3, la rama 8
tiene altura 2, la rama 6 tiene altura 1 y la rama 7 tiene altura cero.
Ahora podemos hablar de dos clases de arboles rapidamente.
• Arboles binarios
• Arboles multirrama
Hasta acá hemos logrado representar los elementos. Ahora podemos explicar las funciones de nuestra
estructura de datos.
7.3.1 Find
La función Find nos permite hallar a que conjunto pertenece nuestro nodo x, lo que hace en si es ir
buscando al padre de x recursivamente, hasta llegar a la raíz que es la que nos indica a que conjunto
pertenece.
7.3 Union-Find en Conjuntos Disjuntos 105
En la imagen anterior si deseariamos encontrar el padre de 3 Find(3) devolveria 3 por que el nodo 3
es padre del nodo 3 (ya que estan disjuntos).
En la imagen anterior si deseariamos encontrar el padre de 1 Find(1) devolveria Find(2) por que
el padre de 1 es 2, pero 2 no es la raiz, entonces seguimos buscando, Find(2) devolveria Find(4)
por que el padre de 2 es 4 y Find(4) devolvera 4 por que el padre de 4 es 4, así llegando a la raíz de
arbol. La manera de codificar seria la siguiente.
Cabe destacar que esta función puede y debe ser optimizada para que no realice tantas llamadas
recursivas.
Ahora vamos a explicar esta breve optimización supongamos que hemos usado está función opti-
mizada en el nodo 1 en la imagen anterior, el resultado sería lo siguiente.
En este caso lo que hace nuestro Find optimizado es actualizar el padre de cada nodo por el que
pase a la raíz, entonces todos los nodos que estaban en nuestro camino a la raíz quedan directamente
unidos a esta, lo cual nos ayuda mucho en cuánto a tiempo se refiere. Imaginemos que en la imagen
106 L ECTURE 7. E STRUCTURA DE DATOS ++
anterior en vez de haber un solo nodo entre 1 y la raíz hay 100000 nodos por los que queramos saber
cuál es el padre de 1 tendríamos que pasar por esos 1000000 si tuviéramos que preguntar varias
veces cuál es la raíz de 1 no resulta muy conveniente, en cambio con está optimización al preguntar
varias veces por el padre de 1 sabremos instantáneamente que su padre es el nodo 4. Y lo propio con
los 1000000 nodos que había en el camino.
7.3.2 Union
Como su nombre lo indica esta función une dos conjuntos disjuntos, logrando que ambos conjuntos
tengan una ráíz en común, lo que unimos en este caso no son exactamente el nodo x y el nodo y, sino
todos los elementos que son parte del mismo conjunto que x y todos los elementos que son parte del
conjunto y. Supongamos que tenemos todos los nodos disjuntos como y unimos los nodos 1 , 3 y
por otra lado los nodos 2, 4 y 5, tendríamos algo como esto.
De este modo se uniria dos conjuntos unitarios. Pero si unieramos el conjunto de 4 con el conjunto
de 1 quedaria algo así.
Los lectores más hábiles se habrán percatado que lo que hacemos para unir dos conjuntos (partiendo
de dos nodos) es buscar la raíz de esos dos conjuntos y unirlas como claramente se puede apreciar
en la imagen anterior. A continuación se presenta el código de cómo implementar la función de
Union(x, y). no realice tantas llamadas recursivas.
Para no realizar uniones innecesarias (que los nodos x y y ya pertenezcan al mismo conjunto)
podemos valernos de una función para verificar eso.
5 return u == v ;
6 }
Con esto hemos descrito ya el funcionamiento de esta importante estructura de datos, antes de
concluir mencionaré unas últimas cosas sobre la estructura.
• Esta estructura es muy importante en el campo de los grafos ya que nos permite hallar el
número de componentes conexas (por así decirlo el número de conjuntos que hay) para esto
solo tendríamos que agregar a nuestra función init(N) una variable conjuntos=N y cada vez
que realicemos una unión restar uno a la variable conjuntos.
• Otra aplicación importante que tiene es en dos algoritmos importantes de grafos Kruskal y
Prim que se valen de está estructura para poder funcionar.
• Como el lector debió haber notado en las imágenes cada conjunto es un árbol a este conjunto
de árboles se le denomina bosque, entonces Union-Find es un bosque.
• Si se quisiera separar un nodo de un conjunto es un tanto más difícil desafío al lector para que
lo intente. Pista si el nodo es un nodo hoja es fácil retirarlo porque no tiene más hijos y sino se
debe ver la forma de retirarlo sin separar a todos sus hijos del conjunto, mucha suerte.
7.4.1 Introducción
Antes de entrar al funcionamiento en si del árbol de segmentos, primero veremos un pequeño ejemplo
para así poder entenderel porqué del uso de Segment Tree.
Supongamos que tenemos un arreglo de N elementos (1 ≤ N ≤ 100000), sobre el cual se te hará M
(1 ≤ M ≤ 100000) consultas del tipo ¿Cuál es la suma en el rango L a R? (1 ≤ L ≤ R ≤ N)
Por ejemplo tenemos el siguiente arreglo.
Índice 1 2 3 4 5 6 7 8 9
int A[10] 12 7 6 1 0 12 2 3 4
Y nos preguntan cuál es la suma en el rango [2, 7] que en este caso seria 7 + 6 + 1 + 0 + 12 + 2 = 28
Una primera idea (y la más obvia) para resolver el problema sería usar el siguiente código.
Claramente es algo muy sencillo de codificar; pero es muy lenta si se realizan muchas consultas.
Podría tomarle hasta horas realizar la tarea. Entonces luego de pensar un poco viene a la mente la
siguiente idea. Acumular las sumas hasta cada posición con la ayuda de un segundo array.
Índice 1 2 3 4 5 6 7 8 9
int Array[10] 12 7 6 1 0 12 2 3 4
int Acumulado[10] 12 19 25 26 26 38 40 43 47
Esto nos permitirá saber fácilmente cual es la suma en un rango mediante una simple resta entre
Acumulado[R]-Acumulado[L-1], por ejemplo si queremos saber la suma en el rango [2, 7] solo
tendríamos que calcular Acumulado[7] − Acumulado[1] = 40 − 12 = 28, esto es mucho más rápido
que el anterior método y tambien sencillo de codificar.
Con eso hemos resuelto el problema de manera correcta. Ahora pido al lector pensar en las siguientes
situaciones.
• Aparte de existir consultas en las que te piden la suma del rango [L, R] hay otro tipo de
consultas Actualizar x y donde se pide que le dé un valor y al elemento Array[x].
• En lugar de preguntar por la suma en el rango [L, R] se pregunta por el máximo elemento en
el rango [L, R], además de existir actualizaciones.
Analicemos estás dos situaciones, en la primera ya no nos sirve de mucho el arreglo extra con los
acumulados, porque al actualizar un valor la suma hasta esa posición se verá afectada, entonces
tendríamos que volver a una situación similar al primer código donde se calculaba la suma para ese
rango. En el segundo caso directamente no podemos usar el arreglo de acumulados para preguntar
cuál es el mayor en el rango [L, R] y a eso hay que agregarle el hecho de que también existen
actualizaciones, así que igual estaríamos en una situación similar al primer código, pero en lugar de
sumar preguntando por el máximo.
Antes de seguir tómese un tiempo para pensar en alguna solución para estos problemas.
Si es que el lector no está familiarizado con Segment tree u otra estructura de datos que resuelva este
tipo de problemas no habrá podido dar con una solución óptima. Este tipo de problemas es el que
resuelve un Segment tree, problemas en los que se da un arreglo de elementos y varias consultas
en las que algunas pueden ser actualizaciones de algún elemento, o responder por alguna pregunta
en un rango [L,R], estás preguntas pueden ser cosas como el mínimo en el rango, el máximo en el
rango, suma de los elementos del rango, multiplicación de los elementos en el rango, etc. Con la
condición de que la operación que estemos realizando sea conmutativa, y asociativa.
7.4 Árbol de Segmentos 109
7.4.2 Construcción
Tal y como su nombre indica está estructura es un árbol, en el cual nosotros guardamos la información
de un determinado intervalo [L,R] en un nodo de este árbol, por ejemplo el nodo raíz contiene
la información de todo el arreglo [1,n], ya sea esta información la suma de un intervalo, o hallar
mínimos y máximos en un intervalo, o cualquier otra información de importancia. Ahora veamos
cómo construir este tipo de árboles, partamos las dos ideas mencionadas en el anterior párrafo: Un
nodo guarda la información de un intervalo [L,R] y el nodo raíz guarda la información de todo
el arreglo, ahora diremos que todos los nodos en nuestro árbol tienen 0 o 2 hijos, 0 en el caso
de ser nodos hoja. Cada nodo tiene la información de un intervalo, entonces su hijo izquierdo
tendrá la información del intervalo del rango [L,mitad] y el derecho la información del intervalo
(mitad,R] (donde mitad es (L+R)/2), estos dos nodos hijos a su vez tendrán la información de otros
dos intervalos más pequeños. Por ejemplo supongamos que tenemos el siguiente arreglo.
Índice 1 2 3 4 5 6 7 8
int Array[9] 1 5 11 15 21 25 31 35
En la imagen podemos apreciar que los nodos hoja pertenecen al arreglo original (nodos 8 al 15),
mientras que los demás nodos son la unión de algún intervalo, por ejemplo el nodo 4 representa al
intervalo [1,2], el nodo 2 al intervalo [1,4], el nodo 1 nuestra raíz representa al intervalo [1,n]. Para
implementar esto en código podríamos usar una función recursiva.
5 if ( l == r ) T [ node ] = a [ l ];
6 else {
7 int mi = ( l + r ) / 2; // mitad
8 init (2 * node , l , mi ) ; //llamamos al hijo izquierdo
9 init (2 * node + 1 , mi + 1 , r ) ; //llamamos al hijo derecho
10 T [ node ] = ( T [2 * node ] + T [2 * node + 1] ) ;
11 //como hemos ido llamando recursivamente
12 //ya sabremos el valor de T[2*node] y T[2*node+1] en este caso son sumas
13 //también podrían ser mínimos u otro tipo de datos.
14 }
15 }
En esta función iniciamos node=1 ya que 1 es nuestro nodo raíz que contendrá la información de todo
el arreglo, l y r son los límites de nuestro intervalo inicialmente tomamos todo el rango de elementos.
Como se puede apreciar tenemos un arreglo a[] que es en el que almacenamos los números originales,
el otro arreglo T[] es nuestro árbol el tamaño aconsejable para este arreglo es 4*N ya que como se
habrá podido ver con la anterior imagen el tamaño del árbol es mucho más grande que el arreglo
original y en los peores casos necesitaremos aproximadamente 4 veces el número de elementos
original. Si el lector se preguntaba porque existen los nodos 16 y 17 en la anterior imagen estos
corresponden a a[0] (un elemento que no existe en nuestro arreglo y al valer 0 no afecta en nada a la
suma) y a a[1]. Acá presentamos una tabla que indica que intervalo cubre cada nodo:
Nodo L R
1 0 8
2 0 4
3 5 8
4 0 2
5 3 4
6 5 6
7 7 8
8 0 1
9 2 2
10 3 3
11 4 4
12 5 5
13 6 6
14 7 7
15 8 8
16 0 0
17 1 1
Como se puede ver los nodos hoja contienen al arreglo original y sus rangos son [L,L].
7.4.3 Update
Hasta ahora podemos generar el árbol que contiene la información de los intervalos ahora proseguire-
mos con las actualizaciones, cambiar el valor de un elemento del arreglo por otro, para esto vamos a
usar una función muy parecida a la función previa para construir el árbol, con la diferencia de que
7.4 Árbol de Segmentos 111
ahora necesitamos dos parámetros más un entero x que es la posición que queremos actualizar por
un entero val, también cambia el caso base de nuestra recursión:
1 void update ( int x , int val , int node =1 , int l =0 , int r =N -1) {
2 if ( r < x || l > x ) return ; //si nos salimos del rango
3 //actulización cuando hemos llegado al nodo hoja buscado
4 if ( l == r ) T [ node ] = val ;
5 else {
6 int mi = ( l + r ) /2;
7 update (x , val , 2 * node , l , mi ) ; //hijo izquierdo
8 update (x , val , 2 * node + 1 , mi + 1 , r ) ; //hijo derecho
9 //actualización de los otros nodos
10 T [ node ] = ( T [2 * node ] + T [2 * node + 1] ) ;
11 }
12 }
Como se puede ver en el código es prácticamente igual que la función build, solo que ahora cambia
nuestro caso base. Por ejemplo si en el arreglo de la anterior imagen actualizamos el Array[5] con
un 1 quedaría así.
7.4.4 Query
Finalmente llegamos a la parte en la que realizamos las de cuánto es la suma (o el mínimo o algún
otro dato importante) en el rango [L,R], es probable que se haya percatado de un pequeño “error”
112 L ECTURE 7. E STRUCTURA DE DATOS ++
en nuestro árbol si por ejemplo se quisiera saber la suma en el intervalo [5,7] no tenemos un nodo
que tenga la respuesta para ese intervalo tenemos uno que guarda el intervalo [5,6] (nodo 6), otro
que guarda el intervalo [5,8] (nodo 3). Sin embargo la función query corrige esté aparente problema
tomando el rango [5,6] (nodo 6) y el rango [7,7] (nodo 14) a continuación los suma.
Como vemos en la imagen anterior nuestro algoritmo tomará los dos nodos pintados para responder
a nuestra consulta, el código para resolver esto es el siguiente.
• Segment tree puede ser extendido a más de una dimensión, por ejemplo en matrices. Se debe
tener muy en cuenta la cantidad de espacio que se utiliza para guardar la estructura en estos
casos.
• Existen consulta de actualización en un rango en segment tree, para esto hay que modificar un
poco el algoritmo y usar un algoritmo llamado “lazy propagation”.
• Segment tree suele acompañar a problemas de geometría, y servir en varios problemas de
estructuras de datos.
8. Grafos
8.1 Introducción
Representar una ciudad, una red de carreteras, una red de computadoras, etc., nos resulta un poco
dificil con las estructuras de datos que conocemos asi es que vamos a definir un grafo como un
conjunto de "objetos" relacionados entre si, pero a diferencia de un conjunto normal un grafo G
contiene dos conjuntos, el primer conjunto contiene a los objetos y el segundo conjunto contiene a
las relaciones que existen entre ellos.
Grafo dirigído:
116 CAPÍTULO 8. G RAFOS
Como vimos en los ejemplos anteriores, un grafo puede ser armado de distintas formas. Ahora que
ya sabemos que son los grafos y para que sirven, necesitamos saber ¿Cómo los representamos en un
lenguaje de programación?.
dependera del tipo de grafo, por ejemplo si queremos representar un grafo sin costos (dirigido o no
dirigido) nos basta con una matriz de variables booleanas.
A continuacion su codigo.
21 }
El número 100 nos indica cuantos nodos tiene nuestro grafo, la idea de esta representacion es poner
true cuando existe un arco desde el subindice i (fila) al subindice j (columna) o false cuando no hay
un arco de i a j.
Una de las ventajas que nos ofrece esta representación es la facilidad al momento de codificar. Una
de las desventajas es que el momento de recorrer el grafo debemos revisar toda la matriz, haciendo
que el recorrido sea de orden O(n2 ).
3
4 using namespace std ;
5
6 vector < pair < int , int > > Grafo [100];
7 // para 100 nodos la primera variable de pair es el nodo destino
8 // la segunda variable de pair es el peso
9
10 int main () {
11 int n ,m ,a ,b , p ;
12 cin > >n > > m ; // leemos numero de nodos y arcos
13 for ( int i = 0; i < m ; i ++) {
14 cin > >a > >b > > p ; // leemos arcos (nodo A, nodo B, costo P)
15 G [ a ]. push_back ( make_pair (b , p ) ) ; // grafo dirigido
16 G [ b ]. push_back ( make_pair (a , p ) ) ; // grafo no dirigido
17 }
18
19 // ...
20 // nuestro codigo para solucionar problemas
21 // ...
22
23 //para limpiar el grafo (solo si es necesario)
24 for ( int i = 0; i < n ; i ++)
25 G [ i ]. clear () ;
26
27 return 0;
28 }
Nota: Para grafos ponderados solo reemplazar la linea 6 por vector < vector < pair < int,int > > >
Grafo;
120 CAPÍTULO 8. G RAFOS
Ahora vamos a explicar la ejecución, empezamos en el nodo 5, el primer nodo en ponerse en cola
esta en color rojo, posteriormente revisamos a sus vecinos que estan de color lila y también los
ponemos en cola, luego los nodos lila se ponen en cola y se revisa a sus vecinos (color verde) y
también los ponemos en cola, asi sucesivamente hasta que todo el grafo este visitado.
Para evitar que el algoritmo sea infinito vamos a instanciar un Array que nos indique si un nodo fue
visitado o no. A continuación el codigo de la búsqueda en profundidad.
22 q . push ( v ) ;
23 visitado [ v ] = visitado [ u ] + 1;
24 // al nodo V llegamos con 1 paso adicional
25 }
26 }
27 }
28 }
29
30 int main () {
31 // ...
32 // lectura del grafo
33 // ...
34 memset ( visitado , -1 , sizeof ( visitado ) ) ; // -1 no visitado
35 bfs (5) ; // bfs desde un nodo empírico
36
37 // ya recorrimos todo el grafo :)
38 return 0;
39 }
Si TODOS LOS ARCOS PESAN LO MISMO entonces con el anterior codigo podemos hallar el
camino mas corto desde 5 hasta todos los demás nodos, también podemos saber si el nodo X esta
conectado con el nodo Y, además podemos hallar en número de Componentes Fuertemente Conexas
(Si el grafo es NO DIRIGIDO).
Nota: Un grafo es llamado fuertemente conexo si para cada par de vértices U y V existe un camino
de U hacia V y un camino de V hacia U. Las componentes fuertemente conexas (SCC por sus siglas
en inglés) de un grafo dirigido son sus subgrafos máximos fuertemente conexos. Estos subgrafos
forman una partición del grafo.
Este recorrido es recursivo, y como su nombre indica (Busqueda en Profundidad) lo que hace es
entrar hasta lo mas profundo del grafo y luego empieza a retornar, para visualizar como funciona
pintaremos los vertices de 2 colores, de gris si este nodo se esta procesando y de negro cuando ya
fue procesado, tambien usaremos un borde rojo en el nodo en el que nos encontramos.
8.3 Recorrido de un grafo 123
Como podemos observar en el gráfico anterior, la raiz será el nodo 1, despues con la recursión
vamos "bajando" hasta lo más profundo de esa rama, cuando llegamos a un nodo hoja (sin mas hijos)
entonces volvemos atras y "bajamos" por las otras ramas que aún no han sido visitadas.
El algoritmo se encarga de visitar todo el grafo en profundidad, llegando hasta lo más profundo y
luego volviendo atrás para ir por otras ramas no visitadas.
124 CAPÍTULO 8. G RAFOS
Después de haber terminado con la recursión se ve que el último nodo en terminar de visitar es el
primer nodo. A continuación el codigo:
De este recorrido podemos sacar más información que de la búsqueda en anchura, debido a que este
recorrido nos genera un árbol llamado árbol de DFS, esto lo veremos en el siguiente capitulo.
9. Algoritmos ++
14 vvii G ( MAX ) ;
15 vi D ( MAX , MAXINT ) ;
16
17 void Dijkstra ( int s ) {
18 //usaremos un set para obtener el menor
19 set < ii > Q ;
20 D [ s ] = 0;
21 Q . insert ( ii (0 , s ) ) ;
22 while (! Q . empty () ) {
23 ii top = * Q . begin () ;
24 Q . erase ( Q . begin () ) ;
25 int v = top . second ;
26 int d = top . first ;
27 vii :: const_iterator it ;
28 for ( it = G [ v ]. begin () ; it != G [ v ]. end () ; it ++) {
29 int v2 = it - > first ;
30 int cost = it - > second ;
31 if ( D [ v2 ] > D [ v ] + cost ) {
32 if ( D [ v2 ] != 1000000000) {
33 Q . erase ( Q . find ( ii ( D [ v2 ] , v2 ) ) ) ;
34 }
35 D [ v2 ] = D [ v ] + cost ;
36 Q . insert ( ii ( D [ v2 ] , v2 ) ) ;
37 }
38 }
39 }
40 }
41
42 int main () {
43 int m , ini , fin = 0;
44 scanf ( " % d % d % d % d " , &n , &m , & ini , & fin ) ;
45 // nodos, arcos, inicio, destino
46 for ( int i = 0; i < m ; i ++) {
47 int a , b , p = 0;
48 scanf ( " % d % d % d " , &a , &b , & p ) ;
49 G [ a ]. push_back ( ii (b , p ) ) ;
50 G [ b ]. push_back ( ii (a , p ) ) ;
51 }
52 Dijkstra ( ini ) ;
53 printf ( " % d \ n " , D [ fin ]) ;
54 return 0;
55 }
El anterior código nos halla los caminos más cortos desde un nodo hasta todos los demás, por
ejemplo en el siguiente grafo:
9.3 Puntos de Articulación y Puentes 131
Los caminos violetas forman parte del arbol de caminos cortos, se puede ver que solo hay un camino
desde el nodo 1 hasta cualquier otro nodo, siendo este camino el camino mas corto.
Los únicos nodos que al eliminarlos nos desconecta el grafo son: 4,3.
En el anterior grafo solo hay 3 componentes fuertemente conexas, ¿ Te animas a hallar las compo-
nentes conexas ? recuerda que en una componente conexa hay camino de todos los nodos a todos los
nodos.
La respuesta es 3:
La técnica Búsqueda en profundidad (DFS) puede ser usada para determinar las componentes
fuertemente conexas (SCC) de un grafo dirigido eficientemente mediante el Algoritmo de Kosaraju.
Este algoritmo se basa en el hecho de que si invertimos todas las aristas de un grafo (hallamos el
grafo transpuesto), las SCC son las mismas que en el grafo original.
El algoritmo de Kosaraju lo que hace para hallar las SCC de un grafo dirigido G:
1. Realiza un DFS sobre G y enumera los vértices en orden de terminación de las llamadas
recursivas (como en el anterior algoritmo).
2. Construye un nuevo grafo GT invirtiendo la dirección de todo arco en G.
3. Realiza un DFS sobre GT empezando por el vértice que fue enumerado con el mayor valor
de acuerdo a la numeración asignada en el paso (1). Si el DFS no alcanza todos los vértices,
empieza el siguiente DFS desde el vértice restante enumerado con el mayor valor.
134 CAPÍTULO 9. A LGORITMOS ++
4. Cada árbol obtenido luego del DFS es una componente fuertemente conexa.
Nota: En lugar de enumerar los vértices, también podemos irlos insertando en una pila (stack).
Los anteriores algoritmos se los puede implementar en el mismo código de DFS ya que solo se
necesitan poner algunas variaciónes.
Con los grafos se pueden hacer muchas cosas más que estos algoritmos, asi que se sugiere al lector
seguir investigando.
10. Programación Dinámica
con nuevos problemas. Pero como todo en la vida, la práctica hace al maestro.
El los siguientes párrafos tratare de explicar como llegar a la solución de un problema utilizando
programación dinámica.
Quieres vender todos los vinos que tienes, pero quieres vender exactamente un vino al año, a partir
de este año. Una limitación será que cada año se te permite vender sólo el vino más a la izquierda o el
vino más a la derecha en el estante y no se te permite reordenar los vinos(es decir, deben permanecer
en el mismo orden en el que están al inicio).
Usted quiere saber, ¿Cuaĺ es el máximo beneficio que puede obtener, si vende los vinos en orden
óptimo?
Así por ejemplo, si los precios de los vinos son (en el orden en que se colocan en el estante, de
izquierda a derecha):
p1 = 1, p2 = 4, p3 = 2, p4 = 3
La solución óptima sería vender los vinos en el orden
p1 , p4 , p3 , p2
p1 = 2, p2 = 3, p3 = 5, p4 = 1, p5 = 4
p1 , p2 , p5 , p4 , p3
2 ∗ 1 + 3 ∗ 2 + 4 ∗ 3 + 1 ∗ 4 + 5 ∗ 5 = 49
p1 , p5 , p4 , p2 , p3
2 ∗ 1 + 4 ∗ 2 + 1 ∗ 3 + 3 ∗ 4 + 5 ∗ 5 = 50
Este contraejemplo debe convencerte, de que el problema no es tan fácil como puede parecer a
primera vista, vamos a resolverlo utilizando programación dinámica.
138 CAPÍTULO 10. P ROGRAMACIÓN D INÁMICA
Esta solución simplemente trata de todos los posibles órdenes válidas de la venta de vinos. Si hay N
vinos al principio, intentará 2N posibilidades(cada año tenemos 2 opciones). Así que, aunque ahora
tenemos la respuesta correcta, la complejidad del tiempo de ejecución de nuestra solución crece
exponencialmente.
Una función backtrack escrita correctamente siempre representa una respuesta a una pregunta bien
planteada.
En nuestro caso la función de ganancia representa una respuesta a la pregunta:
¿Cuál es el mejor beneficio que podemos obtener de la venta de los vinos con precios almace-
nados en el vector p, cuando la variable anio es el año en curso y el intervalo de los vinos sin
vender se extiende a través de [izq, der] (incluidos los vinos en las posiciones izq y der)?".
Usted siempre debe tratar de crear una pregunta de este tipo para su función de backtrack para ver si
lo ha hecho bien y entidende exactamente lo que hace la función.
10.1 Un poco de introducción 139
También quiero que pienses en el rango de posibles valores de los parámetros de la función que serian
parámetros validos. En nuestro caso, cada uno de los parámetros "izq" y "der" pueden tomar valores
entre 1 y N. En las entradas válidas también esperamos que izq <= der. Utilizando la notación
O-grande podemos decir que hay O(n2 ) diferentes parámetros para nuestra función, con las cuales
puede ser llamada.
Y eso es todo. Con ese pequeño truco el código funciona en tiempo O(n2 ), porque hay O(n2 )
posibles parámetros en nuestra función, que puede ser llamada varias veces y para cada uno de ellos,
la función se ejecuta sólo una vez con O(1) de complejidad de tiempo.
Una subsecuencia creciente mas larga posible que podemos obtener es:
0, 4, 6, 9, 11, 15
y otra:
0, 4, 6, 9, 13, 15
Entonces la solución sera 6, por ser el tamaño máximo que podemos obtener.
10.2.1.2 Análisis
Para resolver este problema pensemos en el ultimo elemento de una subsecuencia creciente, imag-
inemos que este ultimo elemento esta en la posición i, si i no es el ultimo elemento de la secuencia
original quiere decir que existen elementos a la derecha de i.
Supongo que ya te diste cuenta, podemos adicionar este elemento al final de nuestra subsecuencia e
incrementar su tamaño en 1.
Utilicemos esta observación para construir nuestra solución.
Si nos ponemos a pensar en la solución final(o por lo menos en una de ellas) notamos que nuestra
subsecuencia podría terminar en cualquier posición de la secuencia original, llamemos a esta posición
i y al tamaño de la subsecuencia creciente mas larga que termina en esa posición M,entonces de
acuerdo con la observación que hicimos antes, debe existir una subsecuencia de tamaño M − 1 a la
izquierda de i donde el ultimo elemento de esta subsecuencia mas pequeña es menor que el elemento
en la posición i.
Aquí es donde podemos notar con mas claridad el hecho de que el resultado no es único, si existieran
varias subsecuencias crecientes con tamaño M − 1 a la izquierda de i donde su ultimo elemento es
menor que el elemento en la posición i, cada una formara una subsecuencia creciente que cumple
con la condición de ser la mas larga.
Como habrá notado este mismo análisis se aplica a la subsecuencia de tamaño M − 1, donde
podríamos encontrar subsecuencias de tamaño M − 2 a la izquierda de su ultimo elemento, y así
hasta que solo tengamos un solo elemento. Entonces podemos plantear una pregunta a la cual deberá
responder nuestra función recursiva:
¿Cual es el tamaño de la subsecuencia creciente mas larga que termina en la posición ”i”?
Podemos notar que nuestra función solo recibirá un número como parámetro de entrada que repre-
senta a una posición y deberá retornar un numero que representa la respuesta a la pregunta. Pero
esta no es la solución final ya que dijimos que la solución podría terminar en cualquier posición
de la secuencia original, debemos hacer un ciclo preguntando sobre la subsecuencia creciente mas
larga que termina en la posición actual y tomar la mas grande de todas ellas. Con todo esto hecho
ya estamos en condiciones de entender y analizar el código que soluciona el problema, y tal vez
posteriormente utilizar la misma idea para poder solucionar problemas parecidos o del mismo tipo.
142 CAPÍTULO 10. P ROGRAMACIÓN D INÁMICA
10.2.1.3 Código
Pasemos a entender un poco el código, tenemos 2 funciones, la primera es " f (i)" que es la función
que mencionamos en el análisis(la función que responde a la pregunta) y la otra función "LIS()" es
la que se encarga de utilizar la primera función para solucionar el problema. Respecto a la primera
función lo que hacemos es recorrer con un ciclo en ” j” todos las posiciones que son menores que
”i” y verificamos que el elemento en v[ j] es menor que v[i], si es así entonces a la subsecuencia mas
grande que termina en la posición ” j” le podemos adicionar el elemento en la posición ”i”, es por
eso el ” f ( j) + 1”, simplemente tomamos el mas grande de todos y lo retornamos.
No debemos olvidar cachear la respuesta de la llamada a la función, para no tener que calcular
respuestas otra vez cuando ingresen los mismos parámetros, eso lo hacemos en el vector "d p". En
cuanto a la segunda función, lo primero que hace es setear todos los elementos del vector "d p" en
−1 para indicar que aun no fueron calculadas cuando se llame a la función f , y un ciclo el cual
prueba con todos las posiciones que podrían ser el final de la subsecuencia creciente mas grande,
llama a f con todas esas posiciones y toma el máximo de todos.
Dados N items, cada uno con un valor(o ganancia) y un peso. Y una mochila con una capacidad
de carga de W. Determinar el máximo valor que se puede obtener del proceso de llenar la
mochila con algunos de los items de modo que la suma de pesos de esos items no exceda W.
Del mismo modo que en el caso de La subsecuencia creciente mas larga, ¿Por que nos piden
solamente el máximo valor y no los items?, pues la respuesta es la misma, por que podrían existir
10.2 Problemas clásicos de Programación Dinámica 143
varias respuestas posibles correctas, por ejemplo un caso en el que podría ocurrir esto es cuando la
suma de los valores y pesos respectivamente de algunos items que aun no están en la mochila suman
lo mismo en su peso y valor que un solo objeto que también aun no esta en la mochila, podríamos
meter cualquiera de los 2 en la mochila y obtener repuestas correctas con ambas.
10.2.2.2 Análisis
Primeramente para facilitar el análisis del problema definiremos un entero N que representa la
cantidad de items disponibles, otro entero CAPACIDAD que representa la capacidad de la mochila y
2 vectores de enteros, valor[i] y peso[i] que representan el valor y peso respectivamente del i-esimo
ítem. También por comodidad guardemos nuestros datos en estos vectores en las posiciones 1 a N,
dejando vacía las posiciones 0 de ambos vectores.
En un principio uno podría ponerse a pensar en una solución greedy(golosa) en la que seleccionamos
primero aquellos items que tienen mayor valor primero y nos detenemos cuando ya no quede espacio
suficiente para meter otro objeto. Pero después de probar algunos casos podríamos darnos cuenta
que no nos da una respuesta correcta.
La solución a este problema es un poco parecida al anterior problema, imaginemos que tenemos una
solución con los primeros N − 1 items, algunos items ya están en la mochila y algunos no.
¿Que sucede si aun queda espacio suficiente para meter el N-esimo item?
Pues podríamos meter el N-esimo item en la mochila y tener una mejor solución sumándole su valor
al que ya teníamos con los N − 1 primeros items.
Pero si no quedase espacio en la mochila para meter el N-esimo item nos quedamos con la duda de:
¿Que sucedería si no hubiese tomado algunos items, tal que quede espacio suficiente para meter el
N-esimo item?, ¿No obtendríamos mejor respuesta?
Es posible, como también es posible que no, así que debemos probar con los 2 casos, si metiésemos
el N-esimo item siempre y cuando haya suficiente espacio para él, y si no lo metiésemos dejando
espacio tal vez para posibles respuestas mejores con otros items.
Esto nos hace notar que todo el tiempo debemos saber cuanto espacio disponible queda en la mochila
y con que elementos estamos trabajando.
Así que nuestra solución contara con 2 parámetros, uno que nos dirá con que elementos estamos
trabajando y otro que nos avise cuanto espacio libre queda en la mochila.
Entonces la pregunta que nos podemos plantear sera:
¿Cual es el máximo beneficio que se puede obtener con los primeros ”i” items, con "libre"
espacio libre en la mochila?
Vamos a ello.
10.2.2.3 Código
Analizando un poco el código, lo primero que hacemos es revisar si no metimos un objeto demasiado
pesado en la mochila tal que hizo que el espacio libre que tenemos sea negativo, de ser así retornamos
un numero negativo muy grande para indicar que perdemos en vez de ganar, luego verificamos si i es
igual a 0 de ser así significa que ya no hay mas objetos disponibles(recuerda que indexamos nuestros
vectores desde 1), retornamos 0 por que ya no podemos ganar nada mas. Enseguida verificamos si
la respuesta para esos parámetros no fueron calculados ya y retornamos de ser así. En la opcion1
guardamos el resultado que obtendríamos si no metiésemos el item actual, el parámetro libre no
varia, en la opcion2 probamos la opción de si tomar el item actual, tomamos la ganancia que nos da
el item sumándonos valor[i] a la opción y llamamos recursivamente al resto de items que quedan por
procesar pero en este caso debemos restarle al parámetro libre el peso del item que acabamos de
tomar.
Finalmente devolvemos el máximo de las 2 opciones disponibles, sin olvidar cachear la respuesta
para posibles futuras llamadas.
Bibliography
[4] http://es.wikipedia.org/
[6] http://es.wikibooks.org/wiki/Programacion_en_C++
[8] Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest and Clifford Stein
Introduction to Algorithms, third edition