Самый быстрый самый длинный общий искатель подпоследовательности

11

Ваша задача - решить задачу Longest Common Subsequence для 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 Пока вывод правильный, конечно.
Деннис
Так как я читаю с помощью Bufferedreader, я могу выкинуть ioexception from public static void main(...)?
HyperNeutrino
@JamesSmith Я на самом деле не знаю Java, поэтому не знаю, что это такое, но не волнуюсь об исключениях.
Деннис
4
@JamesSmith Поскольку длина кода не имеет значения для этой задачи, вы не можете просто поймать исключения?
Рето Коради

Ответы:

5

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

Алгоритм

Алгоритм является прямым обобщением стандартного решения динамического программирования для nпоследовательностей. Для 2 строк 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длиной строк. Исходя из того, что я обнаружил, похоже, проблема в NP-полноте. Таким образом, хотя опубликованный алгоритм очень неэффективен для больших значений 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.
Деннис
@ Денис Нет проблем. Он сказал, что решение «это строка», так что это должно быть достаточно ясно. Я почти исключительно использую C ++, поэтому я никогда не уверен, что разрешено в C. Этот код начинался как C ++, но как только я понял, что на самом деле не использую какие-либо функции C ++, я переключил его на C. clang на своем Mac был доволен этим, но он, вероятно, использует другую версию C по умолчанию или просто более снисходительно.
Рето Коради
1
@Dennis Хорошо, я добавил логику трассировки, чтобы я мог создать строку. Теперь занимает около 7 секунд для n = 3.
Рето Коради
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 с Intel Core i7 1,7 ГГц и тестовым набором, который предоставил Деннис, мы получаем следующий вывод для 2 строк:

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 строки в порядке, но имеет тенденцию к краху при большем. Больше улучшений и лучшее объяснение!

BrainSteel
источник
1
Я думаю, что я что-то пропустил. С 2 строками - это не классическая проблема динамического программирования, для решения которой требуется около 1000 ^ 2 шагов? Другими словами, доли секунды.
@Lembik Да, так и должно быть. Этот метод был создан для обработки более двух строк, но в итоге он слишком плохо масштабировался с длиной строки, чтобы иметь хорошие результаты. У меня в рукаве еще много хитростей, и если какие-то из них действительно сработают ... Все должно стать намного лучше.
BrainSteel
Кажется, где-то есть проблема. Код @ RetoKoradi находит допустимую общую подстроку длиной 391 для n = 2, в то время как ваш код сообщает о длине 386 и печатает строку длиной 229.
Деннис
@ Денис Умм ... Да, да, да ... О, дорогой. Ну, это неудобно. Я работаю над этим :) Я отредактирую ответ, чтобы отразить ошибку.
BrainSteel