7

在许多棋盘游戏(如跳棋、围棋和黑白棋)中,每个方格可以由三种状态表示:白色黑色

这种游戏引擎中的 8x8 棋盘通常表示为两个位棋盘:一个 64 位整数用于定位白色棋子,另一个 64 位整数用于黑色棋子。

但是,在存储本地游戏模式时,这种二进制表示可能需要大量空间。例如,为 8 平方行的所有可能值创建一个查找表将需要一个具有 256*256 = 4^8 = 65536 个值的数组,而只有 3^8 = 6561 个可能的位置(因为一个正方形永远不可能被黑白棋子占据)。

另一种方法是将板存储为三进制数 - 所谓的titboards。但是我在任何地方都没有找到在两个二进制整数表示和三元整数表示之间进行转换的快速算法。

因此,我的问题是

是否有一种有效的方法可以将两个互斥的二进制数w & b == 0((最好在 C/C++ 中。)

Python 示例

是我的 Python 解决方案:

white_black_empty = lambda w, b: int(format(w, 'b'), base=3) + \
                                 int(format(b, 'b'), base=3)*2

示例值:

  • w = 1010001 2 = 81
  • b = 0100100 2 = 36
  • 结果 = 1010001 3 + 0100100 3 *2 = 1010001 3 + 0200200 3 = 1210201 3 = 1315

所以white_black_empty(81, 36) == 1315

但是,将整数转换为二进制的字符串表示形式format(x, 'b'),然后使用基数 3 从字符串转换回整数int(x, base=3)看起来相当低效。

4

4 回答 4

4

如果您的硬件具有快速popcount操作,那么您可以将n 个空格的板表示为 2 n位值 ⟨ mask , color ⟩,其中第二个值保证在 [0, 2 popcount( mask ) ]范围内如果方格被占用,则方格对应的位位置第一个值为1;如果第j个占据的方块有一个白色块,则在对应于j的位位置中的第二个值为 1 。为了访问color中的位,让这个函数返回颜色给定掩码中的位位置很有用以及掩码中对应于掩码中的 1 位的位位置(即对应于占用的正方形的位):

static inline int colourBitPos(unsigned mask, unsigned pos) {
  return popcount(mask & ((1U << pos) - 1));
}

(换句话说,它计算掩码中指定位置之后的一位数。)

然后,您可以通过保存基本索引的预先计算的查找表轻松地将 ⟨ mask , color ⟩ 对转换为 [0, 3 n -1]范围内的单个数字。当我最初考虑这个系统时,我认为是n +1 个连接表,每个表对应一个 popcount。这在概念上很好,因为带有 popcount i的代码的可能着色数量显然是 2 i而带有 popcount i的掩码数量是C ( n , i ) (使用C () 作为二项式系数函数,因为没有 MathJax这里)。可爱的身份:

二项式系数的总和乘以 2 的幂是 3 的幂

可能不如其他二项式恒等式知名。

虽然可以利用这种安排在 O( n ) 时间内相当费力地计算索引(在掩码字段中逐位计算),但最简单和最快的解决方案是使用 2 n元素的固定查找表,其大小远小于 3 n 个数据表。通过简单地为每个值累加适当的 2 次方,为每个mask值计算一个基值:

int base[1U<<N];
for (unsigned i = 0, offset = 0; i < 1U<<N; ++i) {
  base[i] = offset;
  offset += 1U<<popcount(i);
}

然后任何对的索引可以计算为:

index = base[mask] + colour;

[见下面的例子]

双分量表示并不是特别难处理,尽管它显然不如两位三路选择那么容易。例如,要查找正方形i中的内容:

(mask & (1U << i))
    ? (colour & ((1U << colouredBitPos(mask, i) - 1) ? WHITE : BLACK
    : EMPTY

再举一个例子,为了向当前未占用的正方形i添加一个彩色col (WHITE = 1, BLACK = 0) ,您可以:

unsigned pos = colouredBitPos(mask, i);
colour += (colour & ~((1U << pos) - 1)) + (col << pos);
mask |= 1U << i;

有效地将颜色的第一部分左移一位以插入新位。如果广场已经被占用,您将避免转移:

unsigned pos = colouredBitPos(mask, i);
colour &= ~(1U << pos);  // Make it black
colour |= col << pos;    // And give it the right colour

其他操作也同样简单。

在你的情况下这项工作是否合理将取决于我无法做出判断的许多因素。但是空间开销接近最佳。除了增加代码大小之外,唯一的开销是单个 2 n元素查找表,它可以与所有数据表一起使用,并且在任何情况下相对于任何数据表的大小来说都是很小的(例如,对于n = 8,数据表有6561个元素,所以一个256个元素的查找表会增加一个数据元素也是short的数据表4%的开销。如果你要持久化数据表,就不需要持久化查找表,因为它可以很容易地重新生成。)


索引编码示例:

为简单起见,使用n =4,基本查找表为:

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

使用 U=Unoccupied、B=Black、W=White(并且如上所述假设 White 为 1),一些示例编码和索引:

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
于 2018-12-10T23:22:18.110 回答
2

如何存储您要转换的内容?使用下面的方案,一行中每增加 8 位,将在一个数组(或哈希表)中花费 512 个数字。权衡将是更多的添加和位提取以减少存储 - 例如,存储 8 位,而不是完整的 8 位,这会导致 255 个数字,我们可以存储 2^4 和 2^4(对于第二组4 位),结果存储了 32 个(黑色加上 32 个)数字,但需要在转换过程中提取每组 4 位和另一个加法。

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

于 2018-12-12T12:02:17.227 回答
1

在实践中,您需要将棋盘状态存储在以 s 为基础的 4 进制中unsigned long,每个棋盘行填充到整数个unsigned longs。这将为您提供最佳的内存位置,非常快速地访问板单元,但使用的 RAM 比三元封装多 26.2%。

要将电路板状态存储在二进制文件中,您可以将 5 个三进制数字(五个电路板单元状态)打包到每个 8 位字节中。这仅比三元打包多使用 5.1% 的内存,并且实现起来简单而健壮。特别是,这种方式您无需担心字节顺序(字节序)。

纯三元打包的问题是每个基数为 3 的数字都会影响表示相同数值的大多数二进制数字。例如,3 8 = 30000000 3 = 6561 = 1100110100001 2。这意味着提取 base-3 数字的唯一实用方法是通过重复除法和模数(除以 3)。

描述一块尺寸为N × M的板子,三元打包和拆包函数本质上是O ( N 2 M 2 ),因此随着板子尺寸的增加越来越慢。通过使用更少 CPU 时间的压缩库(例如liblzma ) ,您可能会获得更好的节省。对于许多电路板配置,游程编码也可以很好地工作。

以下是最多 16777215×16777215 个单元的板的示例实现(仅测试最多 32768×32768 个单元):

#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;
}

board_init()函数初始化板,board_save()将板状态保存到流中,包括板大小,以可移植二进制格式(每个文件将在 big-endian 和 little-endian 架构上生成相同的板),board_load()并将加载以前保存的板从一条溪流。如果成功,它们都返回0,如果错误则非零。

和函数是静态内联访问器函数get_cell()set_cell()用于检查和设置板中各个单元的状态。

正如我最初建议的那样,这个在 RAM 中每个单元使用 2 位(每个字节 4 个单元),存储到文件时每个字节使用 5 个单元。

示例程序采用三个命令行参数:文件名、行数和列数。它将生成该大小的随机状态,将其保存到命名文件中,将其从命名文件读回到单独的板中,最后比较板状态,以验证实现的功能是否正常工作。

于 2018-12-10T17:36:18.823 回答
1

从字符串转换为整数并返回确实效率低下。

如果您只需要对值进行编码,那么根据它们所代表的实际数字来考虑它们将会很有用。例如,在考虑棋盘上的八行时,第一个位置的状态是有效的boardState % 3;我们可以使用黑色块在 a 上1,白色块在 a 上2,空值在 a上的约定0。对于第二个,它变成(boardState % 9)/3,第三个(boardState % 27) / 3等等。

因此,对于编码,我们可以扩展这种想法:我们取 0、1 或 2,将其乘以 3 的幂(无论我们正在考虑哪个棋盘位置),然后将其添加到某个“结果”数。一些(非常未经测试的)示例代码如下:

#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

不幸的是,由于计算机偏向于二进制,因此您只能获得但非常聪明的位移。因此,for这段代码中的循环(就性能而言,在 C 中的粗略程度不如在 python 中)。

请注意,此方法的范围有限;如您所知,您不能用这种方法表示整个电路板(因为 64 位整数没有 3^64 个可能的值)。

希望这比字符串方法更适合您!

于 2018-12-10T16:09:12.067 回答