CP-UPV Bootcamp - Capítulo 1

Capítulo 1: Tipos de datos, operaciones y entrada/salida

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

Bienvenido al primer capítulo consolidado de nuestro Bootcamp de Competitive Programming. En este capítulo, exploraremos los fundamentos de los tipos de datos, las operaciones aritméticas que podemos realizar con ellos y las operaciones de entrada/salida. Al final de este capítulo, tendrás un conocimiento sólido sobre cómo trabajar con la información básica en C++, cómo operar con ella y cómo comunicarte con el programa. A través de diversos ejemplos y explicaciones detalladas, aprenderás a combinar estos conceptos para resolver problemas.

Contenidos

Tipos comunes en C++

char

El tipo char se utiliza para almacenar un solo carácter. Este tipo de dato ocupa 1 byte de memoria y puede representar cualquier carácter de la tabla ASCII.

Detalles y usos

Ejemplo
#include <bits/stdc++.h>
using namespace std;

int main() {
    char letter = 'A';
    cout << "Character: " << letter << endl;
    return 0;
}

int

El tipo int se utiliza para almacenar números enteros. La cantidad de memoria que ocupa puede variar dependiendo del compilador y la arquitectura del sistema, pero generalmente es de 4 bytes.

Detalles y usos

Ejemplo
#include <bits/stdc++.h>
using namespace std;

int main() {
    int age = 25;
    cout << "Age: " << age << endl;
    return 0;
}

long long

El tipo long long se utiliza para almacenar números enteros grandes. Este tipo de dato ocupa 8 bytes de memoria y puede representar valores mucho mayores que el tipo int.

Detalles y usos

Ejemplo
#include <bits/stdc++.h>
using namespace std;

int main() {
    long long distance = 123456789012345LL;
    cout << "Distance: " << distance << endl;
    return 0;
}

double

El tipo double se utiliza para almacenar números de punto flotante de doble precisión. Este tipo de dato ocupa 8 bytes de memoria y ofrece mayor precisión que el tipo float.

Detalles y usos

Ejemplo
#include <bits/stdc++.h>
using namespace std;

int main() {
    double e = 2.718281828459;
    cout << "e: " << e << endl;
    return 0;
}

string

En C++, la clase std::string (de la biblioteca estándar) proporciona una forma sencilla y eficiente de manejar texto. Permite realizar operaciones comunes como declarar, asignar, concatenar y acceder a subcadenas o caracteres individuales.

Declaración y asignación

Para declarar y asignar una cadena de caracteres, usamos la clase std::string.

Ejemplo
#include <bits/stdc++.h>
using namespace std;

int main() {
    // 1. Declaración y asignación
    string greeting = "Hello, World!";
    cout << greeting << endl;

    string name;
    cout << "Enter your name: ";
    cin >> name;
    cout << "Hello, " << name << "!" << endl;

    // 2. Concatenación
    string firstName = "John";
    string lastName = "Doe";
    string fullName = firstName + " " + lastName;
    cout << "Full Name: " << fullName << endl;

    // 3. Acceso a caracteres
    string text = "Hello";
    char firstChar = text[0];
    char lastChar = text[text.length() - 1];
    cout << "First Character of " << text << ": " << firstChar << endl;
    cout << "Last Character of " << text << ": " << lastChar << endl;

    // 4. Longitud de la cadena
    cout << "Length of the string '" << text << "': " << text.length() << endl;

    return 0;
}

Variables signed y unsigned

En C++, las variables pueden ser signed (con signo) o unsigned (sin signo). Una variable signed puede almacenar tanto números positivos como negativos, mientras que una variable unsigned solo puede almacenar números positivos. La elección entre signed y unsigned depende del contexto en el que se usará la variable.

signed

Una variable signed puede almacenar valores negativos y positivos. Por defecto, las variables de tipo int, char, y long son signed a menos que se especifique lo contrario.

Ejemplo
#include <bits/stdc++.h>
using namespace std;

