Las relaciones discretas modelan vínculos reales entre entidades de un programa: usuarios y permisos, productos y categorías, nodos y caminos, datos y transformaciones. Son la base teórica de bases de datos, control de acceso, grafos y algoritmos.
Hasta ahora hemos estudiado las relaciones y funciones discretas desde una perspectiva matemática abstracta. En este tema veremos cómo esos conceptos resuelven problemas concretos de programación.
Cada vez que un programa necesita modelar un vínculo entre dos categorías de objetos—usuarios y roles, artículos y categorías, ciudades y carreteras, datos y procesadores—está usando una relación discreta. La eficiencia, corrección y seguridad del programa dependen de cómo se implemente esa relación.
Este tema recorre las aplicaciones más comunes en el desarrollo de software real.
En un sistema multiusuario, la relación entre usuarios y roles define quién puede hacer qué. Esta es una relación binaria que típicamente se guarda en una tabla de base de datos o en memoria.
La relación se puede representar como pares ordenados:
En JavaScript, esto se implementa típicamente con un objeto o un mapa:
// Enfoque 1: Objeto con arrays de roles
const usuarioRoles = {
"Ana": ["admin", "auditor"],
"Luis": ["editor"],
"Marta": ["viewer"]
};
// Enfoque 2: Array de pares
const usuarioRol = [
["Ana", "admin"],
["Ana", "auditor"],
["Luis", "editor"],
["Marta", "viewer"]
];
// Verificar si un usuario tiene un rol
function tieneRol(usuario, rol) {
return usuarioRoles[usuario]?.includes(rol) ?? false;
}
console.log(tieneRol("Ana", "admin")); // true
console.log(tieneRol("Luis", "admin")); // false
// Obtener todos los usuarios de un rol
function usuariosConRol(rol) {
return Object.keys(usuarioRoles).filter(u =>
usuarioRoles[u].includes(rol)
);
}
console.log(usuariosConRol("admin")); // ["Ana"]
console.log(usuariosConRol("viewer")); // ["Marta"]
// Asignar un nuevo rol (agregar un par a la relación)
function asignarRol(usuario, rol) {
if (!usuarioRoles[usuario]) {
usuarioRoles[usuario] = [];
}
if (!usuarioRoles[usuario].includes(rol)) {
usuarioRoles[usuario].push(rol);
}
}
asignarRol("Luis", "admin");
console.log(tieneRol("Luis", "admin")); // true
Las bases de datos relacionales fundamentan su arquitectura en relaciones. Una tabla es una relación donde cada fila es una tupla y cada columna es un atributo.
Productos y una tabla Categorías. Cada producto pertenece a una categoría (función). Múltiples productos pueden estar en la misma categoría (relación no inyectiva).
Representación abstracta:
En JavaScript, modelamos esta relación con arrays y referencias:
// Base de datos en memoria
const productos = [
{ id: "P1", nombre: "Laptop", categoriaId: "C1" },
{ id: "P2", nombre: "Mouse", categoriaId: "C1" },
{ id: "P3", nombre: "Camiseta", categoriaId: "C2" },
{ id: "P4", nombre: "Manzana", categoriaId: "C3" }
];
const categorias = [
{ id: "C1", nombre: "Electrónica" },
{ id: "C2", nombre: "Ropa" },
{ id: "C3", nombre: "Alimentos" }
];
// Proyección: obtener todos los productos de una categoría
function productosDeCategoria(nombreCategoria) {
const cat = categorias.find(c => c.nombre === nombreCategoria);
if (!cat) return [];
return productos.filter(p => p.categoriaId === cat.id);
}
console.log(productosDeCategoria("Electrónica"));
// [{ id: "P1", nombre: "Laptop", categoriaId: "C1" },
// { id: "P2", nombre: "Mouse", categoriaId: "C1" }]
// Join: obtener productos con sus categorías
function productosConCategoria() {
return productos.map(p => {
const cat = categorias.find(c => c.id === p.categoriaId);
return { ...p, categoria: cat.nombre };
});
}
console.log(productosConCategoria());
// [{ id: "P1", ..., categoria: "Electrónica" },
// { id: "P2", ..., categoria: "Electrónica" },
// ... ]
// Conteo: cuántos productos hay en cada categoría
function productosPorCategoria() {
const resultado = {};
for (const cat of categorias) {
resultado[cat.nombre] = productos.filter(
p => p.categoriaId === cat.id
).length;
}
return resultado;
}
console.log(productosPorCategoria());
// { Electrónica: 2, Ropa: 1, Alimentos: 1 }
Un grafo dirigido es una relación binaria sobre un conjunto de nodos. La relación "hay arista de X a Y" define la estructura del grafo.
Relación de amistad bidireccional:
Implementación en JavaScript (lista de adyacencia):
// Grafo como lista de adyacencia
const grafo = {
"Ana": ["Luis", "Marta"],
"Luis": ["Ana", "Marta"],
"Marta": ["Ana", "Luis", "Carlos"],
"Carlos": ["Marta"]
};
// ¿Están conectados directamente?
function sonAmigos(u1, u2) {
return grafo[u1]?.includes(u2) ?? false;
}
console.log(sonAmigos("Ana", "Luis")); // true
console.log(sonAmigos("Ana", "Carlos")); // false
// Amigos en común
function amigosEnComun(u1, u2) {
if (!grafo[u1] || !grafo[u2]) return [];
return grafo[u1].filter(a => grafo[u2].includes(a));
}
console.log(amigosEnComun("Ana", "Carlos"));
// ["Marta"]
// Vecinos de distancia 2 (amigos de mis amigos)
function amigosDeAmigos(usuario) {
const amigos = grafo[usuario] || [];
const aDA = new Set();
for (const amigo of amigos) {
for (const amigoDelAmigo of grafo[amigo] || []) {
if (amigoDelAmigo !== usuario && !amigos.includes(amigoDelAmigo)) {
aDA.add(amigoDelAmigo);
}
}
}
return Array.from(aDA);
}
console.log(amigosDeAmigos("Ana"));
// ["Carlos"]
// Recorrido en profundidad (DFS) desde un nodo
function dfs(inicio, visitados = new Set()) {
if (visitados.has(inicio)) return [];
visitados.add(inicio);
const resultado = [inicio];
for (const vecino of grafo[inicio] || []) {
resultado.push(...dfs(vecino, visitados));
}
return resultado;
}
console.log(dfs("Ana"));
// ["Ana", "Luis", "Marta", "Carlos"]
Las funciones discretas se usan para mapear datos de un conjunto a otro. Un diccionario de traducciones, un índice invertido o una tabla de símbolos son funciones.
// Tabla de símbolos: identificador → {tipo, dirección}
const tablaSímbolos = {
"x": { tipo: "int", dirección: 1000 },
"nombre": { tipo: "string", dirección: 1008 },
"valores": { tipo: "array[int]", dirección: 1016 }
};
// Consultar tipo de una variable
function obtenerTipo(id) {
return tablaSímbolos[id]?.tipo ?? "desconocido";
}
console.log(obtenerTipo("x")); // "int"
console.log(obtenerTipo("nombre")); // "string"
// Índice invertido: palabra → lista de documentos que la contienen
const indiceInvertido = {
"javascript": [0, 2, 4],
"python": [1, 3],
"programación": [0, 1, 2, 3, 4],
"web": [0, 2]
};
// Buscar documentos que contienen una palabra
function buscar(palabra) {
return indiceInvertido[palabra] ?? [];
}
console.log(buscar("javascript"));
// [0, 2, 4]
// Búsqueda AND: documentos que contienen todas las palabras
function buscarAND(...palabras) {
const resultado = indiceInvertido[palabras[0]] ?? [];
return resultado.filter(doc =>
palabras.every(pal =>
(indiceInvertido[pal] ?? []).includes(doc)
)
);
}
console.log(buscarAND("javascript", "web"));
// [0, 2]
// Aplicación: Conversión de unidades (función discreta)
const conversiones = {
"km_a_m": (x) => x * 1000,
"m_a_km": (x) => x / 1000,
"kg_a_g": (x) => x * 1000,
"g_a_kg": (x) => x / 1000
};
function convertir(cantidad, conversión) {
const fn = conversiones[conversión];
return fn ? fn(cantidad) : null;
}
console.log(convertir(5, "km_a_m")); // 5000
console.log(convertir(3000, "m_a_km")); // 3
Una máquina de estados finitos define una relación entre estados y transiciones. Dado un estado actual y una entrada, la transición determina el nuevo estado.
// Máquina de estados para un reproductor
const transiciones = {
"detenido": {
"play": "reproduciendo",
"pause": "detenido",
"stop": "detenido"
},
"reproduciendo": {
"play": "reproduciendo",
"pause": "pausado",
"stop": "detenido"
},
"pausado": {
"play": "reproduciendo",
"pause": "pausado",
"stop": "detenido"
}
};
class Reproductor {
constructor() {
this.estado = "detenido";
}
transicionar(acción) {
const siguiente = transiciones[this.estado]?.[acción];
if (siguiente) {
this.estado = siguiente;
return true;
}
return false;
}
obtenerEstado() {
return this.estado;
}
}
const player = new Reproductor();
console.log(player.obtenerEstado()); // "detenido"
player.transicionar("play");
console.log(player.obtenerEstado()); // "reproduciendo"
player.transicionar("pause");
console.log(player.obtenerEstado()); // "pausado"
player.transicionar("play");
console.log(player.obtenerEstado()); // "reproduciendo"
player.transicionar("stop");
console.log(player.obtenerEstado()); // "detenido"
Las relaciones y funciones discretas no son conceptos abstractos sin utilidad. Son la base teórica de prácticamente toda la programación real: desde controlar quién accede a qué, hasta navegar redes sociales, procesar consultas de bases de datos y construir compiladores.
En los próximos temas exploraremos aplicaciones más especializadas en estructuras de datos, bases de datos, algoritmos, grafos e inteligencia artificial, siempre con relaciones discretas como fundamento conceptual.