초음속 도미노 타일링


10

직무

STDIN에서 또는 명령 행 인수로 3 개의 정수 m , n 을 읽고 m × n x 2 x 11 × 2 도미노 의 직사각형 사각형의 가능한 모든 타일링 과 마지막으로 유효한 타일링 수를 인쇄하는 프로그램을 작성하십시오.

개별 타일의 도미노는 2 × 1의- 경우 2 개의 대시 ( ) 와 1 × 2의 도미노의 경우 2 개의 세로 막대 ( |)로 표시해야 합니다. 각 타일 (마지막 타일 포함) 뒤에 줄 바꿈이 있어야합니다.

점수를 매기려면 STDIN의 플래그를 받거나 명령 줄 인수로 프로그램이 유효한 타일 수만 인쇄하지만 타일 자체는 인쇄하지 않아야합니다.

프로그램이 1024 바이트를 초과 할 수 없습니다. m × n ≤ 64 가되도록 모든 입력에 대해 작동해야합니다 .

( 4x6 직사각형의 모든 도미노 타일링을 인쇄 하여 영감을 얻었습니다 .)

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

||--
||--

|--|
|--|

--||
--||

||||
||||

5
$ sdt 4 2 scoring
5

채점

점수는 플래그가 설정된 입력 8 8 의 프로그램 실행 시간에 따라 결정됩니다 .

가장 빠른 컴퓨터 문제가 아닌 가장 빠른 코드로 만들기 위해 공식 점수를 결정하기 위해 내 컴퓨터 (Intel Core i7-3770, 16 GiB PC3-12800 RAM)에서 모든 제출물을 실행합니다.

코드를 컴파일 및 / 또는 실행하는 방법에 대한 자세한 지침을 남겨주십시오. 특정 버전의 언어 컴파일러 / 인터프리터가 필요한 경우 해당 효과에 대해 설명하십시오.

다음과 같은 경우 제출물을 채점하지 않을 수 있습니다.

  • 내 운영 체제 (Fedora 21, 64 비트)에 대한 무료 (맥주에서와 같이) 컴파일러 / 인터프리터가 없습니다.

  • Google의 노력에도 불구하고 코드가 작동하지 않거나 내 컴퓨터에서 잘못된 출력을 생성합니다.

  • 컴파일 또는 실행에 1 시간 이상이 걸립니다.

  • 코드 또는 유일하게 사용할 수있는 컴파일러 / 인터프리터에는 시스템 호출 rm -rf ~또는 이와 유사한 기능이 포함되어 있습니다 .

리더 보드

컴파일과 실행을 모두 반복하고 루프는 10,000 번, 컴파일은 100 ~ 1 만 번 반복하고 (코드 속도에 따라) 평균을 계산하여 모든 제출의 점수를 다시 매겼습니다.

결과는 다음과 같습니다.

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

2
샌드 박스에서 제안한 경우있을 수 있습니다. 그것은 내 CPU와 많은 작업을 저장했을 것입니다 ...
Dennis

3
@ kirbyfan64sos 내가 이해하는 한 가지 유형의 도미노는 회전 할 수 있습니다. 가로이면 다음과 같습니다 --.. 세로이면 두 개 |, 다른 하나 아래에 있습니다.
Reto Koradi

1
당신의 도전은 나쁘지 않습니다. 문제는 우리의 최고 코더가 너무 강하다는 것입니다. 행과 열의 유효성을 검사하는 솔루션은 6x8에서 1 분 정도 유지됩니다.
edc65

1
가장 좋은 전략은 어셈블리를 사용하고 1024 바이트 미만의 이진 파일을 가져 와서 합병증 시간을 없애는 것입니다.
jimmy23013

답변:


5

간단한 구현 ...

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

더 빠른 알고리즘에 대한 설명

왼쪽에서 오른쪽으로 스캔하고 상태를 유지합니다 d[i][j].

  • i[0,m)있습니다. 현재 열을 의미합니다.
  • j는 size의 비트 벡터이며 n, i이 열에 대한 작업을 시작하기 전에 열의 해당 위치 가 이미 사용중인 경우 비트는 1 입니다. 즉,의 오른쪽 절반이 차지합니다 --.
  • d[i][j] 타일링의 총 개수입니다.

그런 다음 e[i][j]= = d[i][k]세로 도미노 기반을 놓을 수있는 위치 의 합을 말 k하십시오 j. e[i][j]의 각 1 비트 j가 왼쪽의 절반 이외의 다른 점유에 의해 점유되는 타일링 수입니다 --. 로 채우기 --당신은 얻을 것이다 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(Knapsack 문제의 알고리즘). 그리고 이것은 O (n * 2 ^ n)가됩니다. 그리고 다른 모든 것은 구현 세부 사항입니다. 전체 코드는 O (m * n * 2 ^ n)로 실행됩니다.


