가장 빠른 가장 긴 공통 서브 시퀀스 파인더


11

너의 임무는 길이가 1000 인 n 개의 문자열에 대한 가장 긴 공통 하위 시퀀스 문제 를 해결하는 것 입니다.

두 개 이상의 문자열에 대한 LCS 문제에 대한 올바른 해결책은 S 1 , ... S n은 임의의 문자열입니다 T 최대 길이의 문자 있도록 T가 모두 표시 S I을 과 동일한 순서로, T .

참고 T는 하위 될 필요가 없습니다 문자열S .

우리는 이미 가장 짧은 코드 로이 문제를 해결했습니다 . 이번에는 크기가 중요하지 않습니다.

문자열 axbycz과는 xaybzc길이 3의 8 개 공통 서브 시퀀스를 가지고 :

abc abz ayc ayz xbc xbz xyc xyz

이 중 하나는 LCS 문제에 대한 올바른 솔루션입니다.

세부

위에서 설명한 LCS 문제를 해결하는 전체 프로그램을 작성하여 다음 규칙을 준수하십시오.

  • 입력은 길이가 1000 인 두 개 이상의 문자열로 구성되며 0x30과 0x3F 사이의 코드 포인트를 가진 ASCII 문자로 구성됩니다.

  • STDIN에서 입력을 읽어야합니다.

    입력 형식에 대한 두 가지 선택 사항이 있습니다.

    • 각 문자열 (마지막 포함) 뒤에 줄 바꿈이 이어집니다.

    • 스트링은 분리 자없이 후행 줄 바꿈없이 연결됩니다.

  • 문자열 수는 명령 행 매개 변수로 프로그램에 전달됩니다.

  • 출력, 즉 유효한 솔루션 중 하나를 LCS, STDOUT에 기록한 다음 한 줄 바꿈을 작성해야합니다.

  • 선택한 운영 체제에 내 운영 체제 (페도라 21)에 대한 무료 (맥주에서와 같이) 컴파일러 / 통역사가 있어야합니다.

  • 컴파일러 플래그 또는 특정 인터프리터가 필요한 경우 게시물에서 언급하십시오.

채점

유효한 솔루션을 인쇄하는 데 120 초 이상 걸릴 때까지 2, 3 등의 문자열로 코드를 실행합니다. 이는 n 값에 대해 120 초가 있음을 의미합니다. .

코드가 제 시간에 완료된 가장 많은 문자열은 점수입니다.

동점 인 n의 경우 가장 짧은 시간 에 n 개의 문자열에 대한 문제를 해결 한 제출이 승자로 선언됩니다.

모든 제출물은 컴퓨터에서 시간이 정해집니다 (Intel Core i7-3770, 16 GiB RAM, 스왑 없음).

(n-1) 번째 테스트 의 n 개 문자열은 다음과 같이 정의되어 호출 (및 요청 된 경우 줄 바꿈 제거)하여 생성됩니다 .rand nrand

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 출력이 정확하면 확실합니다.
Dennis

bufferedreader로 읽고 있기 때문에 ioexception을 public static void main(...)?
HyperNeutrino

@JamesSmith 나는 Java를 정말로 모른다. 그래서 그것이 무엇인지 모르지만 예외에 대해 걱정하지 않는다.
Dennis

4
@JamesSmith이 문제에는 코드 길이가 중요하지 않으므로 예외를 간단히 잡을 수 없습니까?
Reto Koradi

답변:


5

C, n = 3 ~ 7 초

연산

이 알고리즘은 표준 동적 프로그래밍 솔루션을 n시퀀스 로 직접 일반화 합니다. 두 문자열 AB의 경우 표준 반복은 다음과 같습니다.

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.

능률

