Найшвидший найдовший пошук загальних наслідків


11

Ваше завдання - вирішити найдовшу задачу загальних наслідків для n рядків довжиною 1000.

Дійсне вирішення проблеми ЛГП для двох або більше рядків S 1 , ... S п будь-який рядок T максимальної довжини, що характери Т з'являються у всіх S I , в тому ж порядку , як і в T .

Зверніть увагу , що T не повинен бути суб рядок з S я .

Ми вже вирішили цю проблему в найкоротшій кількості коду . Цього разу розмір не має значення.

Приклад

Рядки axbyczі xaybzcмають 8 загальних підпослідовності довжини 3:

abc abz ayc ayz xbc xbz xyc xyz

Будь-яке з них було б вагомим рішенням проблеми LCS.

Деталі

Напишіть повну програму, яка вирішує проблему LCS, як пояснено вище, дотримуючись наступних правил:

  • Вхід складається з двох або більше рядків довжиною 1000, що складаються з символів ASCII з кодовими точками від 0x30 до 0x3F.

  • Ви повинні прочитати вхід із STDIN.

    У вас є два варіанти формату введення:

    • Кожен рядок (включаючи останній) супроводжується передачею рядків.

    • Струни пов'язані ланцюжком, не маючи роздільника і не затягуючи ліній.

  • Кількість рядків буде передано вашій програмі як параметр командного рядка.

  • Ви повинні написати вихід, тобто будь-яке з дійсних рішень для LCS, до STDOUT, а потім - один підводний рядок.

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

  • Якщо вам потрібні якісь прапори компілятора або певний перекладач, будь ласка, згадайте про це у своєму дописі.

Оцінка балів

Я запускатиму ваш код з 2, 3 тощо рядків, поки не буде потрібно більше 120 секунд для друку дійсного рішення. Це означає, що у вас є 120 секунд на кожне значення n .

Найбільша кількість рядків, за які ваш код закінчився в часі, - це ваша оцінка.

У разі вирівнювання балів n , подання, яке вирішило проблему для n рядків у найкоротші терміни, буде оголошено переможцем.

Усі подання будуть приурочені до моєї машини (Intel Core i7-3770, 16 Гб оперативної пам’яті, без заміни)

У п струна (п-1) й тест буде генеруватися шляхом виклику rand n(і зачистки перекладу рядка, якщо потрібно), де randвизначаються наступним чином :

rand()
{
    head -c$[500*$1] /dev/zero |
    openssl enc -aes-128-ctr -K 0 -iv $1 |
    xxd -c500 -ps |
    tr 'a-f' ':-?'
}

Ключ знаходиться 0у наведеному вище коді, але я залишаю за собою право змінити його на нерозкрите значення, якщо я підозрюю кого-небудь у (частково) жорсткому кодуванні виводу.


Чи можемо ми кинути винятки?
HyperNeutrino

@JamesSmith До тих пір, поки вихід правильний, обов'язково.
Денніс

Оскільки я читаю з буферизованим читанням, чи можу я кинути ioexception з public static void main(...)?
HyperNeutrino

@JamesSmith Я насправді не знаю Java, тому не знаю, що це, але не хвилюйтесь про винятки.
Денніс

4
@JamesSmith Оскільки ця довжина коду не має значення для цього завдання, ви не можете просто знайти винятки?
Рето Коради

Відповіді:


5

C, n = 3 за ~ 7 секунд

Алгоритм

Алгоритм - це пряме узагальнення стандартного рішення динамічного програмування nпослідовностей. Для двох рядків Aі B, стандартний повтор виглядає так:

L(p, q) = 1 + L(p - 1, q - 1)           if A[p] == B[q]
        = max(L(p - 1, q), L(p, q - 1)) otherwise

Для 3 -х рядків A, B, Cя використовую:

L(p, q, r) = 1 + L(p - 1, q - 1, r - 1)                          if A[p] == B[q] == C[r]
           = max(L(p - 1, q, r), L(p, q - 1, r), L(p, q, r - 1)) otherwise