int main() {
    signed int negativeNumber = -10;
    cout << "Signed Variable: " << negativeNumber << endl;
    return 0;
}

unsigned

Una variable unsigned solo puede almacenar valores positivos, lo que permite representar el doble de valores positivos en comparación con una variable signed del mismo tamaño.

Ejemplo
#include <bits/stdc++.h>
using namespace std;

int main() {
    unsigned int positiveNumber = 10;
    cout << "Unsigned Variable: " << positiveNumber << endl;

    unsigned int positiveNumber = -10; // Dará problemas

    return 0;
}

Entrada y salida de datos

Entrada con cin

La entrada de datos en C++ se realiza con el objeto cin (entrada estándar). Lee datos desde la entrada estándar (generalmente el teclado) utilizando el operador de extracción >>. cin puede diferenciar automáticamente entre varios tipos de datos.

Leer Enteros y Flotantes
#include <bits/stdc++.h>
using namespace std;

int main() {
    int age;
    float height;

    cout << "Enter your age: ";
    cin >> age;

    cout << "Enter your height: ";
    cin >> height;

    cout << "Your age is: " << age << endl;
    cout << "Your height is: " << height << endl;

    return 0;
}

Salida con cout

La salida de datos en C++ se realiza con el objeto cout (salida estándar). cout escribe datos en la salida estándar (generalmente la pantalla). Utilizamos el operador de inserción << para enviar datos a cout.

Ejemplo de Salida
#include <bits/stdc++.h>
using namespace std;

int main() {
    cout << "Hello, World!" << endl;

    int year = 2024;
    cout << "The current year is: " << year << endl;

    return 0;
}

Lectura de líneas enteras con getline

Para leer una línea completa de texto, utilizamos la función getline, que lee hasta encontrar un salto de línea (\n).

Leer una Línea Completa de Texto

#include <bits/stdc++.h>
using namespace std;

int main() {
    string fullName;

    cout << "Enter your full name: ";
    cin.ignore(); // Ignorar el salto de línea restante
    getline(cin, fullName);

    cout << "Your full name is: " << fullName << endl;

    return 0;
}

Funcionamiento interno de cin

El objeto cin en C++ es extremadamente versátil y puede manejar diferentes tipos de entrada de manera inteligente. Esto se logra a través de una combinación de técnicas de lectura y manejo de flujo de entrada.

Detección automática de tipos

cin puede diferenciar automáticamente entre varios tipos de datos debido a la sobrecarga de operadores en C++. El operador >> está sobrecargado para diferentes tipos de datos como int, float, string, etc.

#include <bits/stdc++.h>
using namespace std;

int main() {
    int a;
    float b;
    string c;

    cout << "Enter an integer: ";
    cin >> a;

    cout << "Enter a float: ";
    cin >> b;

    cout << "Enter a string: ";
    cin >> c;

    cout << "Integer: " << a << ", Float: " << b << ", String: " << c << endl;
    return 0;
}

En este ejemplo, cin reconoce y maneja diferentes tipos de datos basándose en el tipo de las variables.

Manejo de espacios y saltos de línea

Por defecto, cin ignora los espacios en blanco y los saltos de línea al leer datos. Esto significa que cin detendrá la lectura de una cadena de texto cuando encuentre un espacio en blanco, a menos que utilicemos getline.

#include <bits/stdc++.h>
using namespace std;

int main() {
    string name;
    cout << "Enter your full name: ";
    cin.ignore();
    getline(cin, name);
    cout << "Hello, " << name << "!" << endl;
    return 0;
}

En este ejemplo, getline lee la línea completa de entrada, incluyendo espacios en blanco.

Overflows y underflows

Los overflows y underflows en las operaciones ocurren debido a que, debido a la operación que se realiza y los operandos que se utilizan, se supera el límite superior o inferior del tipo de dato resultado de la operación, por lo que el resultado de esta no tendrá nada que ver con lo que esperábamos.

