CP-UPV Bootcamp - Capítulo 2
Capítulo 2: Expresiones, control de flujo y arrays
PROBLEMAS DEL CAPÍTULO
Para acceder a los problemas de este capítulo, accede con tu usuario y contraseña a cpupv.contest.codeforces.com. Si no tienes usuario y contraseña, inscríbete aquí.Introducción
En este capítulo abordaremos las bases lógicas de la programación. Empezaremos viendo cómo el código toma decisiones y repite acciones mediante expresiones y bloques de control de flujo. Luego, aplicaremos esa lógica para manejar colecciones de datos utilizando arrays y vectores. El 95% de los problemas en programación competitiva involucran estas estructuras, por lo que aquí es donde verdaderamente empieza la programación divertida.
Contenidos
- Expresiones e instrucciones
- Bloques de control de flujo
- Arrays unidimenssionales
- Vectores
- Arrays y Vectores bidimensionales
- Array vs Vector
- Iterando sobre vectores
- Anexo
Expresiones e instrucciones
Expresiones
Una expresión es una combinación de operadores, constantes y variables que producen un valor. Se interpreta de izquierda a derecha, y a veces el orden de los operandos y operadores importa ya que algunos tienen preferencia
Aritméticas
Los paréntesis tienen prioridad; después van las multiplicaciones y divisiones, por último sumas y restas. En caso de duda se pueden usar paréntesis.
// evalúación por pasos
6 * 2 / ( 2 + 1 * 2 / 3 + 6 ) + 8 * ( 8 / 4 )
12 / ( 2 + 2 / 3 + 6 ) + 8 * ( 2 )
12 / ( 2 + 0 + 6 ) + 16
12 / ( 8 ) + 16
1 + 16
17
Los operadores binarios también se pueden combinar con los aritméticos, pero los binarios siempre tienen menos prioridad, más abajo se muestra una tabla con las prioridades de los operadores
Como curiosidad, en C++ cualquier expresión que se evalue a 0 es false, mientras que si es otra cosa es true, por ejemplo este código es válido (se ven los bloques if más adelante):
if (2+2) cout << "Aparezco en la terminal";
if (2-2) cout << "No aparezco en la terminal";
Relacionales
Las expresiones relacionales usan operadores de comparación como >, <, >=, <=, ==, != o ! y siempre se evalúan a verdadero o falso. Estas expresiones son las que se usarán en los bloques de control de flujo para tomar decisiones
int x, y; // Variables que usaremos
bool resultado;
// COMPARADORES
// > : mayor que
// < : menor que
// >= : mayor o igual que
// <= : menor o igual que
// == : igual que
// != : no igual que
// ! : inversión
x = 4, y = 5;
resultado = x > y; // false
resultado = y > x; // true
resultado = x < y; // true
resultado = y < x; // false
resultado = y > 5; // false !!!
x = 2, y = 2;
resultado = x <= y; // true
resultado = y <= x; // true
y = 3;
resultado = y >= x; // true
x = 2, y = 2;
resultado = x == y; // true
resultado = x != y; // false
x = 2, y = 3;
resultado = x == y; // false
resultado = x != y; // true
resultado = true;
resultado = !resultado; // false
resultado = !(2*1 == 0); // true
Lógicas
Las expresiones lógicas combinan expresiones relacionales usando los operadores && y ||
// && : condicion1 Y condición2
// || : condicion1 O condición2
x >= 4 && x <= 6 // Verdadero si x está entre 4 y 6 inclusive, falso en caso contrario
x > 10 || y <11 // Verdadero si x es mayor que 10 o y es menor que 11
!(x > 10) && y == 2 // Verdadero si x es menor o igual a 10 y además y es igual a 2
Condicionales
Las expresiones condicionales asignan un valor u otro a una expresión sin utilizar un bloque if. El operador que se utiliza se llama operador ternario:
int n = 5;
string status = (n%2 == 0) ? "Par" : "Impar";
// Si la condición `n%2 == 0` se evalúa a verdadero, la expresión se evalúa al primer valor ("Par") en caso contrario al segundo ("Impar")
// Los operadores ternarios se pueden combinar
char resultado = (n>10) ? (n%2 == 0 ? 'A' : 'B') : (n%2 == 0 ? 'C' : 'D');
Instrucciones
Una instrucción es más que una expresión, es una asignación, una acción, la modificación de una variable o la llamada a una función, por ejemplo:
int x = 5+2; // La expresión 5+2 se evalúa a 7, pero esta instrucción asigna el 7 a la variable x
cout << x; // Esta instrucción imprime x en la terminal
Pueden haber múltiples instrucciones en una misma línea, una instrucción acaba con un punto y coma ;.
Declaraciones
Cuando se declara una variable o función, se especifica el tipo de la variable o los parámetros de la función (las funciones se dan más adelante). Algo que está declarado debe ser implementado o adquirir un valor antes de poderse usar, de caso contrario las variables tendrán valores aleatorios o la llamada a funciones dará error. El propósito de una declaración es que hay intención de usar la variable o función declarada. Una función o variable ya declarada no se puede volver a declarar en el mismo alcance.
// Declarar múltiples variables del mismo tipo en una instrucción
int a, b;
// Declarar una función (pero aún no se puede utilizar)
int suma(int a, int b);
int a; // Error, ya existe `a`
Definiciones
La implementación de una función declarada o la asignación de una variable.
// Declarar múltiples variables del mismo tipo en una instrucción
a = 5;
// Declarar una función (pero aún no se puede utilizar)
int suma(int a, int b) {
return a+b;
}
int c = 4; // Declaración + definición
Bloques de control de flujo
Un bloque de control de flujo es una estructura en los lenguajes de programación que permite controlar el orden en que se ejecutan las instrucciones en un programa. Estos bloques permiten tomar decisiones, repetir operaciones y realizar saltos condicionales. Los bloques de control de flujo son fundamentales para la creación de algoritmos y la implementación de lógica en un programa.
A continuación se explicarán los bloques de control de flujo en C++, pero antes es importante conocer el concepto de alcance de una variable y recordar para qué sirven los diferentes tipos de paréntesis
Tipos de paréntesis en C++
Estos paréntesis se usan de la misma manera en varios lenguajes de programación.
// PARÉNTESIS ():
// En las funciones, para definir y pasar parámetros (las funciones se darán más adelante)
int suma(int a, int b) { return a+b; }
int n = suma(2, 3); // n = 5
// Para dar prioridad a parte de una expresión
6 + 8 / 2 // 10
(6 + 8) / 2 // 7
// En los bloques de control de flujo que vamos a ver ahora es obligatorio usarlos
while ( ... ) { ... }
for ( ... ) { ... }
if ( ... ) { ... }
switch ( ... ) { ... }
// CORCHETES []:
// Para acceder a las posiciones de un array
string s = "Buenas";
// s[0] = 'B'
// s[5] = 's'
// s[6] // ERROR
// LLAVES {}:
// Para encapsular bloques de instrucciones
if (true) {
instrucción 1;
instrucción 2;
{
instrucción 3;
instrucción 4;
}
instrucción 5;
}
// Cabe destacar que, en un bloque de control de flujo, si solo hay una instrucción en el bloque de instrucciones, las llaves se pueden omitir, por ejemplo estos dos bloques `if` hacen exactamente lo mismo
if (true) cout << "Hola";
if (true) { cout << "Hola"; }
// OJO: "Adiós" siempre se imprime porque se considera fuera del bloque `if`
if (condicion) cout << "Hola"; cout << "Adiós";
Alcance de una variable
Sobre las llaves que se mencionan antes, una variable declarada dentro de un bloque de instrucciones (dentro de unas llaves) no es visible fuera de ese bloque, pero si dentro de los bloques anidados, por ejemplo:
int x = 2;
cout << x; // 2
if (true) {
int y = 3;
cout << x; // 2
cout << y; // 3
{
cout << x; // 2
cout << y; // 3
}
}
cout << y; // ERROR, no sé qué es `y`
if
Este bloque sirve para ejecutar un bloque u otro dependiendo de si una condición es verdadera o falsa.
if (llueve) {
cout << "Llevate paraguas" << endl;
}
En el caso que se quiera hacer algo si la condición es falsa se puede añadir un else { ... }
if (llueve) {
cout << "Llevate paraguas" << endl;
} else {
cout << "No te lleves paraguas" << endl;
}
Se pueden encadenar múltiples bloques if y solo se debería ejecutar un bloque de código
double nota = 8.75;
if (nota >= 9) {
cout << "Sobresaliente" << endl;
} else if (nota >= 7) {
cout << "Notable" << endl;
} else if (nota >= 5) {
cout << "Aprobado" << endl;
} else {
cout << "Suspenso" << endl;
}
Dentro de un bloque if puede haber otro bloque if
int n = -5, m = 4;
if (n == 0) {
cout << "n es 0 ";
if (m == 0) cout << "y m es 0";
else if (m > 0) cout << "y m es mayor que 0";
else cout << "y m es menor que 0";
} else if (n > 0) {
cout << "n es mayor que 0";
if (m == 0) cout << "y m es 0";
else if (m > 0) cout << "y m es mayor que 0";
else cout << "y m es menor que 0";
} else {
cout << "n es menor que 0";
if (m == 0) cout << "y m es 0";
else if (m > 0) cout << "y m es mayor que 0";
else cout << "y m es menor que 0";
}
for
Un bloque for sirve para ejecutar un bloque de código múltiples veces mientras una condición sea verdadera. La estructura del for es la siguiente:
for (inicialización; condiciones; actualización) { /* Código */ }
// Inicialización: Solo se ejecuta una vez, antes de comenzar el loop. Es el espacio para definir nuevas variables que solo se usarán dentro del loop. Al finalizar, estas no serán visibles
// Condiciones: Una (o más condiciones combinadas con `&&`) que se tienen que dar para entrar al bucle una vez. Cada vez que se ejecute el bloque de codigo dentro del loop se volverá a evalúar la condición, la primera vez que sea falsa se acaba el bucle y no se vuelve a ejecutar
// Actualización: Después de ejecutar el bloque de código del loop, se pueden actualizar variables para estar preparadas para la próxima posible iteración
"""
FLUJO DE UN BUCLE FOR
(true)
──► inicialización ──► condición ──► codigo del bucle ──► actualización
│ ▲ │
(false) │ └────────────────────────────────────┘
▼
seguir con
el programa
"""
// Ejemplos
for ( int i = 0 ; i < n ; ++i ) { ... } // Típico bloque `for`
for (int i=0, j=10 ; i<n && j>5 ; ++i, --j ) { ... } // Dos variables y dos actualizaciones
for ( ; i < n ; ) { ... } // Sin inicialización ni actualización (básicamente un bloque `while`)
// Suma de numeros impares entre el 1 y n
int sum = 0;
for (int i = 1; i < n; ++i) {
if (i%2 == 1) sum += i;
}
// sum = 25
Nota: Para los bloques
forywhilese pueden utilizar las instruccionesbreakycontinue
break: Detiene la ejecución del loop y sale del bloque por completo para seguir con el código que hay debajo del bloquecontinue: Detener la ejecución del código en una iteración y saltar a la condición de entrada del loop
// Imprimir los números del n al 1 saltándose los pares
for (int n = 8; n > 0; --n) {
if (n%2 == 0) continue; // Par
cout << n << endl;
}
/*
7
5
3
1
*/
// Imprimir del n al 1 pero parar en 5
for (int n = 10; n > 0; --n) {
cout << n << endl;
if (n == 5) break;
}
/*
10
9
8
7
6
5
*/
while
Este bloque permite ejecutar código mientras la expresión dentro de los () sea verdadera. Es lo mismo que un for, cualquier cosa que se pueda hacer con uno se puede hacer con el otro, solo que a veces uno es más conveniente, por ejemplo un while para contar los casos de prueba que faltan por leer en un problema
// Programa que imprime del n al 1
int casosDePrueba; cin >> casosDePrueba;
while (casosDePrueba--) {
// Procesar caso de prueba
}
// Imprimir del n al 1 pero saltándose el 3
int n = 5;
while (n > 0) {
cout << "Numero: ";
if (n == 3) continue;
cout << n << endl;
n--;
}
/*
Numero: 5
Numero: 4
Numero:
Numero: 2
Numero: 1
*/
do while
do while es una pequeña variante de while, con este bloque todo lo que haya dentro del do { ... } siempre se ejecuta al menos una vez. Si al finalizar el bloque do la condición del while es verdadera se vuelve a repetir el bloque do.
bool condicion = false;
do {
cout << "Solo me verás una vez" << endl;
} while (condicion);
int imprimirNVeces = 3;
do {
cout << "Hola" << endl;
--imprimirNVeces;
} while (imprimirNVeces > 0);
/*
Hola
Hola
Hola
^/
switch
Este bloque permite ejecutar un bloque u otro de código dependiendo del valor exacto de una variable. En el caso de debajo, la variable brujula debería contener N, E, S o O, entonces se ejecutará el bloque correspondiente e imprimirá la orientación.
Opcionalmente abajo del switch hay un bloque default: que se ejecuta si no hay ningún case con el valor correspondiente. Es muy importante poner break; después de cada bloque de código porque de lo contrario si el programa ejecuta un bloque pasará a ejecutar los siguientes también aunque el valor de la variable no corresponda con esos casos. Prueba a ver el resultado del siguiente bloque switch con y sin break.
char brujula = 'S';
switch (brujula) {
case 'N':
cout << "Norte" << endl;
break;
case 'E':
cout << "Este" << endl;
break;
case 'S':
cout << "Sur" << endl; // <--
break;
case 'O':
cout << "Oeste" << endl;
break;
case '1':
case '2':
case '3':
case '4':
cout << "Un número" << endl;
break;
default:
cout << "Y entonces a donde miras?" << endl;
}
// Bloque equivalente con bloques if
if (brujula == 'N') cout << "Norte" << endl;
else if (brujula == 'E') cout << "Este" << endl;
else if (brujula == 'S') cout << "Sur" << endl;
else if (brujula == 'O') cout << "Oeste" << endl;
else if (brujula == '1' || brujula == '2' || brujula == '3' || brujula == '4') cout << "Un número" << endl;
else cout << "Y entonces a donde miras?" << endl;
Arrays unidimensionales
Un array es un conjunto de elementos del mismo tipo almacenados en ubicaciones de memoria contiguas. Podemos imaginarnos una string s = "prueba"; como un array de 6 carácteres, los 6 se guardan en orden en la memoria, uno detrás de otro. Las posiciones de un array se empiezan a contar desde 0, decimos que la PRIMERA posición es la posición 0. Entonces si la primera posición del array de carácteres s se encuentra en la posición 100000 de la RAM, la posición 1 se encontrará en la posición 100001 y así sucesivamente. Los arrays siempre tienen el mismo tamaño desde que se declaran, quiere decir que si un array empieza con 10 elementos, no se puede hacer más grande o más pequeño, siempre tendrá 10 posiciones.
Trabajando con éstos
Para declarar arrays se hace de las siguientes formas:
Es muy importante saber que en C++ cuando se declara un array de elementos primitivos como
int,double... todos los elementos son inicialmente números aleatorios (lo que había en la memoria antes del array)
// Sin inicializar (En cada posición habrá un número random)
int miArray[10];
// miArray[0] = ???
// miArray[5] = ???
int n; cin >> n;
int arr[n]; // Tamaño n, dado por el usuario, no mayor a 10^6
// Inicializar todos los elementos a 0
memset(miArray, 0, sizeof(miArray));
// Igual que memset() pero menos eficiente
for (int i = 0; i < 10; ++i) {
miArray[i] = 0;
}
// Inicializar con valores predeterminados (poco común)
int miArray[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
Para el ejemplo de encima, hemos creado un array de longitud 10, con índices del 0 al 9 (se empieza a contar por el 0); por tanto tenemos diez posiciones en memoria reservadas para guardar ints.
¿Cómo accedemos para modificar y/u obtener los valores de los elementos de nuestro array? Al final un array de 10 posiciones son como 10 variables, pues nos referiremos a una de las variables por el nombre del array y su posición:
// Asignar 10 a la TERCERA posición del array
miArray[2] = 10;
// Asignar el valor de la variable `a` + 4 a la PRIMERA posición del array
int a = 22;
miArray[0] = a + 4;
// Copiar la SEXTA posición del array a la variable a
a = miArray[5];
// Cualquier operación es posible con los elementos de un array
miArray[0] += miArray[1] - 2;
miArray[0]--;
if (miArray[0] > 5) { ... }
¿Qué ocurre si consultamos elementos fuera del array? Es decir, si para el ejemplo de un array de longitud diez, ¿consultamos el elemento en la ONCEAVA posición o más? El compilador de C++ nos avisará en algunos casos que estamos intentando acceder a una posición en el array que no existe. Si el tamaño del array es desconocido durante la compilación entonces podría ocurrir un 'Segmentation fault' por acceder a una posición no permitida en la memoria y el programa crasheará, o podría devolver un valor aleatorio.
Ejemplo
#include <bits/stdc++.h>
using namespace std;
int main() {
int arr[10]; // creo un array de 10 elementos
memset(arr, 0, sizeof(arr)); // Inicializo todo a '0'
cout << arr[0] << endl; // 0
arr[1]=42;
cout << arr[1] << endl; // 42
cout << arr[1000000] << endl; // Segmentation fault o número random
return 0;
}
Vectores
Un vector es una secuencia dinámica que puede cambiar de tamaño automáticamente cuando se añaden o eliminan elementos. Los vectores son parte de la biblioteca estándar de C++ (<vector>) y proporcionan muchas funcionalidades útiles que no están disponibles en los arrays tradicionales.
Trabajando con éstos
Para crear un vector se hace de la siguiente manera:
// Vector que guarda `int` con tamaño inicial 0
vector<int> a;
// Valores random!
// Vector que guarda `int` con tamaño inicial n
int n = 10;
vector<int> b(n);
// Valores random!
// Vector que guarda `int` con tamaño inicial 10 y todos los elementos inicializados a 0
vector<int> miVector(10, 0);
// Vector con 10 elementos `int` predeterminados
vector<int> c = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
¿Cómo accedemos para modificar y/u obtener los valores de los elementos de nuestro vector? De la misma forma que con los arrays, siempre asegurándonos que esa posición verdaderamente existe y no se sale fuera de límites.
// Vector con 10 elementos `int` predeterminados
vector<int> miVector = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
// Asignar 10 a la TERCERA posición del vector
miVector[2] = 10;
// Asignar el valor de la variable `a` + 4 a la PRIMERA posición del vector
int a = 22;
miVector[0] = a + 4;
// Copiar la SEXTA posición del vector a la variable a
a = miVector[5];
// Pero además podemos eliminar el último elemento del vector
miVector.pop_back();
// Ahora el tamaño del vector
// Añadir el valor `5` al final del vector
miVector.push_back(5);
También en los vectores cuando los creamos los elementos no se inicializan automáticamente a un valor por defecto a menos que se especifique. Podemos inicializar un vector con un tamaño y un valor por defecto usando la sintaxis
vector<int> miVector(10, 0), lo que creará un vector de tamaño 10 con todos sus elementos inicializados a cero. También podemos usar memset().
#include <bits/stdc++.h>
using namespace std;
int main() {
vector<int> vec; // creo un vector de enteros
// Añadir elementos al final del vector
vec.push_back(10);
vec.push_back(20);
cout << vec[0] << endl; // 10
cout << vec[1] << endl; // 20
// Acceder y modificar elementos
vec[0] = 42;
cout << vec[0] << endl; // 42
// Obtener el tamaño del vector
cout << "Tamaño del vector: " << vec.size() << endl; // 2
// Eliminar el último elemento
vec.pop_back();
cout << "Tamaño del vector después de pop_back: " << vec.size() << endl; // 1
// Comprobar si el vector está vacío
cout << "¿El vector está vacío? " << (vec.empty() ? "Sí" : "No") << endl; // No
// Limpiar todos los elementos del vector
vec.clear();
cout << "Tamaño del vector después de clear: " << vec.size() << endl; // 0
return 0;
}
Arrays y Vectores bidimensionales
Un array bidimensional es una estructura de datos que permite almacenar elementos en forma de matriz, es decir, en filas y columnas. Cada elemento del array bidimensional se puede acceder mediante dos índices: uno para la fila y otro para la columna. Se pueden interpretar como un "array de arrays". (De hecho se pueden hacer arrays de múltiples dimensiones)
Array 2D Clásicos (Tamaño estático)
Para crear un array bidimensional estático (cuando sabes el tamaño exacto de antemano), se declara indicando el número de filas y columnas entre corchetes:
// Array 2D de 3 filas y 4 columnas. Inicializado con valores aleatorios!
int miMatriz[3][4];
// Asignar el valor 1 al elemento en la fila 0, columna 1
miMatriz[0][1] = 1;
// Leer un valor y guardarlo en una variable
int a = miMatriz[0][1];
Nota: las propiedades de acceso y problemas sobre los arrays y sus tipos se heredan cuando trabajamos con arrays bidimensionales (No podemos acceder a valores de posiciones que no existen).
Vectores Bidimensionales (Tamaño dinámico)
En programación competitiva, a menudo no conocemos el tamaño de la matriz hasta que leemos los datos del problema. Aquí es donde brillan los vectores bidimensionales. Su sintaxis es un poco especial porque se declaran como un vector que contiene otros vectores: vector<vector<int>>.
// 1. Crear un Vector 2D completamente vacío
vector<vector<int>> matrizVacia;
// 2. Crear un Vector 2D de tamaño N x M, y rellenarlo todo con ceros
int n = 3; // filas
int m = 4; // columnas
vector<vector<int>> matriz(n, vector<int>(m, 0));
Ejemplo
// --- ARRAY 2D ---
// Inicialización directa de un array de 3x4 Representación visual de la matriz:
int arr[3][4] = { 0 1 2 3
{1, 2, 3, 4}, 0: 1 2 3 4
{5, 6, 7, 8}, 1: 5 6 7 8
{9, 10, 11, 12} 2: 9 10 11 12
};
cout << "Array Fila 1, Columna 2: " << arr[1][2] << endl; // Imprime 7
// --- VECTOR 2D ---
// Matriz de 3x4 inicializada a 0
vector> vec(3, vector(4, 0));
// Modificamos un valor
vec[0][0] = 42;
cout << "Vector Fila 0, Columna 0: " << vec[0][0] << endl; // Imprime 42
// Comprobar el tamaño (muy útil para los bucles for)
cout << "Numero de filas: " << vec.size() << endl; // Imprime 3
cout << "Numero de columnas: " << vec[0].size() << endl; // Imprime 4
Array vs Vector
A simple vista, comprendiendo las propiedades y características de estas estructuras de datos antes mencionadas, podemos destacar ciertas ventajas y desventajas:
- Mientras que los arrays, en general, tienen un tamaño fijo, los vectores son capaces de manejar tamaño dinámico, por lo que iterar un array puede ser más o menos cómodo respecto un vector.
- Cada clase puede tener más o menos soporte respecto otras librerías que puedan ayudarnos a trabajar con más comodidad, pueda ser el caso de alguna estructura de datos que NO tenga un iterador definido respecto otras que SÍ.
- A nivel de seguridad de la memoria, los vectores lanzan excepciones usando los métodos adecuados si se accediera de forma incorrecta a sus datos, mientras que con los arrays se puede acceder libremente.
Nota: es importante saber usar todos los tipos de estructura e implementar soluciones según las necesidades; es posible que para ciertos problemas un vector o un array se moldee mejor a éste.
Iterando sobre vectores
Como acabamos de ver en la sección de control de flujo, los bucles nos permiten repetir instrucciones. Si aplicamos esto a los arrays y vectores que acabamos de conocer, obtenemos la herramienta más práctica para leer, modificar y analizar grandes cantidades de datos.
for "tradicional" aplicado a vectores
Ya conocemos la estructura de un bloque for con su contador i. Para recorrer una colección de datos de principio a fin, solo necesitamos que nuestro contador empiece en 0 (la primera posición) y se detenga antes de superar el tamaño total. Aunque se puede iterar sobre arrays estáticos, en programación competitiva es mucho más común y seguro utilizar vectores, ya que nos facilitan la vida permitiéndonos consultar su tamaño exacto en cualquier momento con el método .size(). La condición de nuestro bucle será, simplemente, que el contador i sea estrictamente menor que el tamaño del vector. Veamos cómo encaja todo:
// Inicializamos un vector con 5 elementos
vector numeros = {10, 20, 30, 40, 50};
// Iteramos usando el tamaño dinámico del vector con .size()
for (int i = 0; i < numeros.size(); ++i) {
int temp = numeros[i];
cout << temp << endl;
}
En este caso, el contador i tomará los valores 0, 1, 2, 3, 4. Al llegar a 5, la condición i < numeros.size() ya no se cumple y el bucle termina, evitando así acceder a memoria fuera del vector. La salida en consola será:
10
20
30
40
50
De la misma forma que accedemos a los elementos para leerlos, podemos modificarlos en cada paso numeros[i] += 5; o introducir condicionales if dentro del bucle para aplicar reglas o descartar valores. Estas son exactamente las estrategias que aplicarás en los problemas de este bootcamp.
foreach
Cuando buscamos un recorrido sin preocuparnos mucho por el contador característico del for "tradicional", podemos optar por los foreach. Los foreach son una implementación más cómoda de un recorrido, con una sentencia clara y limpia. Cabe aclarar que los foreach no son universales, es decir, un foreach es funcional porque alguien lo ha definido antes, es decir, por cada tipo de variable se define un iterador que te permite iterar sobre ese tipo, si fuera una estructura de datos; por ende no siempre se podrá usar un foreach, pero para los casos de programación competitiva no debería haber muchas pegas.
Por tanto, un recorrido de un array, vector, etc... se puede simplificar en este bloque:
#include <bits/stdc++.h>
using namespace std;
int main() {
int arr[5] = {1,2,3,4,5};
for(int n : arr) {
cout << n << endl;
}
}
Anexo
Prioridad de operadores

Operador de incremento y decremento
Quizás en el temario has visto esto: ++i. Esto es la operación de incremento, incrementa la variable por 1:
int a = 4;
++a; // 5 (preincremento)
a++; // 6 (postincremento)
--a; // 5 (predecremento)
a--; // 4 (postdecremento)
¿Y cuál es la diferencia entre pre y post incremento/decremento? En una expresión, el pre incremento/decremento ocurre antes de la evalúación de esa expresión, mientras que el post incremento/decremento ocurre después de la evaluación:
int a, b;
a = 2;
b = 1 + a++ // b = 3, luego a = a+1
a = 2;
b = 1 + ++a // primero a = a+1 (3), luego b = 4
Estrategias, tipos de problema y consejos
Ya conociendo la forma de iterar sobre las estructuras, habiendo asimilado la lógica y criterios detrás del concepto de iterar, podemos agrupar los problemas de vectores, arrays y arrays 2D en tres tipos:
- Recorridos: en estos problemas la solución a implementar implica recorrer todos los elementos de una estructura de datos para poder concluir una respuesta.
- Búsquedas: en estos problemas la solución implica generalmente encontrar un elemento o conjunto de elementos que cumplan ciertas condiciones sujetas al problema, y deternerse si no fuera necesario seguir buscando.
- Filtrado: en estos problemas la dinámica consiste en generar una estructura de datos a partir de una dada, por ejemplo, un método al que si le pasas una matriz, te devuelva la matriz diagonal de esa matriz dada.
Adicionalmente, añadimos un pequeño sumario aquí para aplicar durante este capítulo, métodos que puedan llegar a serles útiles:
memset: Inicialización de memoria.- Usado para establecer valores específicos en bloques de memoria. Útil para inicializar arrays y estructuras de datos a un valor determinado.
- Ejemplo:
#include <bits/stdc++.h> using namespace std; int main() { int arr[10]; memset(arr, 0, sizeof(arr)); // Inicializa todos los elementos a 0 for (int i = 0; i < 10; ++i) { cout << arr[i] << " "; // Imprime: 0 0 0 0 0 0 0 0 0 0 } return 0; }
sizeof: Tamaño de objetos en memoria.- Usado para obtener el tamaño en bytes de un objeto o tipo de datos. Útil, aplicando un pequeño truco, para obtener el número de elementos de un array, etc... El problema que tenemos con este método es, que como se define, obtenemos en bytes la longitud de la estructura TOTAL.
- Ejemplo:
#include <bits/stdc++.h> using namespace std; int main() { int arr[10]; cout << "Tamaño del array: " << sizeof(arr) << " bytes" << endl; // Imprime longitud total del array cout << "Número de elementos: " << sizeof(arr) / sizeof(arr[0]) << std::endl; // imprime numero de elementos return 0; }