Код реалізує цю логіку для довільних значень n.

Ефективність

Складність мого коду становить O (s ^ n), з sдовжиною рядків. Виходячи з того, що я знайшов, виглядає, що проблема неповна. Тому хоча розміщений алгоритм дуже неефективний для більших значень n, це може бути фактично неможливо зробити набагато краще. Єдине, що я бачив - це деякі підходи, які покращують ефективність для маленьких алфавітів. Оскільки алфавіт тут помірно малий (16), це може призвести до покращення. Я все ще прогнозую, що ніхто не знайде законного рішення, яке вийде вище, ніж n = 4за 2 хвилини, і n = 4вже виглядає амбітним.

Я зменшив використання пам'яті в початковій реалізації, щоб він міг обробляти n = 4заданий час. Але це створювало лише довжину послідовності, а не саму послідовність. Перевірте історію редагування цієї публікації, щоб побачити цей код.

Код

Оскільки петлі над n-мірними матрицями вимагають більше логіки, ніж фіксованих циклів, я використовую фіксовану петлю для найменшого розміру, а для інших розмірів використовую лише загальну логіку циклічного циклу.

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

#define N_MAX 4

int main(int argc, char* argv[]) {
    int nSeq = argc - 1;
    if (nSeq > N_MAX) {
        nSeq = N_MAX;
    }

    char** seqA = argv + 1;

    uint64_t totLen = 1;
    uint64_t lenA[N_MAX] = {0};
    uint64_t offsA[N_MAX] = {1};
    uint64_t offsSum = 0;
    uint64_t posA[N_MAX] = {0};

    for (int iSeq = 0; iSeq < nSeq; ++iSeq) {
        lenA[iSeq] = strlen(seqA[iSeq]);
        totLen *= lenA[iSeq] + 1;

        if (iSeq + 1 < nSeq) {
            offsA[iSeq + 1] = totLen;
        }

        offsSum += offsA[iSeq];
    }

    uint16_t* mat = calloc(totLen, 2);
    uint64_t idx = offsSum;

    for (;;) {
        for (uint64_t pos0 = 0; pos0 < lenA[0]; ++pos0) {
            char firstCh = seqA[0][pos0];
            int isSame = 1;
            uint16_t maxVal = mat[idx - 1];

            for (int iSeq = 1; iSeq < nSeq; ++iSeq) {
                char ch = seqA[iSeq][posA[iSeq]];
                isSame &= (ch == firstCh);

                uint16_t val = mat[idx - offsA[iSeq]];
                if (val > maxVal) {
                    maxVal = val;
                }
            }

            if (isSame) {
                mat[idx] = mat[idx - offsSum] + 1;
            } else {
                mat[idx] = maxVal;
            }

            ++idx;
        }

        idx -= lenA[0];

        int iSeq = 1;
        while (iSeq < nSeq && posA[iSeq] == lenA[iSeq] - 1) {
            posA[iSeq] = 0;
            idx -= (lenA[iSeq] - 1) * offsA[iSeq];
            ++iSeq;
        }
        if (iSeq == nSeq) {
            break;
        }

        ++posA[iSeq];
        idx += offsA[iSeq];
    }

    int score = mat[totLen - 1];

    char* resStr = malloc(score + 1);
    resStr[score] = '\0';

    for (int iSeq = 0; iSeq < nSeq; ++iSeq) {
        posA[iSeq] = lenA[iSeq] - 1;
    }

    idx = totLen - 1;
    int resIdx = score - 1;

    while (resIdx >= 0) {
        char firstCh = seqA[0][posA[0]];
        int isSame = 1;
        uint16_t maxVal = mat[idx - 1];
        int maxIdx = 0;

        for (int iSeq = 1; iSeq < nSeq; ++iSeq) {
            char ch = seqA[iSeq][posA[iSeq]];
            isSame &= (ch == firstCh);

            uint16_t val = mat[idx - offsA[iSeq]];
            if (val > maxVal) {
                maxVal = val;
                maxIdx = iSeq;
            }
        }

        if (isSame) {
            resStr[resIdx--] = firstCh;
            for (int iSeq = 0; iSeq < nSeq; ++iSeq) {
                --posA[iSeq];
            }
            idx -= offsSum;
        } else {
            --posA[maxIdx];
            idx -= offsA[maxIdx];
        }
    }

    free(mat);

    printf("score: %d\n", score);
    printf("%s\n", resStr);

    return 0;
}