Uno de los casos que más nos suele ocurrir al empezar en programación competitiva es que no verificamos si nuestras operaciones producirán un overflow/underflow, y en general, esto se puede resolver simplemente cambiando el tipo de dato a uno de 64 bits, como por ejemplo, de int a long long.

#include <bits/stdc++.h>
using namespace std;

int main(int argc, char const *argv[]) {
    int a = 2147483647;
    int b = 2147483647;
    cout << a + b << endl; // El resultado es -2

    long long num1 = 2147483647;
    long long num2 = 2147483647;
    cout << num1 + num2 << endl; // Ahora da 4294967294

    return 0;
}

Operaciones básicas

Suma y Resta

Podemos realizar sumas de cualquier tipo de dato numérico, e incluso mezclando estos tipos de datos. Con la resta ocurre lo mismo que con la suma, podemos mezclar los tipos de datos, siempre y cuando tengamos en cuenta los posibles overflows.

#include <bits/stdc++.h>
using namespace std;

int main(int argc, char const *argv[]) {
    int a = 3;
    int b = 9;
    float c = 30.3;
    
    cout << a + b << " es suma de enteros" << endl;
    // 12

    cout << b - a << " es resta de enteros" << endl;
    // 6

    cout << a + c << " es una suma de entero y número de coma flotante" << endl;
    // 33.3

    double doblePrecision = 2878.1235;
    long long numLargo = 182391872;
    a = -12234;

    cout << doblePrecision + numLargo + a << " no importa que sumandos escojamos" << endl;
    // 1.82383e+08

    int num1 = 2147483647;
    int num2 = 2147483647;

    cout << num1 + num2 << " aquí hay un problema de overflow" << endl;
    // -2

    long long num3 = 9223372036854775807;
    long long num4 = 9223372036854775807;

    cout << num3 + num4 << " otro problema" << endl;
    // -2

    return 0;
}

Debemos tener en cuenta que se pueden dar overflows, un problema que en muchos casos puede causar más de un dolor de cabeza cuando estamos programando, lo explicaremos más adelante.

Multiplicación

Podemos realizar la operación de multiplicación con el símbolo *, y también podemos mezclar los tipos de los operandos, como con las operaciones anteriores.

#include <bits/stdc++.h>
using namespace std;

int main(int argc, char const *argv[]) {
    int a = 3;
    float b = 3.1415;

    cout << "a * b es: " << a * b << endl;
    // 9.4245

    return 0;
}

Una curiosidad que nos podemos encontrar es que, si por ejemplo necesitamos multiplicar/dividir por potencias de 2, estas operaciones son optimizadas por el compilador para transformarlas en shiftings, cuando es posible, ya que es una operación más rápida que la multiplicación.

División

La división se realiza con el operando /, y también nos podemos encontrar un caso especial, como es el de la división por 0: cuando dividimos entre 0, nos devuelve un valor undefined, ya que no podemos obtener una solución para esta operación.

Además, otro aspecto importante a tener en cuenta es la precisión de las operaciones, ya que podemos obtener un resultado más preciso si usamos un double en vez de un float, pero perdiendo un poco de velocidad.

#include <bits/stdc++.h>
using namespace std;

int main(int argc, char const *argv[]) {

    int a = 33;
    int b = 30;
    int cociente = a / b;  // La división nos devuelve el cociente de la operación
    
    cout << "a/b = " << cociente << endl;
    // 1

    cout << "a/0 = " << a / 0 << endl; // ERROR

    double num1 = 1.0, num2 = 3.0;
    
    // setprecision especifica las cifras decimales que queremos en el resultado
    cout << setprecision(17) << num1 / num2 << endl; 
    // 0.33333333333333331

    return 0;
}

Módulo

Representado con el símbolo % este nos devuelve el resto de una división, o siendo más precisos, el resultado de la operación \(a\ mod\ b = n\), esto último está relacionado con la aritmética modular (haz click aquí para saber más)

#include <bits/stdc++.h>
using namespace std;

int main(int argc, char const *argv[]) {
    
    // Muchas veces pedirán que demos un resultado modulo 1000000007, podemos usar:
    const int MOD = 1e9 + 7;  // 1000000007
    
    int a = 1231234;
    int b = 3231876;
    
    cout << (a + b) % MOD << endl;
    // 4463110
    
    return 0;
}

Otras operaciones

Existen funciones en C++ que nos permiten hacer operaciones más complejas, como pueden ser las siguientes:

pow()

pow(base, exponente), provista por la librería <cmath> nos permite calcular potencias de los números que le proporcionemos. Pudiendo devolver double, float o long double.

#include <math.h>

cout << pow(3, 4) << endl;
// 81

cout << pow(32.01, 1.54) << endl;
// 208.036691

Para ciertos valores, esta función dará lugar a errores, por eso debemos tener en cuenta que:

sqrt()

Aceptando también como argumentos double, float o long double, la función sqrt(num) no tiene mucho misterio, calcula la raíz cuadrada del tipo de dato que le proporcionemos, y, como es obvio, nos dará errores si tratamos de buscar la raíz cuadrada de un número negativo.

#include <math.h>

cout << sqrt(23) << endl;
// 4.79583

cout << sqrt(-1) << endl;
// -nan

log()

Para calcular logaritmos, nos encontramos con dos funciones que se diferencian por la base del logaritmo: log(), y log10().

log() calcula logaritmos naturales, mientras que log10() calcula logaritmos en base 10.

Debemos tener en cuenta que ambas funciones darán lugar a error si utilizamos números negativos, ya que con esas bases es imposible obtener números negativos.

#include <math.h>

cout << log(5.5) << endl;
// 1.704748
RECUERDA: Todas estas funciones no pueden ser precisas al 100% en todos los casos, ya que tenemos una cantidad limitada de bits para la representación de números.

Operaciones de formateo y manipulación de datos

Conversiones de tipos

En C++, podemos convertir explícitamente un tipo de dato a otro utilizando conversiones explícitas (esto se llama 'cast'), pero generalmente solo se puede hacer entre tipos que tienen sentido, por ejemplo float a int (se pierde la parte decimal), pero no se puede convertir una string a un int de la misma manera (claramente incompatibles en muchos casos, pero si que existe la función stoi("45") para hacer la conversión de una string que contiene un número a un int).

Conversión de int a float

#include <bits/stdc++.h>
using namespace std;

int main() {
    int num = 10;
    float numFloat = ((float) num);

    cout << "Integer: " << num << endl;
    cout << "Float: " << numFloat << endl;

    return 0;
}

Conversión de float a int

#include <bits/stdc++.h>
using namespace std;

int main() {
    float pi = 3.14;
    int piInt = ((int) pi);

    cout << "Float: " << pi << endl;
    cout << "Integer: " << piInt << endl;

    return 0;
}

Curiosidades

Cómo se guardan los enteros

Los números enteros, aquellos que no tienen punto decimal, se guardan en binario como una suma de potencias de \(2\) de la siguiente manera: de derecha a izquierda cada bit representa una potencia de \(2\) de mayor peso empezando por el \(1\) que es igual a \(2^0\). Este ejemplo con 16 bits lo simplifica un poco (El número \(100\) que conocemos de toda la vida es igual a \(1100100\) en binario):

Posición :   ...   15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
  ...   \(2^{15}\) \(2^{14}\) \(2^{13}\) \(2^{12}\) \(2^{11}\) \(2^{10}\) \(2^{9}\) \(2^{8}\) \(2^{7}\) \(2^{6}\) \(2^{5}\) \(2^{4}\) \(2^{3}\) \(2^{2}\) \(2^{1}\) \(2^{0}\)
\(2^{Posición}\) :   ...   32768 16384 8192 4096 2048 1024 512 256 128 64 32 16 8 4 2 1
Número \(100\) en binario:   ...   0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0
Suma de potencias de \(2\):   ...   0 0 0 0 0 0 0 0 0 64 32 0 0 4 0 0

