超音波多米诺瓷砖


10

任务

编写一个程序,从STDIN或作为命令行参数读取三个整数mn,打印出所有可能的矩形为m×n且2×11×2 Domino组成的平铺,最后打印有效平铺的数量。

单个平铺的多米诺骨牌必须用两个破折号(-)表示2×1,用两个竖线(|)表示1×2的多米诺骨牌。每个平铺(包括最后一个平铺)都必须紧跟换行符。

出于评分目的,您还必须接受来自STDIN的标志或作为命令行参数的标志,该标志使您的程序仅打印有效拼贴的数量,而不打印拼贴本身。

您的程序不得超过1024个字节。它必须适用于所有输入,使得m×n≤64

(灵感来自于打印所有4x6矩形的多米诺骨牌。)

$ sdt 4 2
----
----

||--
||--

|--|
|--|

--||
--||

||||
||||

5
$ sdt 4 2 scoring
5

计分

您的分数取决于设置了标志的输入8 8的程序执行时间。

为了使此代码成为最快的代码,而不是最快的计算机挑战,我将在自己的计算机上运行所有提交的文件(英特尔酷睿i7-3770、16 GiB PC3-12800 RAM)以确定官方成绩。

请留下有关如何编译和/或执行代码的详细说明。如果您需要使用特定版本的语言的编译器/解释器,请对此声明。

如果发生以下情况,我保留不对提交内容进行评分的权利:

  • 我的操作系统(Fedora 21,64位)没有免费的(如在啤酒中)编译器/解释器。

  • 尽管我们竭尽全力,但是您的代码无法正常工作和/或在我的计算机上产生不正确的输出。

  • 编译或执行需要一个多小时。

  • 您的代码或唯一可用的编译器/解释器包含对的系统调用rm -rf ~或同样令人讨厌的东西。

排行榜

我对所有提交进行了重新评分,以10,000次迭代进行编译,并以100至10,000次迭代执行(取决于代码的速度),并在循环中运行编译和执行(取决于代码的速度)并计算平均值。

结果是:

User          Compiler   Score                              Approach

jimmy23013    GCC (-O0)    46.11 ms =   1.46 ms + 44.65 ms  O(m*n*2^n) algorithm.
steveverrill  GCC (-O0)    51.76 ms =   5.09 ms + 46.67 ms  Enumeration over 8 x 4.
jimmy23013    GCC (-O1)   208.99 ms = 150.18 ms + 58.81 ms  Enumeration over 8 x 8.
Reto Koradi   GCC (-O2)   271.38 ms = 214.85 ms + 56.53 ms  Enumeration over 8 x 8.