Інструкція з бігу

Бігти:

  • Збережіть код у файлі, наприклад lcs.c.
  • Компілюйте з великими опціями оптимізації. Я використав:

    clang -O3 lcs.c
    

    У Linux я б спробував:

    gcc -Ofast lcs.c
    
  • Запустити з 2 до 4 послідовностей, поданих у якості аргументів командного рядка:

    ./a.out axbycz xaybzc
    

    Однозначно наведіть аргументи командного рядка, якщо необхідно, оскільки алфавіт, який використовується для прикладів, містить спеціальні символи оболонки.

Результати

test2.shі test3.shє тестовими послідовностями від Денніса. Я не знаю правильних результатів, але результат виглядає принаймні правдоподібно.

$ ./a.out axbycz xaybzc
score: 3
abc

$ time ./test2.sh 
score: 391
16638018802020>3??3232270178;47240;24331395?<=;99=?;178675;866002==23?87?>978891>=9<6<9381992>>7030829?255>6804:=3>:;60<9384=081;0:?66=51?0;5090724<85?>>:2>7>3175?::<9199;5=0:494<5<:7100?=95<91>1887>33>67710==;48<<327::>?78>77<6:2:02:<7=5?:;>97<993;57=<<=:2=9:8=118563808=962473<::8<816<464<1==925<:5:22?>3;65=>=;27?7:5>==3=4>>5>:107:20575347>=48;<7971<<245<??219=3991=<96<??735698;62?<98928

real  0m0.012s
user  0m0.008s
sys   0m0.003s

$ time ./test3.sh 
score: 269
662:2=2::=6;738395=7:=179=96662649<<;?82?=668;2?603<74:6;2=04=>6;=6>=121>1>;3=22=3=3;=3344>0;5=7>>7:75238;559133;;392<69=<778>3?593?=>9799<1>79827??6145?7<>?389?8<;;133=505>2703>02?323>;693995:=0;;;064>62<0=<97536342603>;?92034<?7:=;2?054310176?=98;5437=;13898748==<<?4

real  0m7.218s
user  0m6.701s
sys   0m0.513s

Вибачте, якщо це було не ясно, але ви повинні надрукувати LCS, а не лише його довжину.
Денніс

@Денніс я бачу. Деякі мої оптимізації тоді були марними. Мені доведеться повернутися до версії, яка зберігає повну матрицю, щоб я міг реконструювати рядок. Це не зможе запуститися протягом n = 4, але все ж слід закінчити нижче 10 секунд протягом n = 3. Я думаю, мені було близько 6-7 секунд, коли я ще мав повну матрицю.
Рето Коради

Знову вибачте. Питання щодо цього було не дуже зрозумілим ... Коли ви опублікуєте свій результат, я зможу порівняти його з BrainSteel. Довжина, про яку ви повідомляєте у програмі, перевищує довжину його результатів на 5 для n = 2. До речі, мені довелося визначити N_MAXяк макрос і додати прапор компілятора -std=c99для складання вашого коду з GCC.
Денніс

@Dennis Немає проблем. У ньому сказано, що рішення "є рядком", тому воно повинно було бути досить зрозумілим. Я майже ексклюзивно використовую C ++, тому я ніколи не впевнений, що дозволено в C. Цей код почався як C ++, але, як тільки я зрозумів, що насправді не використовую жодних функцій C ++, переключив його на C. clang на моєму Mac був задоволений цим, але він, ймовірно, використовує іншу версію С за замовчуванням, або просто більш м'який.
Рето Коради