@Dennis 당신은 아마 그것을 바꾸기 위해 투표를 시작하고 싶을 것입니다.
jimmy23013 2016 년

@Dennis 크기를 늘리는 데 많은 도움이되었을 것입니다. 계산 시간을 크게 늘리는 반면 약 100 배 더 많은 출력을 생성합니다. 상대적으로 말하자면 출력량은 실제로 더 큽니다.
레토 코라디

첫 번째 버전 실행 : 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

1
실행 : 190 MS 편집 : 68 MS 합계 : 258 밀리 초 ( -O1스위트 스팟을 것 같다 나는 모든 최적화 레벨을 시도했습니다..)
데니스

3

일련의 최적화 후 수정 된 규칙에 적합합니다.

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위치와 가로 막대가있는 위치가 포함됩니다. 이 비트 마스크를 ASCII 형식으로 변환하고 실제 출력 만 건너 뜁니다.


@ 데니스 새로운 버전. 실행은 변경되지 않지만 컴파일 속도는 빨라야합니다. 컴파일 시간을 최적화해야한다면 시스템 헤더가 필요 없습니다!
Reto Koradi 2016 년

@Dennis 새로운 스코어링 및 일련의 최적화를 위해 업데이트되었습니다. 나는 지금 최적화를 원한다 -O2.
레토 코라디

실행 : 256ms 컴파일 : 65ms 합계 : 321ms ( -O2스위트 스폿 인 것 같습니다. 모든 최적화 수준을 시도했습니다.)
Dennis

1

개념은 먼저 수평 도미노의 가능한 모든 배열을 찾아서 저장 r[]한 다음 수직 도미노의 모든 가능한 배열을 제공하도록 구성하는 것입니다.

가로 도미노를 행으로 배치하는 코드는 https://codegolf.stackexchange.com/a/37888/15599 의이 답변에서 수정되었습니다 . 넓은 그리드에서는 느리지 만 8x8 경우에는 문제가되지 않습니다.

혁신은 열이 조립되는 방식에 있습니다. 보드에 홀수 행이 있으면 입력 구문 분석에서 90도 회전하므로 이제 짝수 행이됩니다. 이제 중심선에 세로 도미노를 배치합니다. 대칭으로 인해 c하단 반에 나머지 도미노를 배열하는 방법이 있다면 c상단 반에 나머지 도미노를 배열하는 방법 도 있어야합니다 . 중심선에 수직 도미노가 주어진 배열에 대해 c*c가능한 해결책 이 있음을 의미 합니다 . 따라서 프로그램이 솔루션 수만 인쇄해야하는 경우 중심선과 보드의 절반 만 분석됩니다.

f()수평 도미노의 가능한 배열 표를 작성하고 중심선에서 가능한 수직 도미노의 배열을 통해 스캔합니다. 그런 다음 g()행을 채우는 재귀 함수를 호출 합니다. 인쇄가 필요한 경우 h()이를 위해 기능 이 호출됩니다.

g()3 개의 매개 변수로 호출됩니다. y현재 행이며 d중앙에서 바깥쪽으로 보드를 채우는 방향 (위 또는 아래)입니다. x비트 맵을 포함하면 이전 행에서 불완전한 수직 도미노를 나타냅니다. r []에서 연속으로 모든 도미노 배열을 시도합니다. 이 배열에서 1은 세로 도미노를 나타내고 한 쌍의 0은 가로 도미노를 나타냅니다. 배열의 유효한 항목은 마지막 행에서 불완전한 수직 도미노를 완료하기에 최소한 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()하도록 변경할 수 있습니다. (편집 : 필요하지 않음, 의견 참조)

EDIT : 새로운 기능 e()의 패리티를 검사하는 데 사용되었다 i의 패리티 (중심선 벌리고 도미노의 수 즉.) i(기판의 각각 절반으로 돌출 중심선에 반 도미노의 수)와 동일해야 n/2도미노가 사용 가능한 모든 공간을 채울 수 있기 때문에 각 절반의 총 공간 수의 확률 (로 표시 ). 이 편집은 i 값의 절반을 제거하므로 프로그램을 약 2 배 빠르게 만듭니다.


실행 : 18ms 컴파일 : 50ms 합계 : 68ms ( -O0전체의 달콤한 지점이었습니다. 다른 옵션은 컴파일 속도를 늦췄습니다.)
Dennis

이것은 input에 대해 절대 종료되지 않거나 적어도 시간이 오래 걸립니다 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 바이트 미만이며 컴파일 시간에 미치는 영향은 최소화되어야합니다. 어젯밤 너무 피곤해서이 패치들을 포함시키지 않았습니다.
레벨 리버 St

나는 지난 밤에 의견을 말하려고했지만 잊어 버렸습니다. 점수는 정사각형으로 이루어지기 때문에 특정 순서를 고집하지 않겠습니다.
Dennis
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.