最速の最長共通サブシーケンスファインダー


11

あなたの仕事は解決することで最長共通部分列問題をするためにn個の長さが1000の文字列を。

二つ以上の文字列のLCSの問題に対する有効な解S 1、... S nは任意の文字列であるTの文字というように最大長のTが全てに現れるS Iと同じ順序で、T

TS iの部分文字列である必要はないことに注意してください。

この問題は、最短のコードですでに解決しています。今回は、サイズは関係ありません。

文字列axbyczとにxaybzcは、長さ3の8つの共通サブシーケンスがあり ます。

abc abz ayc ayz xbc xbz xyc xyz

これらはいずれも、LCS問題の有効な解決策です。

詳細

上記で説明したように、次の規則に従って、LCSの問題を解決する完全なプログラムを作成します。

  • 入力は、コードポイントが0x30〜0x3FのASCII文字で構成される、長さ1000の2つ以上の文字列で構成されます。

  • STDINから入力を読み取る必要があります。

    入力形式には2つの選択肢があります。

    • 各文字列(最後を含む)の後に改行が続きます。

    • 文字列は、セパレータおよび末尾の改行なしで連結されます。

  • 文字列の数は、コマンドラインパラメータとしてプログラムに渡されます。

  • 出力、つまりLCSに対する有効なソリューションのいずれかをSTDOUTに書き込み、その後に1つの改行を書き込む必要があります。

  • 選択する言語には、オペレーティングシステム(Fedora 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上記のコードにありますが、出力を(部分的に)ハードコーディングしている疑いがある場合は、非公開の値に変更する権利を留保します。


例外をスローできますか?
ハイパーニュートリノ

@JamesSmith出力が正しい限り、必ず。
デニス

bufferedreaderで読んでいるので、ioexceptionをスローできpublic static void main(...)ますか?
ハイパーニュートリノ

@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つの文字列の場合ABC私が使用します。

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.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

はっきりしない場合はおApびしますが、長さだけでなくLCSを印刷する必要があります。
デニス

@デニス、なるほど。私の最適化のいくつかはその時無駄でした。文字列を再構築できるように、完全な行列を格納するバージョンに戻る必要があります。これは、n = 4では実行できませんが、n = 3では10秒未満で終了するはずです。私はまだ完全なマトリックスを持っていたとき、私はおよそ6-7秒であったと思います。
レトコラディ

繰り返しますが、ごめんなさい。これについての質問はあまり明確ではありませんでした...出力を投稿すると、BrainSteelの出力と比較することができます。プログラムが報告する長さは、n = 2の場合、出力の長さを5超えます。ところで、N_MAXマクロとして定義し、-std=c99GCCでコードをコンパイルするにはコンパイラフラグを追加する必要がありました。
デニス

@Dennis問題ありません。解決策は「文字列」であると述べたので、それは十分に明確でなければなりませんでした。私はほとんどC ++のみを使用しているため、Cで何が許可されているかわかりません。このコードはC ++として始まりましたが、C ++の機能を実際に使用していないことに気付いたら、MacでC.clangに切り替えました。それに満足していましたが、おそらくデフォルトで異なるCバージョンを使用するか、より寛大です。
レトコラディ

1
@Dennis OK、文字列を生成できるようにトレースバックロジックを追加しました。n = 3の場合、約7秒かかります。
レトコラディ

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を搭載した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ステップかかる古典的な動的プログラミング問題ではありませんか?言い換えれば、ほんの一瞬です。

@Lembikええ、そうすべきです。このメソッドは、3つ以上の文字列を処理するために構築されましたが、文字列の長さが適切な結果を得るにはスケーリングが不十分でした。私はスリーブにもっと多くのトリックを持っています、そしてそれらのどれかが実際に働くならば...物事は非常に改善するはずです。
BrainSteel

どこかに問題があるようです。あなたのコードは、386の長さを報告し、長さ229の文字列を出力しながら、RetoKoradiのコードは、長さ391のためのn = 2の有効な共通部分を見つけた@
デニス

@Dennis Umm ...うん、そうだね...ああ。まあ、これは恥ずかしいです。私はそれに取り組んでいます:)答えを編集してバグを反映します。
BrainSteel
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.