Vemos como para el número \(100\), los bits en las posiciones \(2\), \(5\) y \(6\) son \(1\) y el resto \(0\). Ahora \(2^2 + 2^5 + 2^6 = 4 + 32 + 64 = 100\), y así se guardan los números enteros en binario, como sumas de potencias de \(2\).

Otro ejemplo, tenemos el siguiente número binario que hay que pasar a base-10 (los números que conocemos de toda la vida): \(1101101\). Vemos que de derecha a izquierda los bits en las posiciones \(0\), \(2\), \(3\), \(5\) y \(6\) están en 1. Si sumamos \(2^0\), \(2^2\), \(2^3\), \(2^5\) y \(2^6\) obtenemos \(109\) que es correcto.

También se puede convertir un número como \(66\) a binario, solo hay que descomponerlo en la suma de potencias de \(2\): \(66 = 64 + 2 = 2^6 + 2^1\). En binario si las posiciones \(1\) y \(6\) están en 1 obtendríamos \(1000010\), que es la representación correcta de \(66\)

Para los números negativos cambia un poco la cosa, el peso del primer 1 en el número binario pasa a ser esa base de \(2\) en negativo, por ejemplo el número \(111\) en binario es \(2^2 + 2^1 + 2^0 = 7\) si se interpreta como un número positivo pero \(-2^2 + 2^1 + 2^0 = -1\) si se interpreta como un número negativo, hay más detalles en la sección de Signed o Unsigned

Al principio no hay que saber estos detalles, pero sí es importante saber los números más grandes y pequeños que se pueden guardar en los tipos de datos más comunes de de C++ (hay más de los que se muestra en la tabla):

Tipo de dato Tamaño en bits Mínimo valor que se puede guardar Máximo valor que se puede guardar
char 8 bits -128 127
int 32 bits -2,147,483,648 2,147,483,647
long long 64 bits -9,223,372,036,854,775,808 9,223,372,036,854,775,807

Diferencia entre endl y “\n”

Muchas veces verás en el código de otras personas, que cuando tratan de hacer un salto de línea, algunos utilizan endl mientras que otros utilizan "\n". La diferencia entre estos dos es que: mientras el primero añade el salto de línea y vacía el buffer de salida, el segundo solamente añade el salto de línea.

Ten en cuenta que la salida, para reducir operaciones, puede ir almacenando la información en un buffer que se vaciará cuando llegue a un límite, pero al usar endl estás forzando que imprima sus contenidos, pudiendo incrementar el tiempo que tarda tu programa en ejecutarse, sobre todo si se realizan muchas operaciones de entrada y salida.

Entrada y salida más rápida

Relacionado con lo anterior, si quieres aprovechar este buffer, y estás más familiarizado con C++, puedes utilizar la siguiente configuración:

std::ios::sync_with_stdio(0); // Deshabilita sincronización con stdio
std::cin.tie(0);            // Separa cin de cout para que el buffer no se vacíe al cambiar de uno a otro

Redondear decimales

Para esta primera semana del bootcamp seguramente necesitarás redondear valores, existen 3 funciones útiles round(), ceil() y floor()

// Redondear un valor al entero más cercano
round(3.3333)       // = 3
round(3.5)          // = 4
round(3.4999999999) // = 4

// Redondear un valor al siguiente entero
ceil(3.3333)        // = 4
ceil(3.5)           // = 4
ceil(4.0)           // = 4

// Redondear un valor al anterior entero
floor(3.3333)       // = 3
floor(3.5)          // = 3
floor(4.0)          // = 4

Conclusión

En este capítulo, hemos cubierto los conceptos básicos de los tipos de datos y las operaciones de entrada/salida en C++. Estos fundamentos son cruciales para escribir programas eficientes y correctos en C++. Practica estos conceptos con los ejemplos proporcionados y experimenta con variaciones para fortalecer tu comprensión. En los siguientes capítulos, exploraremos temas más avanzados para expandir tus habilidades en programación competitiva, el viaje acaba de empezar.