Сверхзвукові обшивки доміно


10

Завдання

Напишіть програму, яка зчитує три цілі числа m , n або з STDIN, або як аргументи командного рядка, друкує всі можливі нахили прямокутника розмірами m × n на 2 × 1 і 1 × 2 доміно і, нарешті, кількість дійсних обмоток.

Доміно окремої черепиці повинно бути представлене двома штрихами ( -) на 2 × 1 та двома вертикальними брусками ( |) для 1 × 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 ОЗУ), щоб визначити офіційний бал.

Будь ласка, залиште детальні інструкції щодо компіляції та / або виконання коду. Якщо вам потрібна певна версія компілятора / перекладача вашої мови, зробіть заяву на це.

Я залишаю за собою право залишати матеріали без захисту, якщо:

  • Для моєї операційної системи немає безкоштовного (як у пива) компілятора / інтерпретатора (Fedora 21, 64 біт).

  • Незважаючи на наші зусилля, ваш код не працює та / або видає неправильний висновок на моєму комп’ютері.

  • Складання або виконання займає більше години.

  • Ваш код або єдиний доступний компілятор / інтерпретатор містять системний виклик rm -rf ~або щось рівномірне.

Таблиця лідерів

Я повторно оцінив всі подані матеріали, виконуючи і компіляції, і виконання у циклі з 10 000 ітерацій для компіляції та від 100 до 10000 ітерацій для виконання (залежно від швидкості коду) та обчислення середнього значення.

Такі результати:

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.

Чому б не зробити це конкурсом GOLF? :(
orlp

2
Якби ти запропонував це в пісочниці, я б міг. Це врятувало б мій процесор і мені багато роботи ...
Денніс

3
@ kirbyfan64sos Наскільки я це розумію, існує лише один тип доміно, який можна обертати. Якщо це горизонтальне, це виглядає наступним чином : --. Якщо вона вертикальна, це дві |, одна нижче іншої.
Рето Коради

1
Ваш виклик непоганий. Проблема полягає в тому, що наші топ-кодери занадто сильні. Моє рішення, яке перевіряє дійсність рядків і стовпців, залишається близько 1 хвилини протягом 6x8.
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- бітовий вектор розміру n, де біт був би 1, якщо відповідна позиція у стовпці iвже зайнята до початку роботи над цим стовпцем. Тобто вона зайнята правою половиною a --.
  • 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

@Dennis Не впевнений, що збільшення розміру могло б дуже допомогти. Хоча це значно збільшує час обчислення, воно також дає приблизно в 100 разів більше випуску. Відносно кажучи, обсяг випуску продукції фактично більший.
Рето Коради

1-я версія Виконання: 0,286 с. Компіляція: 0,053 с. Сума: 0,339 с. 2-е версія. Виконання: 0,002 с. Компіляція: 0,061 с. Сума: 0,063 с. (Що тут що сталося?)
Денніс

@Dennis Він використовував інший алгоритм в O (m * n * 2 ^ n), якщо прапор встановлений.
jimmy23013

1
Виконання: 190 мс Компіляція: 68 мс Сума: 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 та фактичний вихід.


@Dennis Нова версія. Виконання має бути незмінним, але компіляція швидшою. Якщо нам потрібно оптимізувати час компіляції, нам не потрібні заголовки системи!
Рето Кораді

@Dennis Оновлено для нових балів та для оптимізації. Зауважте, що я зараз хочу оптимізації, щось подібне -O2повинно бути добре.
Рето Коради

Виконання: 256 мс Компіляція: 65 мс Сума: 321 мс ( -O2здається, це приємне місце. Я спробував усі рівні оптимізації.)
Денніс

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 являє собою вертикальне доміно, а пара нулів - горизонтальне доміно. Дійсний елемент масиву повинен мати принаймні , досить 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: не потрібно, дивіться коментарі.)

EDIT: Нова функція e()була використана для перевірки паритету i(тобто кількості доміно, що перетинають центральну лінію). Паритет i(кількість напівдоміно на центральній лінії, що виступає в кожну половину дошки), має бути таким же, як непарність загальної кількості пробілів у кожній половині (задана n/2), оскільки лише тоді доміно може заповнити весь наявний простір. Це редагування виключає половину значень i, тому робить мою програму приблизно вдвічі швидшою.


Виконання: 18 мс Компіляція: 50 мс Сума: 68 мс (це -O0було солодким місцем для загальної кількості. Інші параметри сповільнили компіляцію.)
Денніс

Це або ніколи не припиняється, або принаймні забирає дуже тривалий час для введення інформації 32 2 s. Я зупинив це приблизно через 15 хвилин.
Рето Коради

@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[]якого, безумовно, має можливість вдосконалити.
Рівень річки Св.

Для повноти ось патч для повернення виводу в потрібний спосіб, якщо потрібно: 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 байт, і вплив на час компіляції повинен бути мінімальним. Я не включив ці патчі минулої ночі, оскільки я був занадто втомлений.
Рівень річки Св.

Я мав намір прокоментувати це вчора ввечері, але забув. Оскільки бал проводиться на площі, я не збираюся наполягати на певному наказі.
Денніс
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.