1
@Dennis Добре, я додав логіку відстеження, щоб я міг створити рядок. Тепер для n = 3 потрібно близько 7 секунд.
Рето Коради

3

Ця відповідь наразі порушена через помилку. Незабаром виправлення ...

C, 2 струни за ~ 35 секунд

Це дуже триває робота (як це показало жахливий безлад), але, сподіваємось, це дає і хороші відповіді!

Код:

#include "stdlib.h"
#include "string.h"
#include "stdio.h"
#include "time.h"

/* For the versatility */
#define MIN_CODEPOINT 0x30
#define MAX_CODEPOINT 0x3F
#define NUM_CODEPOINT (MAX_CODEPOINT - MIN_CODEPOINT + 1)
#define CTOI(c) (c - MIN_CODEPOINT)

#define SIZE_ARRAY(x) (sizeof(x) / sizeof(*x))

int LCS(char** str, int num);
int getshared(char** str, int num);
int strcount(char* str, char c);

int main(int argc, char** argv) {
    char** str = NULL;
    int num = 0;
    int need_free = 0;
    if (argc > 1) {
        str = &argv[1];
        num = argc - 1;
    }
    else {
        scanf(" %d", &num);
        str = malloc(sizeof(char*) * num);
        if (!str) {
            printf("Allocation error!\n");
            return 1;
        }

        int i;
        for (i = 0; i < num; i++) {
            /* No string will exceed 1000 characters */
            str[i] = malloc(1001);
            if (!str[i]) {
                printf("Allocation error!\n");
                return 1;
            }

            scanf(" %1000s", str[i]);

            str[i][1000] = '\0';
        }

        need_free = 1;
    }

    clock_t start = clock();

    /* The call to LCS */
    int size = LCS(str, num);

    double dt = ((double)(clock() - start)) / CLOCKS_PER_SEC;
    printf("Size: %d\n", size);
    printf("Elapsed time on LCS call: %lf s\n", dt);

    if (need_free) {
        int i;
        for (i = 0; i < num; i++) {
            free(str[i]);
        }
        free(str);
    }

    return 0;
}

/* Some terribly ugly global variables... */
int depth, maximum, mapsize, lenmap[999999][2];
char* (strmap[999999][20]);
char outputstr[1000];

/* Solves the LCS problem on num strings str of lengths len */
int LCS(char** str, int num) {
    /* Counting variables */
    int i, j;

    if (depth + getshared(str, num) <= maximum) {
        return 0;
    }

    char* replace[num];
    char match;
    char best_match = 0;
    int earliest_set = 0;
    int earliest[num];
    int all_late;
    int all_early;
    int future;
    int best_future = 0;
    int need_future = 1;

    for (j = 0; j < mapsize; j++) {
        for (i = 0; i < num; i++)
            if (str[i] != strmap[j][i])
                break;
        if (i == num) {
            best_match = lenmap[j][0];
            best_future = lenmap[j][1];
            need_future = 0;
            if (best_future + depth < maximum || !best_match)
                goto J;
            else {
                match = best_match;
                goto K;
            }
        }
    }

    for (match = MIN_CODEPOINT; need_future && match <= MAX_CODEPOINT; match++) {

    K:
        future = 1;
        all_late = earliest_set;
        all_early = 1;
        for (i = 0; i < num; replace[i++]++) {
            replace[i] = strchr(str[i], match);
            if (!replace[i]) {
                future = 0;
                break;
            }

            if (all_early && earliest_set && replace[i] - str[i] > earliest[i])
                all_early = 0;
            if (all_late && replace[i] - str[i] < earliest[i])
                all_late = 0;
        }
        if (all_late) {
            future = 0;
        }

    I:
        if (future) {
            if (all_early || !earliest_set) {
                for (i = 0; i < num; i++) {
                    earliest[i] = (int)(replace[i] - str[i]);
                }
            }

            /* The recursive bit */
            depth++;
            future += LCS(replace, num);
            depth--;

            best_future = future > best_future ? (best_match = match), future : best_future;
        }
    }

    if (need_future) {
        for (i = 0; i < num; i++)
            strmap[mapsize][i] = str[i];
        lenmap[mapsize][0] = best_match;
        lenmap[mapsize++][1] = best_future;
    }

J:
    if (depth + best_future >= maximum) {
        maximum = depth + best_future;
        outputstr[depth] = best_match;
    }

    if (!depth) {
        mapsize = 0;
        maximum = 0;
        puts(outputstr);
    }

    return best_future;
}

