C99 - tablero 3x3 en 0.084s
Editar: modifiqué mi código e hice un análisis más profundo de los resultados.
Ediciones adicionales: poda adicional por simetrías. Esto hace 4 configuraciones de algoritmo: con o sin simetrías X con o sin poda alfa-beta
Ediciones más lejanas: lejanas agregó la memorización usando una tabla hash, finalmente logrando lo imposible: ¡resolver un tablero 3x3!
Características principales:
- implementación directa de minimax con poda alfa-beta
- muy poca administración de memoria (mantiene dll de movimientos válidos; O (1) actualizaciones por rama en la búsqueda del árbol)
- segundo archivo con poda por simetrías. Todavía logra actualizaciones O (1) por rama (técnicamente O (S) donde S es el número de simetrías. Esto es 7 para tableros cuadrados y 3 para tableros no cuadrados)
- los archivos tercero y cuarto agregan memorización. Tienes control sobre el tamaño de la tabla hash (
#define HASHTABLE_BITWIDTH
). Cuando este tamaño es mayor o igual que el número de paredes, garantiza que no haya colisiones y actualizaciones de O (1). Las tablas hash más pequeñas tendrán más colisiones y serán un poco más lentas.
- compilar con
-DDEBUG
impresiones
Posibles mejoras:
arregla una pequeña pérdida de memoria arreglada en la primera edición
poda alfa / beta agregada en la 2da edición
podar simetrías agregadas en la tercera edición (tenga en cuenta que las simetrías no se manejan mediante la memorización, por lo que sigue siendo una optimización separada).
memoria agregada en la 4ta edición
- Actualmente la memorización utiliza un bit indicador para cada muro. Una tabla de 3x4 tiene 31 paredes, por lo que este método no podría manejar tablas de 4x4 independientemente de las limitaciones de tiempo. la mejora sería emular enteros de X bits, donde X es al menos tan grande como el número de muros.
Código
Debido a la falta de organización, el número de archivos se ha ido de las manos. Todo el código se ha movido a este repositorio de Github . En la edición de la memorización, agregué un archivo MAKE y un script de prueba.
Resultados
Notas sobre la complejidad
Los enfoques de fuerza bruta a los puntos y cuadros explotan en complejidad muy rápidamente .
Considere un tablero con R
filas y C
columnas. Hay R*C
cuadrados, R*(C+1)
paredes verticales y C*(R+1)
paredes horizontales. Eso es un total deW = 2*R*C + R + C
.
Debido a que Lembik nos pidió que resolvamos el juego con minimax, debemos atravesar las hojas del árbol del juego. Ignoremos la poda por ahora, porque lo que importa son órdenes de magnitud.
Hay W
opciones para el primer movimiento. Para cada uno de ellos, el siguiente jugador puede jugar cualquiera de los W-1
muros restantes, etc. Eso nos da un espacio de búsqueda de SS = W * (W-1) * (W-2) * ... * 1
, o SS = W!
. Los factoriales son enormes, pero eso es solo el comienzo. SS
es el número de nodos hoja en el espacio de búsqueda. Más relevante para nuestro análisis es el número total de decisiones que tuvieron que tomarse (es decir, el número de ramas B
en el árbol). La primera capa de ramas tiene W
opciones. Para cada uno de ellos, el siguiente nivel tiene W-1
, etc.
B = W + W*(W-1) + W*(W-1)*(W-2) + ... + W!
B = SUM W!/(W-k)!
k=0..W-1
Veamos algunos tamaños de mesa pequeños:
Board Size Walls Leaves (SS) Branches (B)
---------------------------------------------------
1x1 04 24 64
1x2 07 5040 13699
2x2 12 479001600 1302061344
2x3 17 355687428096000 966858672404689
Estas cifras se están volviendo ridículas. Al menos explican por qué el código de fuerza bruta parece colgar para siempre en una placa de 2x3. El espacio de búsqueda de una placa de 2x3 es 742560 veces mayor que 2x2 . Si 2x2 tarda 20 segundos en completarse, una extrapolación conservadora predice más de 100 días de tiempo de ejecución para 2x3. Claramente necesitamos podar.
Análisis de poda
Comencé agregando podas muy simples usando el algoritmo alfa-beta. Básicamente, deja de buscar si un oponente ideal nunca le daría sus oportunidades actuales. "Oye, mira, ¡gano mucho si mi oponente me permite obtener todas las casillas!", Pensó AI, nunca.
editar También he agregado poda basada en tablas simétricas. No uso un enfoque de memorización, por si algún día agrego memorización y quiero mantener ese análisis por separado. En cambio, funciona así: la mayoría de las líneas tienen un "par simétrico" en otro lugar de la cuadrícula. Hay hasta 7 simetrías (horizontal, vertical, rotación 180, rotación 90, rotación 270, diagonal y la otra diagonal). Los 7 se aplican a tableros cuadrados, pero los últimos 4 no se aplican a tableros no cuadrados. Cada pared tiene un puntero a su "par" para cada una de estas simetrías. Si, al entrar en un turno, el tablero es horizontalmente simétrico, entonces solo se necesita jugar uno de cada par horizontal .
editar editar ¡Memorización! Cada muro obtiene una identificación única, que convenientemente configuré para ser un bit indicador; la enésima pared tiene la identificación 1 << n
. El hash de un tablero, entonces, es solo el OR de todas las paredes jugadas. Esto se actualiza en cada rama en el tiempo O (1). El tamaño de la tabla hash se establece en a #define
. Todas las pruebas se ejecutaron con tamaño 2 ^ 12, porque ¿por qué no? Cuando hay más paredes que bits que indexan la tabla hash (12 bits en este caso), los 12 menos significativos se enmascaran y se usan como índice. Las colisiones se manejan con una lista vinculada en cada índice de tabla hash. El siguiente cuadro es mi análisis rápido y sucio de cómo el tamaño de la tabla hash afecta el rendimiento. En una computadora con RAM infinita, siempre establecemos el tamaño de la mesa en el número de paredes. Una tabla de 3x4 tendría una tabla hash de 2 ^ 31 de largo. Por desgracia no tenemos ese lujo.
Ok, volvamos a la poda. Al detener la búsqueda en lo alto del árbol, podemos ahorrar mucho tiempo al no bajar a las hojas. El 'Factor de poda' es la fracción de todas las ramas posibles que tuvimos que visitar. La fuerza bruta tiene un factor de poda de 1. Cuanto más pequeño es, mejor.