web-dev-qa-db-fra.com

Conversion de bitboard en titboard (bitboard ternaire)

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:

  • w = 10100012 = 81
  • b = 01001002 = 36
  • résultat = 10100013 + 01001003* 2 = 10100013 + 02002003 = 12102013 = 1315

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.

7
Andriy Makukha

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));

2
גלעד ברקן

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é:

 Sum of binomial coefficients multiplied by powers of two is a power of three

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
4
rici

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!

1
Taylor Nelms

En pratique, vous souhaiterez stocker l’état de la carte au format base-4 dans unsigned longs, chaque rangée de la carte étant complétée avec un nombre entier de unsigned longs. 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. 

1
Nominal Animal