位棋盘到三进制位棋盘的转换

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 = 10100012 = 81
  • b = 01001002 = 36
  • result = 10100013 + 01001003*2 = 10100013 + 02002003 = 12102013 = 1315

因此, white_black_empty(81, 36) == 1315

但是,将整数转换为二进制的字符串表示 format(x, 'b'),然后再使用基础为3的整数将其转换回整数 int(x, base=3),看起来效率不高。


7
我认为计算机术语中应该是“trit”而不是“tit”,“tit”可能指小型林地鸟类或其他物品。(参考链接 https://en.wikipedia.org/wiki/Units_of_information#Primary_units) - Simon F
3
@SimonF,就个人而言,我认为更自然的术语应该是“电路板”(tritboard) 。 - Andriy Makukha
2
请访问 https://en.wikipedia.org/wiki/Ternary_numeral_system。它是一个“三进制”数字,而不是“第三位数”。三进制数字的一位被称为“trit”。 - Jim Mischel
如果有帮助的话,5个三元值(3^5种组合)可以相对高效地打包成一个字节,因此您可以在13个字节中存储64个值,并且仍然具有相对简单的随机访问。 - Simon F
1
@cmaster,在这个应用程序中,二进制整数中对应的位是互斥的。也就是说,它们中至少有一个始终为零。这就是产生开销的原因。目标是在内存中拥有一个微小的查找表。使用包含6561(3^8)个键的线性数组比使用包含65536(4^8)个键的数组更节省空间。因此,我会取一个二进制行(8个nibble)并将其转换为三进制行(8个trit)。- 至少这是想法。 - Andriy Makukha
显示剩余9条评论
4个回答

4
如果您的硬件具有快速的 popcount 操作,则可以将 n 个空间的棋盘表示为 2 个 n-位值 ⟨maskcolour⟩,其中第二个值保证在范围 [0,2popcount(mask)] 内。 如果该方块被占用,则第一个值在相应的位位置上为 1;如果第 j 个占用的方块有一个白色棋子,则第二个值在与 j 对应的位位置上为 1。 为了访问 colour 中的位,有用的是具有以下功能的函数,该函数返回给定 mask 中对应于掩码中的 1 位(即与被占用的方块对应的位)的位位置的值。
static inline int colourBitPos(unsigned mask, unsigned pos) {
  return popcount(mask & ((1U << pos) - 1));
}

(换句话说,它计算了在指定位置后面的掩码mask中的一位数。)
您可以通过预先计算的查找表将⟨maskcolor⟩对轻松转换为范围[0,3 n -1]内的单个数字。该表保存基本索引。当我最初考虑这个系统时,我想到了n+1个串联的表,每个表对应于单个popcount。这在概念上很好,因为具有popcount i的代码的可能着色数量显然是2i,而具有popcount i的掩码数量为C(n,i)(使用C()作为二项式系数函数,因为此处没有MathJax)。美妙的身份证明:

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

这个二项式恒等式可能不如其他二项式恒等式那么出名。

虽然可以利用这种排列方式来费力地计算O(n)时间内的索引(通过mask字段逐位计算),但最简单和最快的解决方案是使用一个2n元素的固定查找表base,其大小比3n数据表小得多。为每个mask值计算一个基值,只需累加每个值的适当的2的幂次即可:

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(白色 = 1,黑色 = 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

其他操作同样很简单。

在你的情况下是否值得这样做,这取决于许多因素,我无法作出判断。但空间开销接近最优。除了增加代码大小之外,唯一的开销是一个单独的2n元素查找表,它可以用于所有数据表,并且相对于任何数据表的大小(例如,对于n=8,数据表有6561个元素,因此256个元素的查找表将增加单个数据表的4%开销,其数据元素也是shorts。如果您持久化数据表,则不需要持久化查找表,因为它可以轻松地重新生成。)


索引编码示例:

为了简单起见,使用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表示未占用,B表示黑色,W表示白色(并且假设白色为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

我不确定我完全理解。你能否举个例子,说明如何找到4位三进制板的索引,例如0012<sub>3</sub>、0021<sub>3</sub>和1020<sub>3</sub>?但这个想法非常好。 - Andriy Makukha
@OhneKleidung:可能是因为我在编写索引计算时搞砸了。重新修正并添加了一个示例。它的开销仍然很小。 - rici
是的,我理解了这个想法,但是发现公式是错误的。不管怎样,干得好! - Andriy Makukha
你的方案相比我提出的方案有什么优点呢?(顺便说一下,你回答中的一个投票是我的。) - גלעד ברקן

2
如何储存您要转换的内容呢?使用以下方案,每一行额外的8位将会在数组(或哈希表)中增加512个数字。这种折衷方式是为了减少存储空间,需要进行更多的加法和位提取 - 例如,为了储存8位而不是全部8位,需要储存2^4和2^4(用于第二组4位),这样就可以储存32个数字(加上32个黑色数字),但在转换期间需要提取每组4位并进行另一个加法操作。请注意保留HTML标签。

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


谢谢!我最终使用了类似的解决方案。然而,这仅适用于小的tritboards,比如patterns(在我的情况下)。对于一般情况(将其转换为完整的64-trit或更大的板),仍需要使用基线算法。 - Andriy Makukha
@OhneKleidung 对于64位,使用我的建议需要16个256个数字的数组(存储4096个数字)。然后,我们使用位掩码和移位提取每组8位,并对白色执行7次加法,对黑色执行7次加法。总共需要32位操作,16次查找和15次加法才能完成转换。什么是“基准算法”? - גלעד ברקן

1
将字符串转换为整数并再次转换确实会低效。
如果您只需要编码值,则考虑它们表示的实际数字将非常有用。例如,在考虑棋盘上的八行时,第一个位置的状态实际上是boardState%3;我们可以使用黑色棋子在1,白色棋子在2,空值在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种可能的值)。

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


1
gcc对128位整数有一定的支持,所以这不是一个大问题。(而且我实际上并不打算在三进制中存储整个棋盘。) 另外,我想知道你在循环内部进行幂运算和转换为整数 (uint64_t) pow(3, i) 是否比声明一个单独的整数变量 p = 1,然后在循环结束时将其乘以3更好,像这样:retval += thisPos * p; p *= 3。无论如何,我可以将其作为基准进行测试。谢谢。 - Andriy Makukha

1
实际上,您需要将棋盘状态存储在以每个棋盘行填充到整数个无符号长整型的基4打包中。这将为您提供最佳的内存局部性,非常快速地访问棋盘单元格,但比三进制打包使用26.2%更多的RAM。
要将棋盘状态存储在二进制文件中,您可以将5个三进制数字(五个棋盘单元格状态)打包到每个8位字节中。这仅比三进制打包使用5.1%的内存更多,并且实现简单且健壮。特别是,这种方式不需要担心字节顺序(字节序)。
纯三进制打包的问题在于,每个基3数字都会影响表示相同数值的大多数二进制数字。例如,3的8次方= 30000000 3 = 6561 = 11001101000012。这意味着提取基3数字的唯一实用方法是通过重复除法和模数(除以3)。
为了描述一个大小为N×M的棋盘,三进制打包和解包函数本质上将是O(N2M2),因此随着棋盘大小的增加而变得越来越慢。使用压缩库(例如liblzma)可以更好地节省CPU时间。对于许多棋盘配置,游程编码也可能效果良好。
以下是最多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()函数将棋盘状态以便携式二进制格式(每个文件在大端和小端架构上都生成相同的棋盘)保存到流中,包括棋盘大小,board_load()函数将从流中加载先前保存的棋盘。如果成功,它们都返回0,如果出错则返回非零值。

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

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

示例程序需要三个命令行参数:文件名、行数和列数。它将生成指定大小的随机状态,将其保存到指定文件中,从该文件中读取到一个单独的棋盘中,最后比较棋盘状态,以验证实现的函数是否正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接