TL;DR
- El Problema: Optimización de complejidad temporal y espacial para estructuras de datos clave.
- El Enfoque: Problema estilo CTCI 8.10 para principiantes: bote de pintura en una pantalla 2D de colores. Sustituye una region conexa con un color nuevo usando DFS recursivo o BFS iterativo en Java.
- Complejidad: Relación óptima de tiempo y espacio con gestión de casos límite.
Los editores de imagen tienen una herramienta de bote de pintura. Haces clic en un pixel, eliges un color nuevo, y todo el bloque conexo del color viejo cambia. La pantalla es un array 2D de valores de color. El clic es fila y columna. El trabajo es recolorear cada pixel al que puedas llegar moviendote arriba, abajo, izquierda y derecha sin salir del color original.
Este post es ensenanza original para principiantes en Java. Misma familia que las preguntas clasicas de flood fill en entrevistas, no una copia de un libro. Parte de la serie CTCI en Java. Capitulo 8, recursion y DP: busqueda en grafo sobre una rejilla, no una tabla de memo esta vez.
1. Analogia cotidiana
Piensa en un suelo de baldosas pintado a manchas de color. Te paras en una baldosa azul y quieres que cada baldosa azul a la que puedas caminar (compartiendo un borde, no solo una esquina) se pinte de rojo.
- Empieza en la baldosa del clic. Recuerda que era azul.
- Pintala de rojo.
- Mira los cuatro vecinos: norte, sur, este, oeste.
- Por cada vecino que siga azul, camina alli y haz lo mismo.
- Para cuando un vecino esta fuera de limites, ya es rojo, o nunca fue azul (pared, mancha verde, lo que sea).
No saltas en diagonal salvo que el entrevistador pida conectividad de ocho. No repintas baldosas que no eran el color original. Eso es flood fill: crece una region conexa hasta el borde de color.
Si el clic ya es el color nuevo, no hagas nada. Pintar azul a azul para siempre es un bug real (recursion infinita o un BFS que no termina).
2. Enunciado en palabras claras
Entrada:
screen: array 2D de colores (ints, enums o chars; en entrevistas suele serint[][]oColor[][])r,c: coordenadas del clicnewColor: color de relleno
Salida: la misma pantalla, con la region conexa del color original en (r, c) sustituida por newColor. Mutar in place o devolver el array; dilo en voz alta.
Conectividad (por defecto en este problema): cuatro direcciones, solo vecinos por borde:
(-1, 0), (1, 0), (0, -1), (0, 1)
Forma de la firma:
void paintFill(int[][] screen, int r, int c, int newColor);
// o con un enum / tipo Color
boolean paintFill(Color[][] screen, int r, int c, Color newColor);
Devolver boolean (hubo relleno?) es un detalle opcional de algunos esquemas de libro. Mutar con void basta.
Aclara en la entrevista:
- Vecinos de cuatro o de ocho?
- Reglas de limites y pantalla vacia?
- Que pasa si
(r, c)esta fuera de rango? - Clic del mismo color: no-op?
- Pueden ser null los colores (si el tipo es objeto)?
- Mutar la entrada o copiar?
Ejemplo pequeno:
Antes (clic en (1,1), color nuevo = 9):
1 1 1 2
1 1 0 2
1 0 1 2
Despues (relleno de cuatro del bloque de 1 arriba-izquierda):
9 9 9 2
9 9 0 2
9 0 1 2
El 1 solo del centro inferior se queda. Solo comparte esquina con la region rellenada, no un borde.
3. Piensa primero
Esto es busqueda en un grafo
Cada celda es un nodo. Hay arista a un vecino si esta dentro de limites y sigue con el color original. Flood fill es "visitar cada nodo del componente conexo de la celda de inicio y recolorear."
DFS (recursion o pila explicita) y BFS (cola) valen. Los entrevistadores aceptan cualquiera. Di el marco de grafo en voz alta; demuestra que no solo repites la historia del bote de pintura.
Captura el color original primero
oldColor = screen[r][c]
if oldColor == newColor: return
// luego inunda solo celdas iguales a oldColor
Si recoloras el inicio antes de leer oldColor, pierdes el objetivo. Si omites la salida temprana cuando los colores coinciden, el DFS reentra en celdas que acabas de pintar con newColor cuando newColor es lo que compruebas... en realidad compruebas oldColor, asi que si oldColor == newColor cada celda recolorada sigue "coincidiendo" y recursas para siempre. Protegelo.
Esquema DFS recursivo
function fill(r, c):
if out of bounds: return
if screen[r][c] != oldColor: return
screen[r][c] = newColor
fill(r-1, c); fill(r+1, c); fill(r, c-1); fill(r, c+1)
Entrada:
oldColor = screen[r][c]
if oldColor == newColor: return
fill(r, c)
Esquema BFS iterativo
queue.push(start)
screen[start] = newColor
while queue not empty:
cell = queue.pop
for each neighbor:
if in bounds and screen[neighbor] == oldColor:
screen[neighbor] = newColor
queue.push(neighbor)
Pinta al encolar (o marca visitado) para no encolar la misma celda dos veces. En una rejilla, cambiar de oldColor es la marca de visitado. No hace falta un boolean[][] aparte.
DFS vs BFS en la entrevista
| DFS recursivo | BFS iterativo | |
|---|---|---|
| Longitud de codigo | Corta | Un poco mas (cola + dirs) |
| Riesgo de pila | Una region serpiente larga puede reventar la pila de llamadas | Cola en heap; mas segura en pantallas enormes |
| Orden | Profundidad primero | Por niveles; mismo resultado final |
Para tamanos de entrevista, cualquiera vale. Menciona la profundidad de pila del DFS en una pantalla N x M de un solo color: peor profundidad cerca de N*M.
Por que esta en "Recursion and DP"
La escritura natural es recursiva. No hay tabla de memo elaborada. Los "subproblemas" son los vecinos. Sigue alineado con el capitulo: recursion sobre rejilla, misma familia que caminos de robot e inundacion de laberinto.
Boceto en la pizarra
- Dibuja una rejilla 3x4 con un bloque de color
1y otros colores. - Marca el clic. Escribe
old = 1,new = 9. - Recolorea el inicio y sigue las cuatro direcciones.
- Muestra una celda que detiene el relleno (otro color o limite).
- Nota la salida temprana si
old == new.
4. Solucion en Java
Ayudas compartidas
static final int[][] DIRS = {
{-1, 0}, {1, 0}, {0, -1}, {0, 1}
};
static boolean inBounds(int[][] screen, int r, int c) {
return r >= 0 && r < screen.length
&& c >= 0 && c < screen[0].length;
}
Asume una pantalla rectangular no vacia en el codigo de ensenanza. Protege arrays vacios en produccion.
DFS recursivo
/**
* Paint-bucket fill: recolor the 4-connected region of screen[r][c].
* Mutates screen in place.
*/
void paintFillDfs(int[][] screen, int r, int c, int newColor) {
if (screen == null || screen.length == 0 || screen[0].length == 0) {
return;
}
if (!inBounds(screen, r, c)) {
return;
}
int oldColor = screen[r][c];
if (oldColor == newColor) {
return;
}
fill(screen, r, c, oldColor, newColor);
}
void fill(int[][] screen, int r, int c, int oldColor, int newColor) {
if (!inBounds(screen, r, c)) {
return;
}
if (screen[r][c] != oldColor) {
return;
}
screen[r][c] = newColor;
for (int[] d : DIRS) {
fill(screen, r + d[0], c + d[1], oldColor, newColor);
}
}
BFS iterativo
void paintFillBfs(int[][] screen, int r, int c, int newColor) {
if (screen == null || screen.length == 0 || screen[0].length == 0) {
return;
}
if (!inBounds(screen, r, c)) {
return;
}
int oldColor = screen[r][c];
if (oldColor == newColor) {
return;
}
java.util.ArrayDeque<int[]> q = new java.util.ArrayDeque<>();
screen[r][c] = newColor;
q.add(new int[] {r, c});
while (!q.isEmpty()) {
int[] cell = q.removeFirst();
int cr = cell[0];
int cc = cell[1];
for (int[] d : DIRS) {
int nr = cr + d[0];
int nc = cc + d[1];
if (inBounds(screen, nr, nc) && screen[nr][nc] == oldColor) {
screen[nr][nc] = newColor;
q.add(new int[] {nr, nc});
}
}
}
}
ArrayDeque como cola es claro y rapido. Una cola enlazada manual vale en la pizarra.
Estilo opcional con enum Color
Algunos esquemas usan un enum para parecer pixeles "reales":
enum Color { RED, GREEN, BLUE, YELLOW }
boolean paintFill(Color[][] screen, int r, int c, Color newColor) {
if (screen == null || screen.length == 0) {
return false;
}
if (!inBoundsColor(screen, r, c)) {
return false;
}
Color oldColor = screen[r][c];
if (oldColor == newColor) {
return false;
}
fillColor(screen, r, c, oldColor, newColor);
return true;
}
void fillColor(Color[][] screen, int r, int c, Color oldColor, Color newColor) {
if (!inBoundsColor(screen, r, c)) {
return;
}
if (screen[r][c] != oldColor) {
return;
}
screen[r][c] = newColor;
fillColor(screen, r - 1, c, oldColor, newColor);
fillColor(screen, r + 1, c, oldColor, newColor);
fillColor(screen, r, c - 1, oldColor, newColor);
fillColor(screen, r, c + 1, oldColor, newColor);
}
boolean inBoundsColor(Color[][] screen, int r, int c) {
return r >= 0 && r < screen.length
&& c >= 0 && c < screen[0].length;
}
Mismo algoritmo. Los enums se leen bien cuando hablas de "colores" en vez de ints magicos.
Comprobaciones minimas
int[][] g = {
{1, 1, 1, 2},
{1, 1, 0, 2},
{1, 0, 1, 2}
};
paintFillDfs(g, 1, 1, 9);
assert g[0][0] == 9 && g[0][1] == 9 && g[0][2] == 9;
assert g[1][0] == 9 && g[1][1] == 9;
assert g[2][0] == 9;
assert g[1][2] == 0; // not part of the 1-region via edges
assert g[2][1] == 0;
assert g[2][2] == 1; // diagonal only; four-way leaves it
assert g[0][3] == 2;
int[][] same = {{3, 3}, {3, 3}};
paintFillBfs(same, 0, 0, 3); // no-op, must not hang
assert same[1][1] == 3;
int[][] one = {{5}};
paintFillBfs(one, 0, 0, 7);
assert one[0][0] == 7;
5. Tabla de complejidad
| Enfoque | Tiempo | Espacio extra | Notas |
|---|---|---|---|
| DFS recursivo | O(R * C) | O(R * C) pila de llamadas en el peor caso | Visita cada celda de la region una vez; la peor region es toda la pantalla |
| BFS iterativo | O(R * C) | O(R * C) cola en el peor caso | Mismo limite de visitas; sin riesgo de pila de la JVM |
| Variante de ocho | O(R * C) | igual | Mas aristas por celda; sigue lineal en celdas |
Nunca necesitas mas de trabajo constante por celda. No digas O(1) de espacio para DFS recursivo en rellenos grandes; la pila es real.
6. Casos borde y errores frecuentes
Los entrevistadores tocan estos:
- El color del clic ya es el nuevo: return al momento. Si no, recursion / cola infinitas.
- Clic fuera de limites: return; no lances excepcion salvo que la API lo prometa.
- Pantalla 1x1: una sola asignacion si los colores difieren.
- Toda la pantalla un color: cada celda cambia; la profundidad del DFS puede ser enorme.
- La region toca los bordes: chequeos de limites en cada vecino, no solo en el inicio.
- Filas irregulares: el codigo de ensenanza asume rectangulo; dilo si las filas pueden diferir.
- Ocho vs cuatro direcciones: un sangrado diagonal incorrecto cambia la respuesta (ver celda
(2,2)del ejemplo). - Recolorear antes de guardar
oldColor: no sabes que igualar.
Errores comunes:
- Olvidar el guard
oldColor == newColor. - Comprobar
!= newColoren vez de== oldColoral expandir (recorreria toda celda no-nueva). - Faltar una direccion en la lista de cuatro.
- Usar ocho direcciones por accidente.
- Encolar sin recolorear (BFS revisita sin fin) o recolorear sin marca de visitado.
- Limites off-by-one (
<= lengthen vez de< length). - Asumir pantalla cuadrada cuando solo usas
screen[0].lengthcomo ancho (vale si es rectangular; di la asuncion).
7. Resumen para contarselo a un amigo
Paint fill en una frase larga:
- La pantalla es una rejilla de colores. Clic en una celda y un color nuevo.
- Guarda
oldColor. Si ya es igual anewColor, para. - Recolorea cada celda alcanzable con pasos arriba/abajo/izquierda/derecha que se queden en
oldColor. - DFS recursivo o cola BFS: misma imagen final.
- Recolorea (o marca visitado) al entrar en una celda para no procesarla dos veces.
- Tiempo y espacio lineales en el tamano de la region rellenada (peor caso: toda la rejilla).
Si puedes recorrer el ejemplo 3x4 a mano, escribir el guard de salida temprana y explicar por que cuatro direcciones dejan sola una celda diagonal, dominas el problema 8.10. Lo siguiente del capitulo es conteo al estilo monedas con DP.
Serie
- Guia: Guia de la serie CTCI
- Anterior: Parens
- Siguiente: Coins
