我正在为日本象棋变体写一个桌面。为了索引表基,我将每个国际象棋位置编码为整数。在其中一个编码步骤中,我编码棋盘上的棋子。由于实际方法有点复杂,让我以简化的方式解释这个问题。
在最后的桌面游戏中,我有(比方说)六个不同的棋子,我想在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作为程序。在这个问题的上下文中,我对使用非标准结构,内联汇编或内在函数的答案不感兴趣。
因为这个问题似乎有些混乱:
答案 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 x
中y
右侧的所有值都会递减。
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
执行以下步骤:
p
是否可用p
设置为忙碌p
减去忙碌的较低位置数您可以通过为忙碌状态维护n
位数组来执行此操作:
这是一个实现:
#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
__builtin_popcount
用于编码,大大加快了速度。如果使用手写的popcount, bitcount 仍然是最快的便携式编码实现。树使用差异树,这是我组建的数据结构。它是一个完整的二叉树深度⌈log 2 n ⌉其中叶子代表每个方格,每个方向的路径上的内部节点将总和留给该方块的代码(只添加你右转的节点)。不存储平方数,导致 n - 1个额外内存字。
使用这种数据结构,我们可以在⌈log 2 n ⌉ - 1步中计算每个方块的代码,并将一个方块标记为占用相同数量的脚步。内循环是very simple,包括分支和两个动作,具体取决于您是向左还是向右下降。在ARM上,此分支编译为一些条件指令,从而实现非常快速的实现。在x86上,gcc和clang都不够聪明,无法摆脱分支。
pdep
和pext
指令以非常快的方式实现算法。对于我的实际项目,我可能会使用 shuffle 实现,因为它是最快的,不依赖于任何不可移植的扩展(例如Intel内在函数)或实现细节(例如作为128位整数的可用性。)
答案 5 :(得分:0)
从(5,2,3,0,7,4)到(5,2,2,0,3,1)你只需要: