如何有效编码/解码压缩位置描述?

时间:2016-09-21 17:36:15

标签: c encoding compression chess

我正在为日本象棋变体写一个桌面。为了索引表基,我将每个国际象棋位置编码为整数。在其中一个编码步骤中,我编码棋盘上的棋子。由于实际方法有点复杂,让我以简化的方式解释这个问题。

编码

在最后的桌面游戏中,我有(比方说)六个不同的棋子,我想在9个方格的棋盘上分发。我可以天真地用六元组代表他们的位置( a b c d e f )其中每个变量 a f 是一个0到8范围内的数字,包括相应的棋子位于。

然而,这种表示并不是最佳的:没有两个国际象棋棋子可以占据同一个方格,但前面提到的编码很乐意允许这样做。我们可以通过六元组[ a,b',c',d',e',f']对相同的位置进行编码,其中 a 是相同的 a 和以前一样, b'是0到7之间的数字,表示第二件所在的方格数。这是通过为第一块未打开的每个方格分配0到7的数字来实现的。例如,如果第一件位于正方形3上,则第二件的正方形编号为:

1st piece: 0 1 2 3 4 5 6 7 8
2nd piece: 0 1 2 - 3 4 5 6 7

其他部分的编码方式相似, c'编号为0到6, d'编号为0到5之间的数字等。例如,幼稚编码(5,2,3,0,7,4)产生紧凑编码(5,2,2,0,3,1):

1st: 0 1 2 3 4 5 6 7 8 --> 5
2nd: 0 1 2 3 4 - 5 6 7 --> 2
3rd: 0 1 - 2 3 - 4 5 6 --> 2
4th: 0 1 - - 2 - 3 4 5 --> 0
5th: - 0 - - 1 - 2 3 4 --> 3
6th: - 0 - - 1 - 2 - 3 --> 1

在我的实际编码中,我想要编码的片段数量不固定。然而,电路板上的方格数是。

问题

如何有效地将朴素表示转换为紧凑表示,反之亦然?我使用标准C99作为程序。在这个问题的上下文中,我对使用非标准结构,内联汇编或内在函数的答案不感兴趣。

问题澄清

因为这个问题似乎有些混乱:

  • 问题是要找到一种实用的方法来实现naïve compact 位置表示之间的转换
  • 两个表示都是 n - 某些范围内的整数元组。问题不在于如何将这些表示编码成其他任何东西。
  • 在我所拥有的一个案例中,正方形的数量是25,而且数量是多达12个。然而,我对一个适用于合理参数空间的实现很感兴趣(例如,最多64个方格,最多32件)。
  • 我对替代陈述或编码不感兴趣,尤其是不是最佳的陈述或编码。
  • 我对 compact 表示不值得付出的评论感兴趣。
  • 我对使用内在函数,内联汇编或任何其他非标准结构(除了POSIX描述的那些)之外的答案也不感兴趣。

6 个答案:

答案 0 :(得分:3)

问题的天真解决方案:创建一个数组,其中值最初等于索引。使用正方形时,从数组中获取其值,并将所有值减少到右侧。此解决方案的运行时间为O(n*p),其中n是电路板上的方块数,p是电路板上的块数。

int codes[25];

void initCodes( void )
{
    for ( int i = 0; i < 25; i++ )
        codes[i] = i;
}

int getCodeForLocation( int location )
{
    for ( int i = location + 1; i < 25; i++ )
        codes[i]--;
    return codes[location];
}

您可以尝试使用分箱来提高此代码的性能。将板上的位置视为每个5个位置的5个箱。每个bin都有一个偏移量,bin中的每个位置都有一个值。如果从位置y的bin x获取值,则会减少y以下所有bin的偏移量。并且bin xy右侧的所有值都会递减。

int codes[5][5];
int offset[5];

void initCodes( void )
{
    int code = 0;
    for ( int row = 0; row < 5; row++ )
    {
        for ( int col = 0; col < 5; col++ )
            codes[row][col] = code++;
        offset[row] = 0;
    }
}

int getCodeForLocation( int location )
{
    int startRow = location / 5;
    int startCol = location % 5;
    for ( int col = startCol+1; col < 5; col++ )
        codes[startRow][col]--;
    for ( int row = startRow+1; row < 5; row++ )
        offset[row]--;
    return codes[startRow][startCol] + offset[startRow];
}

此解决方案的运行时间为O(sqrt(n) * p)。但是,在有25个方块的电路板上,你不会看到太多改进。要了解为什么要考虑天真解决方案与分箱解决方案所做的实际操作。最糟糕的情况是,天真的解决方案更新了24个位置。最坏的情况是,分箱解决方案更新offset阵列中的4个条目以及codes阵列中的4个位置。所以这似乎是3:1的加速。但是,分箱代码包含令人讨厌的分区/模数指令,并且总体上更复杂。如果幸运的话,你可能会获得2:1的加速。

如果电路板尺寸很大,例如256x256,然后binning将是伟大的。天真解决方案的最坏情况是65535个条目,而分箱将更新最多255 + 255 = 510个数组条目。所以这肯定会弥补令人讨厌的划分和增加的代码复杂性。

其中就是试图优化小问题集是徒劳的。当您拥有O(n)时,不会将O(sqrt(n))更改为O(log(n))n=25 sqrt(n)=5 log(n)=5。你得到了一个理论上的加速,但是当你考虑到大O这么无法忽视的无数常数因素时,这几乎总是一种虚假的节省。

为了完整性,这里是可以与上面的代码段

一起使用的驱动程序代码
int main( void )
{
    int locations[6] = { 5,2,3,0,7,4 };
    initCodes();
    for ( int i = 0; i < 6; i++ )
        printf( "%d ", getCodeForLocation(locations[i]) );
    printf( "\n" );
}

输出:5 2 2 0 3 1

答案 1 :(得分:3)

我找到了一个更优雅的解决方案,使用64位整数,最多16个位置,单个循环用于编码和解码:

#include <stdio.h>
#include <stdlib.h>

void encode16(int dest[], int src[], int n) {
    unsigned long long state = 0xfedcba9876543210;
    for (int i = 0; i < n; i++) {
        int p4 = src[i] * 4;
        dest[i] = (state >> p4) & 15;
        state -= 0x1111111111111110 << p4;
    }
}

void decode16(int dest[], int src[], int n) {
    unsigned long long state = 0xfedcba9876543210;
    for (int i = 0; i < n; i++) {
        int p4 = src[i] * 4;
        dest[i] = (state >> p4) & 15;
        unsigned long long mask = ((unsigned long long)1 << p4) - 1;
        state = (state & mask) | ((state >> 4) & ~mask);
    }
}

int main(int argc, char *argv[]) {
    int naive[argc], compact[argc];
    int n = argc - 1;

    for (int i = 0; i < n; i++) {
        naive[i] = atoi(argv[i + 1]);
    }

    encode16(compact, naive, n);
    for (int i = 0; i < n; i++) {
        printf("%d ", compact[i]);
    }
    printf("\n");

    decode16(naive, compact, n);
    for (int i = 0; i < n; i++) {
        printf("%d ", naive[i]);
    }
    printf("\n");
    return 0;
}

代码使用64位无符号整数来保存范围为0..15的16个值的数组。这样的数组可以在一个步骤中并行更新,提取值很简单,删除一个值有点麻烦,但仍然只有几个步骤。

您可以使用非可移植的128位整数将此方法扩展到25个位置(gcc和clang都支持类型__int128),将每个位置编码为5位,利用{{{ 1}},但神奇的常数写起来比较麻烦。

答案 2 :(得分:2)

您的编码技术具有以下属性:输出元组的每个元素的值取决于相应元素的值以及输入元组的所有前面元素。 我没有看到在计算一个编码元素期间累积部分结果的方法,该编码元素可以在计算不同的编码元素时重复使用,没有这个,编码的计算可以比(或时间)更高效地扩展。 o(n 2 )要编码的元素数量。因此,对于您描述的问题规模,我认为您可以做得比这更好:

typedef <your choice> element_t;

void encode(element_t in[], element_t out[], int num_elements) {
    for (int p = 0; p < num_elements; p++) {
        element_t temp = in[p];

        for (int i = 0; i < p; i++) {
            temp -= (in[i] < in[p]);
        }

        out[p] = temp;
    }
}

相应的解码可以这样完成:

void decode(element_t in[], element_t out[], int num_elements) {
    for (int p = 0; p < num_elements; p++) {
        element_t temp = in[p];

        for (int i = p - 1; i >= 0; i--) {
            temp += (in[i] <= temp);
        }

        out[p] = temp;
    }
}

有些方法可以更好地扩展,其中一些方法在评论和其他答案中进行了讨论,但我最好的猜测是你的问题规模不足以改进扩展,以克服增加的开销。

显然,这些转换本身并不会改变表示的大小。然而,编码表示 更容易验证,因为元组中的每个位置都可以独立于其他位置进行验证。因此,有效元组的整个空间也可以在编码形式中比在解码形式中更有效地枚举。

我继续认为解码后的表单几乎与编码表单一样有效,特别是如果您希望能够处理单个位置描述。如果您编码表单的目标是支持批量枚举,那么您可以考虑在&#34;编码&#34;中计算元组。表单,但存储并随后以解码的形式使用它们。所需的少量额外空间可能非常值得,因为不需要在阅读后执行解码,特别是如果您打算阅读大量这些内容。

<强>更新

在回复您的评论时,会议室中的大象是如何将编码形式转换为您描述的单个索引的问题,以便尽可能少地使用未使用的索引。我认为这是产生如此多讨论的脱节,你认为是偏离主题的,我认为你有一些假设,这些假设可以为你节省24倍的空间节省。

编码的表单 更容易转换为紧凑索引。例如,您可以将该位置视为小端数字,其中电路板大小为其基数:

#define BOARD_SIZE 25
typedef <big enough> index_t;

index_t to_index(element_t in[], int num_elements) {
    // The leading digit must not be zero
    index_t result = in[num_elements - 1] + 1;

    for (int i = num_elements - 1; i--; ) {
        result = result * BOARD_SIZE + in[i];
    }    
}

当然,仍然存在差距,但我估计它们在所使用的指数值的总体范围中构成相当小的比例(并且安排这样做是因为采用小端解释的原因)。我把逆向转换留作练习:)。

答案 3 :(得分:1)

要从朴素位置转换为紧凑位置,您可以遍历n元组并为每个位置p执行以下步骤:

  1. 可选择检查位置p是否可用
  2. 将排名p设置为忙碌
  3. p减去忙碌的较低位置数
  4. 将结果存储到目标n-tuple
  5. 您可以通过为忙碌状态维护n位数组来执行此操作:

    • 步骤1,2和4以恒定时间计算
    • 如果数组很小,则可以有效地计算步骤3,即:64位。

    这是一个实现:

    #include <stdio.h>
    #include <stdlib.h>
    
    /* version for up to 9 positions */
    #define BC9(n)  ((((n)>>0)&1) + (((n)>>1)&1) + (((n)>>2)&1) + \
                     (((n)>>3)&1) + (((n)>>4)&1) + (((n)>>5)&1) + \
                     (((n)>>6)&1) + (((n)>>7)&1) + (((n)>>8)&1))
    #define x4(m,n)    m(n), m((n)+1), m((n)+2), m((n)+3)
    #define x16(m,n)   x4(m,n), x4(m,(n)+4), x4(m,(n)+8), x4(m,(n)+12)
    #define x64(m,n)   x16(m,n), x16(m,(n)+16), x16(m,(n)+32), x16(m,(n)+48)
    #define x256(m,n)  x64(m,n), x64(m,(n)+64), x64(m,(n)+128), x64(m,(n)+192)
    
    static int const bc512[1 << 9] = {
        x256(BC9, 0),
        x256(BC9, 256),
    };
    
    int encode9(int dest[], int src[], int n) {
        unsigned int busy = 0;
        for (int i = 0; i < n; i++) {
            int p = src[i];
            unsigned int bit = 1 << p;
            //if (busy & bit) return 1;  // optional validity check
            busy |= bit;
            dest[i] = p - bc512[busy & (bit - 1)];
        }
        return 0;
    }
    
    /* version for up to 64 positions */
    static inline int bitcount64(unsigned long long m) {
        m = m - ((m >> 1) & 0x5555555555555555);
        m = (m & 0x3333333333333333) + ((m >> 2) & 0x3333333333333333);
        m = (m + (m >> 4)) & 0x0f0f0f0f0f0f0f0f;
        m = m + (m >> 8);
        m = m + (m >> 16);
        m = m + (m >> 16 >> 16);
        return m & 0x3f;
    }
    
    int encode64(int dest[], int src[], int n) {
        unsigned long long busy = 0;
        for (int i = 0; i < n; i++) {
            int p = src[i];
            unsigned long long bit = 1ULL << p;
            //if (busy & bit) return 1;  // optional validity check
            busy |= bit;
            dest[i] = p - bitcount64(busy & (bit - 1));
        }
        return 0;
    }
    
    int main(int argc, char *argv[]) {
        int src[argc], dest[argc];
        int cur, max = 0, n = argc - 1;
    
        for (int i = 0; i < n; i++) {
            src[i] = cur = atoi(argv[i + 1]);
            if (max < cur)
                max = cur;
        }
        if (max < 9) {
            encode9(dest, src, n);
        } else {
            encode64(dest, src, n);
        }
        for (int i = 0; i < n; i++) {
            printf("%d ", dest[i]);
        }
        printf("\n");
        return 0;
    }
    

    核心优化是{{​​1}}的实施,您可以通过将其专门化到实际的职位数量来定制您的需求。我发布了上述高效解决方案,适用于高达9的小数和高达64的大数,但您可以为12或32个位置制定更有效的解决方案。

    就时间复杂度而言,在一般情况下,我们仍然有 O(n 2 ,但对于bitcount()的小值,它实际上是以 O(n.Log(n))或更好的方式运行,因为并行n的实现可以减少到bitcount()以上的log(n)步或更少至64岁。

    你可以看一下http://graphics.stanford.edu/~seander/bithacks.html#CountBitsSetNaive的灵感和惊奇。

    不幸的是,我仍在寻找使用此方法或类似技巧进行解码的方法......

答案 4 :(得分:1)

在这个答案中,我想展示一些我自己的想法,以实现转换以及一些基准测试结果。

您可以找到代码on Github。这些是我主机上的结果:

algorithm   ------ total  time ------  ---------- per  call -----------
            decoding encoding total    decoding   encoding   total
baseline    0.0391s  0.0312s  0.0703s    3.9062ns   3.1250ns   7.0312ns
count       1.5312s  1.4453s  2.9766s  153.1250ns 144.5312ns 297.6562ns
bitcount    1.5078s  0.0703s  1.5781s  150.7812ns   7.0312ns 157.8125ns
decrement   2.1875s  1.7969s  3.9844s  218.7500ns 179.6875ns 398.4375ns
bin4        2.1562s  1.7734s  3.9297s  215.6250ns 177.3438ns 392.9688ns
bin5        2.0703s  1.8281s  3.8984s  207.0312ns 182.8125ns 389.8438ns
bin8        2.0547s  1.8672s  3.9219s  205.4688ns 186.7188ns 392.1875ns
vector      0.3594s  0.2891s  0.6484s   35.9375ns  28.9062ns  64.8438ns
shuffle     0.1328s  0.3438s  0.4766s   13.2812ns  34.3750ns  47.6562ns
tree        2.0781s  1.7734s  3.8516s  207.8125ns 177.3438ns 385.1562ns
treeasm     1.4297s  0.7422s  2.1719s  142.9688ns  74.2188ns 217.1875ns
bmi2        0.0938s  0.0703s  0.1641s    9.3750ns   7.0312ns  16.4062ns

实现

  • 基线是一种除了读取输入外什么都不做的实现。它的目的是测量函数调用和内存访问开销。
  • count 是一种“天真”的实施方式,可以存储一张占用地图,指出哪些方块上已有碎片
  • bitcount 是相同的,但占用地图存储为位图。 __builtin_popcount用于编码,大大加快了速度。如果使用手写的popcount, bitcount 仍然是最快的便携式编码实现。
  • 减量是第二个天真的实现。它存储了每个方块板的编码,在添加一块后,右边的所有方形都会递减。
  • bin4 bin5 bin8 根据user3386109的建议,对包含4,5和8个条目的分档使用分箱
  • shuffle 根据Fisher-Yates shuffle计算略有不同的编码。它的工作原理是重构随机值,这些随机值会进入一个shuffle,产生我们想要编码的permuation。该代码是无分支且快速的,特别是在解码时。
  • 向量使用chqrlie建议的五位数向量。
  • 使用差异树,这是我组建的数据结构。它是一个完整的二叉树深度⌈log 2 n ⌉其中叶子代表每个方格,每个方向的路径上的内部节点将总和留给该方块的代码(只添加你右转的节点)。不存储平方数,导致 n - 1个额外内存字。

    使用这种数据结构,我们可以在⌈log 2 n ⌉ - 1步中计算每个方块的代码,并将一个方块标记为占用相同数量的脚步。内循环是very simple,包括分支和两个动作,具体取决于您是向左还是向右下降。在ARM上,此分支编译为一些条件指令,从而实现非常快速的实现。在x86上,gcc和clang都不够聪明,无法摆脱分支。

  • treeasm 的变体,它使用inline assembly通过小心操作来实现的内部循环而没有分支携带旗帜。
  • bmi2 使用BMI2指令集中的pdeppext指令以非常快的方式实现算法。

对于我的实际项目,我可能会使用 shuffle 实现,因为它是最快的,不依赖于任何不可移植的扩展(例如Intel内在函数)或实现细节(例如作为128位整数的可用性。)

答案 5 :(得分:0)

从(5,2,3,0,7,4)到(5,2,2,0,3,1)你只需要:

  • 以(5,2,3,0,7,4)开头,在结果中推五(<)
  • 取2并计算前面的值小于2,0,然后按2-0:(5,2)
  • 取3,计算前面的数值小于3,1然后按3-1:(5,2,2)
  • 取0,计算前面的值小于0,0然后按0-0(5,2,2,0)
  • 取7,计数......,然后按7-4:(5,2,2,0,3)
  • 取4,计数......,3然后按4-3:(5,2,2,0,3,1)