为什么不将其设为高尔夫比赛?:(
orlp 2015年

2
如果您曾在沙箱中建议过,那我可能有。那会节省我的CPU和我很多的工作...
丹尼斯

3
@ kirbyfan64sos以我的理解,只有一种类型的多米诺骨牌可以旋转。如果是水平的,则如下所示:--。如果是垂直的,则为两个|,一个在另一个下方。
Reto Koradi 2015年

1
您的挑战还不错。问题是我们的顶级编码人员太强大了。我的用于检查行和列有效性的解决方案在6x8的时间内保持接近1分钟的时间。
edc65

1
我认为现在最好的策略是使用汇编,并尝试获取小于1024字节的二进制文件,以消除复杂时间。
jimmy23013 2015年

Answers:


5

C

一个简单的实现...

#include<stdio.h>
int a,b,c,s[65],l=0,countonly;
unsigned long long m=0;
char r[100130];
void w(i,x,o){
    int j,k;
    j=(1<<b)-1&~x;
    if(i<a-1){
        s[i+1]=j;
        w(i+1,j,1);
        for(k=j&j/2&-o;j=k&-k;k&=~j)
            w(i,x|3*j,j);
    }
    else if((j/3|j/3*2)==j)
        if(countonly)
            m++;
        else{
            if(c==b)
                for(k=0;k<b;r[k++,l++]=10)
                    for(j=0;j<a;j++)
                        r[l++]=45+79*!((s[j]|s[j+1])&(1<<k));
            else
                for(j=0;j<a;r[j++,l++]=10)
                    for(k=0;k<b;k++)
                        r[l++]=124-79*!((s[j]|s[j+1])&(1<<k));
            r[l++]=10;
            if(l>=100000){
                fwrite(r,l,1,stdout);
                l=0;
            }
        }
}

int main(){
    scanf("%d %d %d",&a,&b,&countonly);
    c=b;
    if(a<b){a^=b;b^=a;a^=b;}
    s[0]=s[a]=0;
    w(0,0,1);
    if(countonly)
        printf("%llu\n",m);
    else if(l)
        fwrite(r,l,1,stdout);
}

作弊版

#include<stdio.h>
#include<string.h>
int a,b,c,s[65],l=0,countonly;
unsigned long long m=0,d[256];
char r[100130];
void w2(){
    int i,j,k,x;
    memset(d,0,sizeof d);
    d[0]=1;
    j=0;
    for(i=0;i<a-1;i++){
        for(k=1;k<(1<<(b-1));k*=2)
            for(x=0;x<(1<<(b-2));x++)
                d[(x+x/k*k*3+k*3)^j]+=d[(x+x/k*k*3)^j];
        j^=(1<<b)-1;
    }
    for(x=0;x<(1<<b);x++)
        if((x/3|x/3*2)==x)
            m+=d[x^((1<<b)-1)^j];
    printf("%llu\n",m);
}

void w(i,x,o){
    int j,k;
    j=(1<<b)-1&~x;
    if(i<a-1){
        s[i+1]=j;
        w(i+1,j,1);
        for(k=j&j/2&-o;j=k&-k;k&=~j)
            w(i,x|3*j,j);
    }
    else if((j/3|j/3*2)==j){
        if(c==b)
            for(k=0;k<b;r[k++,l++]=10)
                for(j=0;j<a;j++)
                    r[l++]=45+79*!((s[j]|s[j+1])&(1<<k));
        else
            for(j=0;j<a;r[j++,l++]=10)
                for(k=0;k<b;k++)
                    r[l++]=124-79*!((s[j]|s[j+1])&(1<<k));
        r[l++]=10;
        if(l>=100000){
            fwrite(r,l,1,stdout);
            l=0;
        }
    }
}

int main(){
    scanf("%d %d %d",&a,&b,&countonly);
    c=b;
    if(a<b){a^=b;b^=a;a^=b;}
    s[0]=s[a]=0;
    if(countonly)
        w2();
    else{
        w(0,0,1);
        if(l)
            fwrite(r,l,1,stdout);
    }
}

更快算法的说明

它从左向右扫描,并保持state d[i][j],其中:

  • i在中[0,m),表示当前列。
  • j是size的位向量,n如果i在开始处理此列之前已经占用了列中的相应位置,则该位为1 。即它被一个右半边占据--
  • d[i][j] 是不同拼贴的总数。

然后说e[i][j]=的总和d[i][k],你可以把垂直骨牌基地k形成je[i][j]将是每个1比特j被a的左半部分以外的任何东西占用的平铺数目--。用它们填充--,您将获得d[i+1][~j]= e[i][j]e[m-1][every bit being 1]还是d[m][0]最终答案。

天真的实现会使您的时间复杂度接近g[n]=2*g[n-1]+g[n-2] = O((sqrt(2)+1)^n)(如果n = m = 8,已经足够快了)。但是,您可以首先循环查找每个可能的多米诺骨牌,然后尝试将其添加到可以添加此多米诺骨牌的每个平铺中,然后将结果合并到原始数组中d(例如背包问题的算法)。这就是O(n * 2 ^ n)。其他所有都是实现细节。整个代码在O(m * n * 2 ^ n)中运行。


@Dennis您可能想开始投票以对其进行更改。
jimmy23013 2015年

@Dennis不确定增加大小会有所帮助。尽管它大大增加了计算时间,但它也产生了大约100倍的输出。相对而言,输出量实际上更大。
Reto Koradi

第一版执行:0.286 s编译:0.053 s总和:0.339 s 第二版执行:0.002 s编译:0.061 s总和:0.063 s (这里发生了什么?)
Dennis

@Dennis如果设置了标志,则在O(m * n * 2 ^ n)中使用另一种算法。
jimmy23013 2015年

1
执行:190毫秒编译:68毫秒总计:258毫秒(-O1似乎是最佳选择。我已经尝试了所有优化级别。)
Dennis

3

C

经过一轮优化,并针对修改后的规则进行了调整:

typedef unsigned long u64;

static int W, H, S;
static u64 RM, DM, NSol;
static char Out[64 * 2 + 1];

static void place(u64 cM, u64 vM, int n) {
  if (n) {
    u64 m = 1ul << __builtin_ctzl(cM); cM -= m;

    if (m & RM) {
      u64 nM = m << 1;
      if (cM & nM) place(cM - nM, vM, n - 1);
    }

    if (m & DM) {
      u64 nM = m << W;
      vM |= m; vM |= nM; place(cM - nM, vM, n - 1);
    }
  } else if (S) {
    ++NSol;
  } else {
    char* p = Out;
    for (int y = 0; y < H; ++y) {
      for (int x = 0; x < W; ++x) { *p++ = vM & 1 ? '|' : '-'; vM >>= 1; }
      *p++ = '\n';
    }
    *p++ = '\0';
    puts(Out);
    ++NSol;
  }
}

int main(int argc, char* argv[]) {
  W = atoi(argv[1]); H = atoi(argv[2]); S = (argc > 3);

  int n = W * H;
  if (n & 1) return 0;

  for (int y = 0; y < H; ++y) {
    RM <<= W; RM |= (1ul << (W - 1)) - 1;
  }
  DM = (1ul << (W * (H - 1))) - 1;

  place(-1, 0, n >> 1);
  printf("%lu\n", NSol);

  return 0;
}

我开始碰到1024个字符的长度限制,因此不得不降低可读性。短得多的变量名,等等。

制作说明:

> gcc -O2 Code.c

在启用解决方案输出的情况下运行:

> ./a.out 8 8 >/dev/null

仅以解决方案计数运行:

> ./a.out 8 8 s

一些评论:

  • 对于较大的测试示例,我现在想进行优化。尽管我的系统不同(Mac),但是周围-O2似乎还不错。
  • 对于生成输出的情况,代码变得越来越慢。为了优化“仅计数”模式并减少代码长度,这是有意识的牺牲。
  • 由于缺少系统功能的包含和外部声明,将会出现一些编译器警告。这是在不使代码完全不可读的情况下,使我最终达到1024个字符以下的最简单方法。

还要注意,即使在“仅计数”模式下,代码仍会生成实际的解决方案。每当找到解决方案时,vM位掩码都包含1具有垂直条0的位置的a和具有水平条的位置的a。仅跳过此位掩码到ASCII格式的转换以及实际输出。


@Dennis新版本。执行应保持不变,但编译速度更快。如果我们需要针对编译时间进行优化,则不需要系统头文件!
Reto Koradi 2015年

@Dennis更新以获取新的评分,并进行一轮优化。请注意,我现在确实想要优化,类似的东西-O2应该很好。
Reto Koradi

执行:256毫秒编译:65毫秒总计:321毫秒(-O2似乎是最佳选择。我已经尝试了所有优化级别。)
丹尼斯

1

C

其概念是首先找到一行中所有可能的水平多米诺骨牌排列,将它们存储起来r[],然后组织它们以给出所有可能的垂直多米诺骨牌排列。

根据我的以下答案修改了用于将水平多米诺骨牌连续放置的代码:https : //codegolf.stackexchange.com/a/37888/15599。对于较宽的网格,它的速度很慢,但这对于8x8的情况来说不是问题。

创新之处在于行的组装方式。如果开发板的行数为奇数,则在输入解析中将其旋转90度,因此现在的行数为偶数。现在,我在中心线上放置一些垂直的多米诺骨牌。由于对称性,如果c在下半部中有一些剩余的多米诺骨牌的安排,那么在上半部中还必须有c一些剩余的多米诺骨牌的安排,这意味着对于中心线上的垂直多米诺骨牌的给定排列,有c*c可能的解决方案。因此,仅在需要打印少量解决方案的程序时,仅分析中心线和电路板的一半。

f()建立水平多米诺骨牌的可能排列表,并扫描中心线上垂直多米诺骨牌的可能排列。然后调用递归函数g()填充行。如果需要打印,h()则调用函数来执行此操作。

g()用3个参数调用。y是当前行, d是我们从中心向外填充木板的方向(向上或向下)。x包含一个位图,指示上一行中不完整的垂直多米诺骨牌。尝试从r []开始的所有多米诺骨牌排列。在此数组中,1代表垂直多米诺骨牌,一对零代表水平多米诺骨牌。数组中的有效条目必须至少具有1才能完成最后一行中所有不完整的垂直多米诺骨牌(x&r[j])==x。它可能有更多的1,表明新的垂直多米诺骨牌正在开始。然后,对于下一行,我们只需要新的多米诺骨牌,因此我们再次使用调用该过程x^r[j]

如果到达了最后一行,并且板的顶部或底部没有悬挂不完整的垂直多米诺骨牌,x^r[j]==0则说明一半已成功完成。如果我们不进行打印,则只需完成下半部分并用于c*c计算布置的总数即可。如果要打印,则必须同时完成上半部分,然后调用打印功能h()

unsigned int W,H,S,n,k,t,r[1<<22],p,q[64];
long long int i,c,C;


//output: ascii 45 - for 0, ascii 45+79=124 | for 1
h(){int a;
  for(a=n;a--;a%W||puts(""))putchar(45+(q[a/W]>>a%W)%2*79);
  puts("");
}

g(y,d,x){int j;
  for(j=0;j<p;j++)if((x&r[j])==x){
    q[y]=r[j];
    if(y%(H-1)==0){
       (x^r[j])==0?
        y?c++,S||g(H/2-1,-1,i):h() 
       :0;
    }else{
      g(y+d,d,x^r[j]);
    }

  }    
}

e(z){int d;
  for(d=0;z;d++)z&=z-1;return n/2+1+d&1; 
}

f(){
  //k=a row full of 1's
  k=(1ULL<<W)-1;

  //build table of possible arrangements of horizontal dominoes in a row;
  //flip bits so that 1=a vertical domino and save to r[]
  for(i=0;i<=k;i++)(i/3|i/3<<1)==i?r[p++]=i^k:0;

  //for each arrangement of vertical dominoes on the centreline, call g()
  for(i=0;i<=k;i++)e(i)?c=0,g(H/2,1,i),C+=c*c:0;
  printf("%llu",C);
}


main(int argc, char* argv[]) {
  W = atoi(argv[1]); H = atoi(argv[2]); S = (argc > 3);
  1-(n=W*H)%2?
      H%2?W^=H,H^=W,W^=H,t=1:0,f()
    :puts("0");

}

请注意,在解析阶段将具有奇数行和偶数列的输入转过90度。如果这是不可接受的,h()则可以更改打印功能以适应它。(编辑:不是必需的,请参阅注释。)

编辑:一个新的功能e()已被用于检查的奇偶i(即骨牌横跨中心线的编号。)的奇偶性i(上突出到电路板的各一半的中心线半骨牌的数量)必须是一样每半部分中的空格总数的奇数(由给出n/2),因为只有这样,多米诺骨牌才能填充所有可用的空格。此编辑消除了i值的一半,因此使我的程序快了大约两倍。


执行:18毫秒编译:50毫秒总计:68毫秒(这-O0是总数的最佳选择。其他选项减慢了编译速度。)
Dennis

输入要么永不终止,要么至少花费很长时间32 2 s。大约15分钟后,我停了下来。
Reto Koradi

@RetoKoradi确实,但是2 32 s几乎立即运行。扫描所有可能的垂直多米诺骨牌在这种H=2情况下非常浪费,因为实际上我们已经在中拥有了所有必要的信息r[]。我对正式时间感到非常满意,8 8 s这是您提到的一个补丁:if(H==2){C=p;if(!S)for(i=0;i<p;i++)q[0]=q[1]=r[i],h();}else for(i=0;i<=k;i++)c=0,g(H/2,1,i),C+=c*c;如您所见,此代码段将H=2 在设置了标志的情况下 立即运行。这样,整个运行时间就受到其构建的限制,该构建r[]肯定还有改进的空间。
Level River St

为了完整if(t)for(a=n;a--;a%H||puts(""))putchar(124-(q[a%H]>>a/H)%2*79);else for(a=n;a--;a%W||puts(""))putchar(45+(q[a/W]>>a%W)%2*79);起见,如果需要,这里是用于按正确方式调高输出的补丁:代码长度仍远低于1000字节,并且对编译时间的影响应最小。由于我太累了,昨晚我没有包括这些补丁。
级圣河

我本打算在昨晚发表评论,但我忘了。由于计分是在正方形上完成的,因此我不会坚持特定的顺序。
丹尼斯
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.