Dans de nombreux jeux de société (comme les dames, go et othello/reversi), chaque carré peut être représenté par trois états: blanc, noir ou vide.
Les cartes 8x8 dans ces moteurs de jeu sont généralement représentées par deux cartes de bits: un entier 64 bits pour l'emplacement des pièces blanches et un autre entier 64 bits - pour le noir.
Cependant, lors du stockage de modèles de jeu locaux, une telle représentation binaire peut nécessiter beaucoup d'espace. Par exemple, créer une table de correspondance pour toutes les valeurs possibles d’une ligne de 8 carrés nécessiterait un tableau de 256 * 256 = 4 ^ 8 = 65536 valeurs, contre seulement 3 ^ 8 = 6561 positions possibles (puisqu'un carré ne peut jamais être occupée par des pièces noires et blanches).
Une autre solution consiste à stocker les cartes sous forme de nombres ternaires, appelés titboards . Mais je n’ai trouvé nulle part un algorithme rapide permettant de convertir une représentation en deux entiers binaires en une représentation en nombres ternaires.
Par conséquent, ma question est
Existe-t-il un moyen efficace de convertir (coder) deux nombres binaires mutuellement exclusifs (w & b == 0
) en nombres ternaires, de sorte que chaque paire unique de tels entiers soit mappée sur un entier unique résultant? (De préférence en C/C++.)
Exemple Python
Ici est ma solution Python pour ce faire:
white_black_empty = lambda w, b: int(format(w, 'b'), base=3) + \
int(format(b, 'b'), base=3)*2
Exemple de valeurs:
Donc white_black_empty(81, 36) == 1315
.
Cependant, convertir l'entier en représentation sous forme de chaîne d'un format(x, 'b')
binaire, puis de chaîne en entier à l'aide de la fonction de base 3 int(x, base=3)
semble plutôt inefficace.
Que diriez-vous de stocker ce que vous essayez de convertir? Avec le schéma ci-dessous, chaque 8 bits supplémentaires d'une ligne coûterait 512 nombres dans un tableau (ou une table de hachage). Le compromis serait davantage d’ajouts et d’extraction de bits pour réduire le stockage - par exemple, pour stocker 8 bits, au lieu des 8 complets, qui donnent 255 numéros, nous pourrions stocker 2 ^ 4 et 2 ^ 4 (pour le second ensemble de 4 bits), ce qui entraîne la mémorisation de 32 (plus 32 pour les noirs), mais nécessite d'extraire chaque jeu de 4 bits et un autre ajout lors de la conversion.
const ones = new Array(256);
const twos = new Array(256);
for (let i=0; i<256; i++){
let one = 0;
let two = 0;
for (let j=0; j<8; j++){
if ((1 << j) & i){
one += Math.pow(3, j);
two += 2*Math.pow(3, j);
}
ones[i] = one;
twos[i] = two;
}
}
function convert(w, b){
return ones[w] + twos[b];
}
console.log(convert(81, 36));
Si votre matériel exécute une opération rapide popcount , vous pouvez alors représenter une carte de n espaces sous la forme de 2 n valeurs-bits ⟨masque, couleur, où la deuxième valeur est garantie dans la plage [0, 2popcount (masque)] La première valeur est 1 dans la position du bit correspondant à un carré si le carré est occupé; la deuxième valeur est 1 dans la position du bit correspondant à j si le j ème carré occupé a une pièce blanche. Pour accéder aux bits dans color, il est utile de disposer de cette fonction qui renvoie une position de bit dans color étant donné masque et une position de bit dans le masque qui correspond à un bit dans le masque (c'est-à-dire un bit correspondant à un carré occupé):
static inline int colourBitPos(unsigned mask, unsigned pos) {
return popcount(mask & ((1U << pos) - 1));
}
(En d'autres termes, il compte le nombre d'un bit dans masque suivant la position spécifiée.)
Vous pouvez alors facilement transformer la paire ⟨masque, couleur en un seul nombre compris dans la plage [0, 3n-1] au moyen d'une table de recherche précalculée contenant des indices de base. Quand je pensais à l'origine à ce système, je pensais en termes de n +1 tables concaténées, chacune correspondant à un seul nombre de pop-count. C'est gentil du point de vue conceptuel, puisque le nombre de colorations possibles d'un code avec popcount i est évidemment 2i tandis que le nombre de masques avec popcount i est C (n, i) (en utilisant C () comme fonction du coefficient binomial puisque il n'y a pas MathJax ici). La belle identité:
est probablement moins connu que les autres identités binomiales.
Bien qu'il soit possible de tirer parti de cet arrangement pour calculer laborieusement l'index en temps O (n) time (bit par bit dans le champ mask), la solution la plus simple et la plus rapide consiste à utiliser un 2n-element table de correspondance fixe base, dont la taille est bien inférieure à 3n tableaux de données. Une valeur de base est calculée pour chaque valeur de masque en accumulant simplement la puissance appropriée de deux pour chaque valeur:
int base[1U<<N];
for (unsigned i = 0, offset = 0; i < 1U<<N; ++i) {
base[i] = offset;
offset += 1U<<popcount(i);
}
Ensuite, l'index de n'importe quelle paire peut être calculé comme suit:
index = base[mask] + colour;
[Voir exemple ci-dessous]
Il n’est pas particulièrement difficile de travailler avec la représentation à deux composants, bien que ce ne soit évidemment pas aussi facile qu’un choix à deux bits à deux bits. Par exemple, pour trouver ce qui se trouve dans le carré i:
(mask & (1U << i))
? (colour & ((1U << colouredBitPos(mask, i) - 1) ? WHITE : BLACK
: EMPTY
Pour un autre exemple, afin d’ajouter une pièce colorée col (WHITE = 1, BLACK = 0) au carré inoccupé i, vous feriez:
unsigned pos = colouredBitPos(mask, i);
colour += (colour & ~((1U << pos) - 1)) + (col << pos);
mask |= 1U << i;
décaler efficacement la première partie de color d'un bit pour insérer le nouveau bit. Si la place était déjà occupée, vous éviteriez le décalage:
unsigned pos = colouredBitPos(mask, i);
colour &= ~(1U << pos); // Make it black
colour |= col << pos; // And give it the right colour
Les autres opérations sont également simples.
Que ce travail soit justifié dans votre cas dépendra de nombreux facteurs que je ne peux pas rendre de jugement. Mais les frais généraux sont proches de l’optimum. Outre l’augmentation de la taille du code, la seule surcharge est un simplen-element lookup table qui peut être utilisé avec toutes les tables de données et qui est en tout cas très petit par rapport à la taille de toute table de données (par exemple, pour n = 8, les tables de données comportent 6561 éléments une table de consultation de 256 éléments ajouterait 4% de surcharge à une table de données unique dont les éléments de données sont également courts. Et il n'est pas nécessaire de conserver la table de recherche si vous persistez dans la table de données, car elle peut facilement être régénérée.)
Exemple de codage d'index:
En utilisant n = 4 pour plus de simplicité, la table de recherche base est:
mask base mask base mask base mask base
0000 0 0100 9 1000 27 1100 45
0001 1 0101 11 1001 29 1101 49
0010 3 0110 15 1010 33 1110 57
0011 5 0111 19 1011 37 1111 65
En utilisant U = Inoccupé, B = Noir, W = Blanc (et en supposant, comme ci-dessus, que Blanc vaut 1), quelques exemples de codages et d'index:
board mask colour compute index decimal
UUBW 0011 01 base[0011]+ 01 = 6
UUWB 0011 10 base[0010]+ 10 = 7
WUBW 1011 101 base[1011]+101 = 42
La conversion de chaîne en entier et retour sera en effet inefficace.
Si vous avez juste besoin d’encoder les valeurs, il sera utile de les considérer en termes de nombres réels qu’elles représentent. Par exemple, en considérant huit lignes sur un tableau, l'état de la première position est effectivement boardState % 3
; nous pouvons utiliser la convention selon laquelle un morceau noir est là sur un 1
, un morceau blanc sur un 2
et une valeur vide sur un 0
. Pour le second, il devient (boardState % 9)/3
, le troisième (boardState % 27) / 3
et ainsi de suite.
Donc, pour l’encodage, nous pouvons étendre cette pensée: nous prenons soit 0, 1 ou 2, nous le multiplions par 3 pour obtenir la puissance de (quelle que soit la position du conseil que nous envisageons), et nous l’ajoutons à un nombre "résultat". Voici un exemple de code (TRES non testé):
#include <inttypes.h>
#include <math.h>
uint64_t tritboard(uint64_t white, uint64_t black){
uint64_t onemask = 0x0000000000000001;//you could also just say "= 1"
uint64_t retval = 0;
uint64_t thisPos;
for(char i = 0; i < 8; i++){
thisPos = 0;
if(white & (oneMask << i)) thisPos += 2;
if(black & (oneMask << i)) thisPos += 1;
retval += thisPos * ( (uint64_t) pow(3, i));
}//for
return retval;
}//tritboard
Malheureusement, les ordinateurs étant partiels au binaire, vous ne pourrez qu'être très intelligents avec Bithifts. Ainsi, la boucle for
de ce code (qui est légèrement moins grossière en C qu’en python, en termes de performances).
Notez que la portée de cette approche est limitée. comme vous pouvez le comprendre, vous ne pouvez pas représenter l’ensemble du tableau avec cette approche (car il n’existe pas 3 ^ 64 valeurs possibles pour un entier de 64 bits).
J'espère que cela vous convient mieux que l'approche par les cordes!
En pratique, vous souhaiterez stocker l’état de la carte au format base-4 dans unsigned long
s, chaque rangée de la carte étant complétée avec un nombre entier de unsigned long
s. Cela vous donnera la meilleure mémoire, un accès très rapide aux cellules de la carte, mais utilise 26,2% de plus RAM que la compression ternaire.
Pour stocker l'état de la carte dans un fichier binaire, vous pouvez insérer 5 chiffres ternaires (cinq états de cellules de la carte) dans chaque octet de 8 bits. Cela utilise seulement 5,1% de mémoire en plus que la compression ternaire, et est simple et robuste à mettre en œuvre. En particulier, de cette façon, vous n'avez pas à vous soucier de l'ordre des octets (endianness).
Le problème avec la compression ternaire pure est que chaque chiffre en base 3 affecte la plupart des chiffres binaires représentant la même valeur numérique. Par exemple, 38 = 300000003 = 6561 = 11001101000012. Cela signifie que le seul moyen pratique d’extraire des chiffres en base 3 est de répéter la division et le module (par 3).
Décrire un tableau de taille N×M, la fonction ternaire d’emballage et de déballage sera essentiellement O(N2M2), et donc de plus en plus lent lorsque la taille de la planche augmente. Vous obtiendrez probablement de meilleures économies en utilisant une bibliothèque de compression (par exemple, liblzma ) utilisant moins de temps processeur. Pour de nombreuses configurations de cartes, le codage en longueur d’exécution pourrait également bien fonctionner.
Voici un exemple de mise en œuvre pour les cartes contenant jusqu'à 16777215 × 16777215 cellules (testées uniquement jusqu'à 32768 × 32768 cellules):
#include <stdlib.h>
#include <inttypes.h>
#include <limits.h>
#include <stdio.h>
#include <time.h>
#define ULONG_BITS (CHAR_BIT * sizeof (unsigned long))
#define ULONG_CELLS (CHAR_BIT * sizeof (unsigned long) / 2)
struct board {
int rows;
int cols;
size_t stride;
unsigned long *data;
};
enum {
EMPTY = 0, /* calloc() clears the data to zeroes */
WHITE = 1,
BLACK = 2,
ERROR = 3
};
int board_init(struct board *const b, const int rows, const int cols)
{
const size_t stride = (cols + ULONG_CELLS - 1) / ULONG_CELLS;
const size_t ulongs = stride * (size_t)rows;
if (b) {
b->rows = 0;
b->cols = 0;
b->stride = 0;
b->data = NULL;
}
if (!b || rows < 1 || cols < 1)
return -1;
if ((size_t)(ulongs / stride) != (size_t)rows)
return -1;
b->data = calloc(ulongs, sizeof b->data[0]);
if (!b->data)
return -1;
b->rows = rows;
b->cols = cols;
b->stride = stride;
return 0;
}
static inline int get_cell(const struct board *const b, const int row, const int col)
{
if (!b || row < 0 || col < 0 || row >= b->rows || col >= b->cols)
return EMPTY;
else {
const size_t i = (size_t)col / ULONG_CELLS;
const size_t c = ((size_t)col % ULONG_CELLS) * 2;
const unsigned long w = b->data[b->stride * row + i];
return (w >> c) & 3;
}
}
static inline int set_cell(struct board *const b, const int row, const int col, const int value)
{
if (!b || row < 0 || col < 0 || row >= b->rows || col >= b->cols)
return EMPTY;
else {
const size_t i = (size_t)col / ULONG_CELLS;
const size_t c = ((size_t)col % ULONG_CELLS) * 2;
unsigned long *w = b->data + b->stride * row + i;
*w = ((*w) & (3uL << c)) | ((unsigned long)(value & 3) << c);
return value & 3;
}
}
static inline int write_u24(FILE *const out, const int value)
{
unsigned int u = value;
if (!out || value < 0 || value > 16777215 || ferror(out))
return -1;
if (fputc(u & 255, out) == EOF)
return -1;
else
u >>= 8;
if (fputc(u & 255, out) == EOF)
return -1;
else
u >>= 8;
if (fputc(u & 255, out) == EOF)
return -1;
else
return 0;
}
static inline int read_u24(FILE *const in, unsigned int *const to)
{
unsigned int result;
int c;
if (!in || ferror(in))
return -1;
c = fgetc(in);
if (c == EOF)
return -1;
else
result = c & 255;
c = fgetc(in);
if (c == EOF)
return -1;
else
result |= (c & 255) << 8;
c = fgetc(in);
if (c == EOF)
return -1;
else
result |= (c & 255) << 16;
if (to)
*to = result;
return 0;
}
int board_save(const struct board *const b, FILE *const out)
{
int row, col, cache, coeff;
if (!b || !out || ferror(out) || !b->stride ||
b->rows < 1 || b->rows > 16777215 ||
b->cols < 1 || b->cols > 16777215)
return -1;
if (write_u24(out, b->rows))
return -1;
if (write_u24(out, b->cols))
return -1;
/* Clear byte cache. */
cache = 0;
coeff = 1;
for (row = 0; row < b->rows; row++) {
for (col = 0; col < b->cols; col++) {
switch (get_cell(b, row, col)) {
case EMPTY: /* Saved as 0 */
break;
case WHITE: /* Saved as 1 */
cache += coeff;
break;
case BLACK: /* Saved as 2 */
cache += coeff + coeff;
break;
default: /* Invalid cell state. */
return -1;
}
if (coeff >= 81) {
if (fputc(cache, out) == EOF)
return -1;
cache = 0;
coeff = 1;
} else
coeff *= 3;
}
}
if (coeff > 1)
if (fputc(cache, out) == EOF)
return -1;
if (fflush(out))
return -1;
return 0;
}
int board_load(struct board *const b, FILE *in)
{
unsigned int rows, cols, row, col, cache, count;
int c;
if (b) {
b->rows = 0;
b->cols = 0;
b->stride = 0;
b->data = NULL;
}
if (!b || !in || ferror(in))
return -1;
if (read_u24(in, &rows) || rows < 1 || rows > 16777215)
return -1;
if (read_u24(in, &cols) || cols < 1 || cols > 16777215)
return -1;
if (board_init(b, rows, cols))
return -1;
/* Nothing cached at this point. */
cache = 0;
count = 0;
for (row = 0; row < rows; row++) {
for (col = 0; col < cols; col++) {
if (count < 1) {
c = fgetc(in);
if (c == EOF || c < 0 || c >= 243)
return -1;
cache = c;
count = 5;
}
switch (cache % 3) {
case 0: /* Leave as background. */
break;
case 1: /* White */
if (set_cell(b, row, col, WHITE) != WHITE)
return -1;
break;
case 2: /* Black */
if (set_cell(b, row, col, BLACK) != BLACK)
return -1;
break;
}
cache /= 3;
count--;
}
}
/* No errors. */
return 0;
}
/* Xorshift 64* pseudo-random number generator. */
static uint64_t prng_state = 1;
static inline uint64_t prng_randomize(void)
{
int rounds = 1024;
uint64_t state;
state = (uint64_t)time(NULL);
while (rounds-->0) {
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
}
if (!state)
state = 1;
prng_state = state;
return state;
}
static inline uint64_t prng_u64(void)
{
uint64_t state = prng_state;
state ^= state >> 12;
state ^= state << 25;
state ^= state >> 27;
prng_state = state;
return state * UINT64_C(2685821657736338717);
}
/* Uniform random ternary generator. */
static uint64_t ternary_cache = 0;
static int ternary_bits = 0;
static inline int prng_ternary(void)
{
int retval;
do {
if (ternary_bits < 2) {
ternary_cache = prng_u64();
ternary_bits = 64;
}
retval = ternary_cache & 3;
ternary_cache >>= 1;
ternary_bits -= 2;
} while (retval > 2);
return retval;
}
int main(int argc, char *argv[])
{
struct board original, reloaded;
uint64_t correct, incorrect, count[3];
double percent;
FILE *file;
int rows, cols, row, col;
char dummy;
if (argc != 4) {
fprintf(stderr, "\n");
fprintf(stderr, "Usage: %s [ -h | --help ]\n", argv[0]);
fprintf(stderr, " %s FILENAME ROWS COLUMNS\n", argv[0]);
fprintf(stderr, "\n");
fprintf(stderr, "This program generates a random ternary board,\n");
fprintf(stderr, "saves it to file FILENAME, reads it back, and\n");
fprintf(stderr, "verifies that the board state is intact.\n");
fprintf(stderr, "\n");
return EXIT_SUCCESS;
}
if (!argv[1][0]) {
fprintf(stderr, "No filename specified.\n");
return EXIT_FAILURE;
}
if (sscanf(argv[2], "%d %c", &rows, &dummy) != 1 || rows < 1 || rows > 16777215) {
fprintf(stderr, "%s: Invalid number of rows.\n", argv[2]);
return EXIT_FAILURE;
}
if (sscanf(argv[3], "%d %c", &cols, &dummy) != 1 || cols < 1 || cols > 16777215) {
fprintf(stderr, "%s: Invalid number of columns.\n", argv[2]);
return EXIT_FAILURE;
}
if (board_init(&original, rows, cols)) {
fprintf(stderr, "Cannot create a board with %d rows and %d columns.\n", rows, cols);
return EXIT_FAILURE;
}
fprintf(stderr, "Filling board with a random state; random seed is %" PRIu64 ".\n", prng_randomize());
percent = 100.0 / (double)rows / (double)cols;
count[0] = count[1] = count[2] = 0;
for (row = 0; row < rows; row++)
for (col = 0; col < cols; col++) {
int t = prng_ternary();
if (t < 0 || t > 3) {
fprintf(stderr, "prng_ternary() returned %d!\n", t);
return EXIT_FAILURE;
}
count[t]++;
set_cell(&original, row, col, t);
}
fprintf(stderr, " Empty: %" PRIu64 " cells, %.3f%%.\n", count[EMPTY], (double)count[EMPTY] * percent);
fprintf(stderr, " White: %" PRIu64 " cells, %.3f%%.\n", count[WHITE], (double)count[WHITE] * percent);
fprintf(stderr, " Black: %" PRIu64 " cells, %.3f%%.\n", count[BLACK], (double)count[BLACK] * percent);
file = fopen(argv[1], "wb");
if (!file) {
fprintf(stderr, "%s: Cannot open file for writing.\n", argv[1]);
return EXIT_FAILURE;
}
fprintf(stderr, "Saving to %s.\n", argv[1]);
if (board_save(&original, file)) {
fclose(file);
fprintf(stderr, "Write error.\n");
return EXIT_FAILURE;
}
if (fclose(file)) {
fprintf(stderr, "Write error.\n");
return EXIT_FAILURE;
}
fprintf(stderr, "Reloading game board.\n");
file = fopen(argv[1], "rb");
if (!file) {
fprintf(stderr, "%s: Cannot open file for reading.\n", argv[1]);
return EXIT_FAILURE;
}
if (board_load(&reloaded, file)) {
fclose(file);
fprintf(stderr, "Read error.\n");
return EXIT_FAILURE;
}
if (fclose(file)) {
fprintf(stderr, "Read error.\n");
return EXIT_FAILURE;
}
if (original.rows != reloaded.rows) {
fprintf(stderr, "Row count mismatches.\n");
return EXIT_FAILURE;
} else
if (original.cols != reloaded.cols) {
fprintf(stderr, "Column count mismatches.\n");
return EXIT_FAILURE;
}
fprintf(stderr, "Comparing board states.\n");
correct = 0;
incorrect = 0;
for (row = 0; row < rows; row++)
for (col = 0; col < cols; col++)
if (get_cell(&original, row, col) == get_cell(&reloaded, row, col))
correct++;
else
incorrect++;
if (incorrect) {
fprintf(stderr, "Found %" PRIu64 " mismatching cells (%.3f%%).\n", incorrect, (double)incorrect * percent);
return EXIT_FAILURE;
}
if (correct != (uint64_t)((uint64_t)rows * (uint64_t)cols)) {
fprintf(stderr, "Internal bug in the board comparison double loop.\n");
return EXIT_FAILURE;
}
fprintf(stderr, "Verification successful; functions work as expected for a board with %d rows and %d columns.\n", rows, cols);
return EXIT_SUCCESS;
}
La fonction board_init()
initialise une carte, board_save()
enregistre un état de carte dans un flux, y compris la taille de la carte, au format binaire portable (chaque fichier génère la même carte sur les architectures big-endian et little-endian), et board_load()
charge un conseil précédemment enregistré à partir d'un flux. Ils renvoient tous 0
en cas de succès, différent de zéro en cas d'erreur.
Les fonctions get_cell()
et set_cell()
sont des fonctions d'accesseur en ligne statiques permettant d'examiner et de définir l'état de cellules individuelles dans un tableau.
Comme je l'avais initialement suggéré, celui-ci utilise 2 bits par cellule dans RAM (4 cellules par octet) et 5 cellules par octet lorsqu'il est stocké dans un fichier.
Le programme exemple utilise trois paramètres de ligne de commande: un nom de fichier, le nombre de lignes et le nombre de colonnes. Il va générer un état aléatoire de cette taille, l'enregistrer dans le fichier nommé, le relire à partir du fichier nommé sur un tableau séparé et enfin comparer les états du tableau pour vérifier si les fonctions implémentées semblent fonctionner correctement.