/* Return the number of characters total (not necessarily in order) that the strings all share */
int getshared(char** str, int num) {
    int c, i, tot = 0, min;
    for (c = MIN_CODEPOINT; c <= MAX_CODEPOINT; c++) {
        min = strcount(str[0], c);
        for (i = 1; i < num; i++) {
            int count = strcount(str[i], c);
            if (count < min) {
                min = count;
            }
        }
        tot += min;
    }

    return tot;
}

/* Count the number of occurrences of c in str */
int strcount(char* str, char c) {
    int tot = 0;
    while ((str = strchr(str, c))) {
        str++, tot++;
    }
    return tot;
}

Відповідна функція, яка виконує всі обчислення LCS, є функцією LCS. Код, наведений вище, спричинить власний виклик цієї функції.

Збережіть як main.cі компілюйте з:gcc -Ofast main.c -o FLCS

Код може бути запущений або аргументами командного рядка, або через stdin. Використовуючи stdin, він очікує кількість рядків, за якими слідують самі рядки.

~ Me$ ./FLCS "12345" "23456"
2345
Size: 4
Elapsed time on LCS call: 0.000056 s

Або:

~ Me$ ./FLCS
6 
2341582491248123139182371298371239813
2348273123412983476192387461293472793
1234123948719873491234120348723412349
1234129388234888234812834881423412373
1111111112341234128469128377293477377
1234691237419274737912387476371777273
1241231212323
Size: 13
Elapsed time on LCS call: 0.001594 s

На коробці Mac OS X з 1,7 ГГц Intel Core i7 та тестовому корпусі Денніса, ми отримуємо наступний вихід для двох рядків:

16638018800200>3??32322701784=4240;24331395?<;=99=?;178675;866002==23?87?>978891>=9<66=381992>>7030829?25265804:=3>:;60<9384=081;08?66=51?0;509072488>2>924>>>3175?::<9199;330:494<51:>748571?153994<45<??20>=3991=<962508?7<2382?489
Size: 386
Elapsed time on LCS call: 33.245087 s

Підхід дуже схожий на мій підхід до попереднього виклику, тут . Окрім попередньої оптимізації, ми перевіряємо, чи існує загальна кількість спільних символів між рядками під час кожного рекурсії та рано виходити, чи немає способу отримати більш довгу підрядку, ніж вже існує.

Поки що він обробляє 2 рядки добре, але має тенденцію отримувати кращі результати на більше. Більше вдосконалень та кращого пояснення!


1
Я думаю, що я щось пропустив. З двома рядками це не класична проблема динамічного програмування, для вирішення якої потрібно близько 1000 ^ 2 кроків? Іншими словами, частку секунди.

@Lembik Так, так і повинно. Цей метод був розроблений для обробки багатьох більш ніж 2 рядків, але в кінцевому підсумку масштабування було занадто поганим з довжиною рядка, щоб мати хороші результати. У мене ще багато хитрощів у рукаві, і якщо будь-який з них насправді спрацює ... Речі мають надзвичайно покращитися.
BrainSteel

Здається, десь є проблема. @ Код RetoKoradi знаходить дійсну загальну підрядку довжиною 391 для n = 2, тоді як ваш код повідомляє про довжину 386 і друкує рядок довжиною 229.
Денніс,

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