내 코드의 복잡성은 s문자열 길이 와 함께 O (s ^ n) 입니다. 내가 찾은 것을 바탕으로 문제가 NP- 완전한 것처럼 보입니다. 따라서 게시 된 알고리즘은의 값이 클수록 비효율적 n이지만 실제로는 더 잘 수행 할 수 없습니다. 내가 본 유일한 것은 작은 알파벳의 효율성을 향상시키는 몇 가지 접근법입니다. 여기서 알파벳은 적당히 작기 때문에 (16) 개선 될 수 있습니다. 나는 여전히 아무도 n = 42 분 보다 더 높은 합법적 인 솔루션을 찾지 못할 것으로 예상한다.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.shtest3.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를 인쇄해야합니다.
Dennis

@Dennis 알겠습니다. 내 최적화 중 일부는 헛된 일이었습니다. 문자열을 재구성 할 수 있도록 전체 행렬을 저장하는 버전으로 돌아 가야합니다. n = 4에서는 실행할 수 없지만 n = 3에서는 10 초 미만으로 계속 완료됩니다. 나는 여전히 전체 행렬을 가졌을 때 약 6-7 초 였다고 생각합니다.
Reto Koradi

다시 한 번 죄송합니다. 질문은 이것에 대해 명확하지 않았습니다 ... 출력을 게시하면 BrainSteel과 비교할 수 있습니다. 프로그램이보고 한 길이가 n = 2 인 경우 출력 길이를 5만큼 초과합니다. 그건 그렇고, N_MAX매크로 로 정의 하고 컴파일러 플래그 -std=c99를 추가하여 GCC로 코드를 컴파일해야했습니다.
Dennis

@Dennis 문제 없습니다. 솔루션은 "문자열"이므로 충분히 명확해야합니다. 나는 거의 독점적으로 C ++을 사용하므로 C에서 무엇이 허용되는지 잘 모르겠습니다.이 코드는 C ++로 시작했지만 실제로 C ++ 기능을 사용하지 않는다는 것을 알게되면 Mac에서 C로 전환했습니다. 그것에 만족했지만 기본적으로 다른 C 버전을 사용하거나 더 관대합니다.
Reto Koradi

1
@Dennis Ok, 문자열을 생성 할 수 있도록 역 추적 로직을 추가했습니다. n = 3에 약 7 초가 걸립니다.
Reto Koradi

3

이 답변은 현재 버그로 인해 중단되었습니다. 곧 수정 중 ...

C, ~ 35 초 안에 2 줄

이것은 끔찍한 혼란에 의해 보여지는 것처럼 진행중인 작업이지만, 좋은 답변이 될 수 있기를 바랍니다.

코드:

#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

1.7Ghz Intel Core i7 및 테스트 케이스 Dennis가 설치된 Mac OS X 상자에서 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 개의 문자열을 처리하지만 더 많이 충돌하는 경향이 있습니다. 더 많은 개선과 올 더 나은 설명!


1
나는 뭔가를 놓친 것 같아요. 2 개의 문자열 로이 문제를 해결하는 데 약 1000 ^ 2 단계가 필요한 고전적인 동적 프로그래밍 문제가 아닙니까? 다시 말해, 1 초의 일부입니다.

@Lembik 그래, 그래. 이 방법은 두 개 이상의 문자열을 처리하기 위해 만들어졌지만 문자열 길이가 너무 작아서 좋은 결과를 얻지 못했습니다. 나는 소매에 더 많은 트릭을 가지고 있으며, 실제로 작동 한다면 ... 일이 크게 개선되어야합니다.
BrainSteel

어딘가에 문제가있는 것 같습니다. @RetoKoradi의 코드는 n = 2에 대해 길이가 391 인 유효한 공통 부분 문자열을 찾은 반면, 코드는 길이가 386이고 길이는 229 인 문자열을 인쇄합니다.
Dennis

@Dennis Umm ... 네, 그래요 .. 오 이런. 이건 좀 ... 부끄러워. 나는 그것을하고있다 :) 버그를 반영하기 위해 답변을 편집 할 것이다.
BrainSteel
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.