Tối ưu hóa bộ nhớ bị hạn chế


9

Khoảng cách chỉnh sửa (hoặc Levenshtein) giữa hai chuỗi là số lần chèn, xóa và thay thế ký tự đơn tối thiểu cần thiết để chuyển đổi một chuỗi thành chuỗi khác. Nếu hai chuỗi có độ dài n mỗi chuỗi, thì điều này có thể được thực hiện trong thời gian O (n ^ 2) bằng lập trình động. Mã Python sau đây thực hiện phép tính này cho hai chuỗi s1s2.

def edit_distance(s1, s2):
    l1 = len(s1)
    l2 = len(s2)

    matrix = [range(l1 + 1)] * (l2 + 1)
    for zz in range(l2 + 1):
      matrix[zz] = range(zz,zz + l1 + 1)
    for zz in range(0,l2):
      for sz in range(0,l1):
        if s1[sz] == s2[zz]:
          matrix[zz+1][sz+1] = min(matrix[zz+1][sz] + 1, matrix[zz][sz+1] + 1, matrix[zz][sz])
        else:
          matrix[zz+1][sz+1] = min(matrix[zz+1][sz] + 1, matrix[zz][sz+1] + 1, matrix[zz][sz] + 1)
    return matrix[l2][l1]

Trong tác vụ này, bạn phải tiến gần đến mức có thể tính toán khoảng cách chỉnh sửa nhưng với sự hạn chế bộ nhớ nghiêm trọng. Mã của bạn được phép xác định một mảng chứa 1000 số nguyên 32 bit và đây là lưu trữ tạm thời duy nhất bạn sử dụng trong tính toán của mình. Tất cả các biến và cấu trúc dữ liệu sẽ được chứa trong mảng này. Cụ thể, bạn sẽ không thể thực hiện thuật toán ở trên như đối với các chuỗi có độ dài 1000 vì nó sẽ yêu cầu bạn lưu trữ ít nhất 1.000.000 số. Trường hợp ngôn ngữ của bạn không tự nhiên có số nguyên 32 bit (ví dụ Python), bạn chỉ cần đảm bảo rằng bạn không bao giờ lưu trữ một số lớn hơn 2 ^ 32-1 trong mảng.

Bạn có thể đọc dữ liệu bằng bất kỳ thư viện tiêu chuẩn nào bạn chọn mà không phải lo lắng về các hạn chế bộ nhớ trong phần đó. Để làm cho cạnh tranh công bằng cho phần chính của mã của bạn, bạn chỉ có thể sử dụng các hoạt động tương đương về chức năng với các hoạt động trong ngôn ngữ lập trình C và không thể sử dụng bất kỳ thư viện bên ngoài nào.

Để rõ ràng hơn, bộ nhớ để lưu trữ dữ liệu đầu vào hoặc được sử dụng bởi trình thông dịch ngôn ngữ của bạn, JVM, v.v. không được tính vào giới hạn của bạn và bạn không được ghi bất cứ điều gì vào đĩa. Bạn phải giả sử dữ liệu đầu vào là chỉ đọc khi trong bộ nhớ để bạn không thể sử dụng lại dữ liệu đó để có thêm không gian làm việc.

Tôi phải làm gì?

Mã của bạn nên đọc trong một tệp theo định dạng sau. Nó sẽ có ba dòng. Dòng đầu tiên là khoảng cách chỉnh sửa thực sự. Thứ hai là chuỗi 1 và thứ ba là chuỗi 2. Tôi sẽ kiểm tra nó với dữ liệu mẫu tại https://bpaste.net/show/6905001d52e8 trong đó các chuỗi có độ dài 10.000 nhưng không nên chuyên dụng cho dữ liệu này. Nó sẽ xuất ra khoảng cách chỉnh sửa nhỏ nhất mà nó có thể tìm thấy giữa hai chuỗi.

Bạn cũng sẽ cần phải chứng minh khoảng cách chỉnh sửa của mình thực sự đến từ một tập hợp chỉnh sửa hợp lệ. Mã của bạn nên có một công tắc biến nó thành chế độ có thể sử dụng nhiều bộ nhớ hơn (bao nhiêu tùy thích) và đưa ra các thao tác chỉnh sửa cung cấp khoảng cách chỉnh sửa của bạn.

Ghi bàn

Điểm của bạn sẽ là (optimal edit distance/divided by the edit distance you find) * 100. Để bắt đầu mọi thứ, hãy lưu ý rằng bạn có thể nhận được điểm bằng cách chỉ đếm số lượng không khớp giữa hai chuỗi.

Bạn có thể sử dụng bất kỳ ngôn ngữ nào bạn thích có sẵn miễn phí và dễ cài đặt trong Linux.

Cà vạt

Trong trường hợp hòa vốn, tôi sẽ chạy mã của bạn trên máy Linux của tôi và mã nhanh nhất sẽ thắng.


Sẽ for(int i=0;i<=5;i++)được phép vì nó lưu trữ dữ liệu trong i?
Beta Decay

2
@BetaDecay Có mặc dù để tuân thủ các quy tắc chặt chẽ hơn, bạn sẽ làm một cái gì đó như thế { uint32_t foo[1000]; for (foo[0] = 0; foo[0] < 5; ++foo[0]) printf("%d ", foo[0]); } này Giả sử mảng số nguyên 32 bit của bạn sẽ được gọi foo.

Điểm có khoảng cách chỉnh sửa thực sự trong tập tin là gì? Là chương trình thực sự cần phải đọc nó? Hoặc (điều có vẻ hợp lý hơn) chỉ là ở đó để bạn thấy chương trình thành công như thế nào?
frageum

@feersum Chính xác. Nó chỉ ở đó để bạn có thể thấy điểm của bạn dễ dàng.

bpaste.net/show/6905001d52e8 cho tôi một trang 404!
sergiol

Câu trả lời:


4

C ++, Điểm 92,35

Thuật toán ước tính: Thuật toán tìm vị trí đầu tiên của hai chuỗi khác nhau và sau đó thử tất cả các hoán vị hoạt động N có thể (chèn, xóa, thay thế - các ký tự trùng khớp được bỏ qua mà không tốn một thao tác). Nó chấm điểm từng bộ hoạt động có thể dựa trên mức độ xa hơn của bộ hoạt động đó khớp với hai chuỗi, cộng với mức độ khiến cho độ dài chuỗi hội tụ. Sau khi xác định tập hợp N hoạt động có điểm cao nhất, thao tác đầu tiên trong tập hợp được áp dụng, sự không khớp tiếp theo được tìm thấy và quá trình lặp lại cho đến khi kết thúc chuỗi.

Chương trình thử tất cả các giá trị của N từ 1-10 và chọn mức cho kết quả tốt nhất. N = 10 nói chung là tốt nhất hiện nay khi phương pháp tính điểm đưa chiều dài chuỗi vào xem xét. Giá trị cao hơn của N có thể sẽ còn tốt hơn, nhưng mất nhiều thời gian hơn theo cấp số nhân.

Sử dụng bộ nhớ: Vì chương trình hoàn toàn lặp lại, nó cần rất ít bộ nhớ. Chỉ có 19 biến được sử dụng để theo dõi trạng thái chương trình. Chúng được đặt bởi #defines để hoạt động như các biến toàn cục.

Cách sử dụng: Chương trình được sử dụng giống như của frageum: tham số đầu tiên được coi là tệp và bất kỳ tham số bổ sung nào cho biết rằng các chỉnh sửa sẽ được hiển thị. Chương trình luôn in khoảng cách chỉnh sửa ước tính và điểm số.

Đầu ra xác minh: Đầu ra xác minh được định dạng thành ba hàng:

11011111100101100111100110100 110 0 0000   0 01101
R I          IR     R        D   D D    DDD D     D
01 1111110010 0001110001101000110101000011101011010

Hàng trên cùng là chuỗi mục tiêu, giữa là các hoạt động và dưới cùng là chuỗi đang được chỉnh sửa. Dấu cách trong dòng thao tác chỉ ra rằng các ký tự khớp với nhau. 'R' chỉ ra rằng chuỗi chỉnh sửa có ký tự ở vị trí đó được thay thế bằng ký tự của chuỗi đích. 'Tôi' chỉ ra rằng chuỗi chỉnh sửa có ký tự của chuỗi đích được chèn tại vị trí đó. 'D' chỉ ra rằng chuỗi chỉnh sửa có ký tự ở vị trí đó bị xóa. Các chuỗi chỉnh sửa và đích có các khoảng trắng được chèn khi cái kia có một ký tự được chèn hoặc xóa để chúng xếp hàng.

#include <stdio.h>
#include <stdlib.h>
#include <string>
#include <math.h>
#include <fstream>

int memory[1000];
#define first (*(const char **)&memory[0])
#define second (*(const char **)&memory[1])
#define block_ia memory[2]
#define block_ib memory[3]
#define block_n memory[4]
#define block_op memory[5]
#define block_o memory[6]
#define block_x memory[7]
#define n memory[8]
#define opmax memory[9]
#define best_op memory[10]
#define best_score memory[11]
#define score memory[12]
#define best_counter memory[13]
#define la memory[14]
#define lb memory[15]
#define best memory[16]
#define bestn memory[17]
#define total memory[18]

// verification variables
char printline1[0xffff]={};
char *p1=printline1;
char printline2[0xffff]={};
char *p2=printline2;
char printline3[0xffff]={};
char *p3=printline3;


// determine how many characters match after a set of operations
int block(){
    block_ia=0;
    block_ib=0;
    for ( block_x=0;block_x<block_n;block_x++){
        block_o = block_op%3;
        block_op /= 3;
        if ( block_o == 0 ){ // replace
            block_ia++;
            block_ib++;
        } else if ( block_o == 1 ){ // delete
            block_ib++;
        } else { // insert
            if ( first[block_ia] ){ 
                block_ia++;
            }
        }
        while ( first[block_ia] && first[block_ia]==second[block_ib] ){ // find next mismatch
            block_ia++;
            block_ib++;
        }
        if ( first[block_ia]==0 ){
            return block_x;
        }
    }
    return block_n;
}

// find the highest-scoring set of N operations for the current string position
void bestblock(){
    best_op=0;
    best_score=0;
    la = strlen(first);
    lb = strlen(second);
    block_n = n;
    for(best_counter=0;best_counter<opmax;best_counter++){
        block_op=best_counter;
        score = n-block();
        score += block_ia-abs((la-block_ia)-(lb-block_ib));
        if ( score > best_score ){
            best_score = score;
            best_op = best_counter;
        }
    }
}

// prepare edit confirmation record
void printedit(const char * a, const char * b, int o){
    o%=3;
    if ( o == 0 ){ // replace
        *p1 = *a;
        if ( *b ){
            *p2 = 'R';
            *p3 = *b;
            b++;
        } else {
            *p2 = 'I';
            *p3 = ' ';
        }
        a++;
    } else if ( o == 1 ){ // delete
        *p1 = ' ';
        *p2 = 'D';
        *p3 = *b;
        b++;
    } else { // insert
        *p1 = *a;
        *p2 = 'I';
        *p3 = ' ';
        a++;
    }
    p1++;
    p2++;
    p3++;
    while ( *a && *a==*b ){
        *p1 = *a;
        *p2 = ' ';
        *p3 = *b;
        p1++;
        p2++;
        p3++;
        a++;
        b++;
    }
}


int main(int argc, char * argv[]){

    if ( argc < 2 ){
        printf("No file name specified\n");
        return 0;
    }

    std::ifstream file(argv[1]);
    std::string line0,line1,line2;
    std::getline(file,line0);
    std::getline(file,line1);
    std::getline(file,line2);

    // begin estimating Levenshtein distance
    best = 0;
    bestn = 0;
    for ( n=1;n<=10;n++){ // n is the number of operations that can be in a test set
        opmax = (int)pow(3.0,n);
        first = line1.c_str();
        second = line2.c_str();
        while ( *first && *first == *second ){
            first++;
            second++;
        }
        total=0;
        while ( *first && *second ){
            bestblock();
            block_n=1;
            block_op=best_op;
            block();
            total ++;
            first += block_ia;
            second += block_ib;
        }
        // when one string is exhausted, all following ops must be insert or delete
        while(*second){
            total++;
            second++;
        }
        while(*first){
            total++;
            first++;
        }
        if ( !best || total < best ){
            best = total;
            bestn = n;
        }
    }
    // done estimating Levenshtein distance

    // dump info to prove the edit distance actually comes from a valid set of edits
    if ( argc >= 3 ){
        p1 = printline1;
        p2 = printline2;
        p3 = printline3;
        n = bestn;
        opmax = (int)pow(3.0,n);
        first = line1.c_str();
        second = line2.c_str();
        while ( *first && *first == *second ){
            *p1 = *first;
            *p2 = ' ';
            *p3 = *second;
            p1++;
            p2++;
            p3++;
            first++;
            second++;
        }
        while ( *first && *second){
            bestblock();
            block_n=1;
            block_op=best_op;
            block();
            printedit(first,second,best_op);
            first += block_ia;
            second += block_ib;
        }
        while(*second){
            *p1=' ';
            *p2='D';
            *p3=*second;
            p1++;
            p2++;
            p3++;
            second++;
        }
        while(*first){
            *p1=*first;
            *p2='I';
            *p3=' ';
            p1++;
            p2++;
            p3++;
            first++;
        }

        p1 = printline1;
        p2 = printline2;
        p3 = printline3;
        int ins=0;
        int del=0;
        int rep=0;
        while ( *p1 ){
            int a;
            for ( a=0;a<79&&p1[a];a++)
                printf("%c",p1[a]);
            printf("\n");
            p1+=a;
            for ( a=0;a<79&&p2[a];a++){
                ins += ( p2[a] == 'I' );
                del += ( p2[a] == 'D' );
                rep += ( p2[a] == 'R' );
                printf("%c",p2[a]);
            }
            printf("\n");
            p2+=a;
            for ( a=0;a<79&&p3[a];a++)
                printf("%c",p3[a]);
            printf("\n\n");
            p3+=a;
        }
        printf("Best N=%d\n",bestn);
        printf("Inserted = %d, Deleted = %d, Replaced=%d, Total = %d\nLength(line1)=%d, Length(Line2)+ins-del=%d\n",ins,del,rep,ins+del+rep,line1.length(),line2.length()+ins-del);
    }

    printf("%d, Score = %0.2f\n",best,2886*100.0/best);
    system("pause");
    return 0;
}

7

C ++ 75.0

Chương trình được thiết kế để làm việc với các chuỗi văn bản tùy ý. Chúng có thể có độ dài khác nhau miễn là không vượt quá 13824 ký tự. Nó sử dụng 1.897 số nguyên 16 bit, tương đương với 949 số nguyên 32 bit. Lúc đầu tôi đang viết nó bằng C, nhưng sau đó nhận ra không có chức năng nào để đọc một dòng.

Đối số dòng lệnh đầu tiên phải là tên tệp. Nếu một đối số thứ hai tồn tại, một bản tóm tắt các chỉnh sửa được in. Dòng đầu tiên trong tệp bị bỏ qua trong khi dòng thứ hai và thứ ba là các chuỗi.

Thuật toán là một phiên bản bị chặn gấp đôi của thuật toán thông thường. Về cơ bản, nó thực hiện cùng một số lượng hoạt động, nhưng tất nhiên là kém chính xác hơn nhiều, vì nếu một chuỗi con chung bị tách ra khỏi rìa của một khối, phần lớn các khoản tiết kiệm tiềm năng sẽ bị mất.

#include <cstring>
#include <inttypes.h>
#include <iostream>
#include <fstream>

#define M 24
#define MAXLEN (M*M*M)
#define SETMIN(V, X) if( (X) < (V) ) { (V) = (X); }
#define MIN(X, Y) ( (X) < (Y) ? (X) : (Y) )

char A[MAXLEN+1], B[MAXLEN+1];
uint16_t d0[M+1][M+1], d1[M+1][M+1], d2[M+1][M+1];

int main(int argc, char**argv)
{

    if(argc < 2)
        return 1;

    std::ifstream fi(argv[1]);

    std::string Astr, Bstr;
    for(int i = 3; i--;)
        getline(fi, i?Bstr:Astr);
    if(!fi.good()) {
        printf("Error reading file");
        return 5;
    }
    if(Astr.length() > MAXLEN || Bstr.length() > MAXLEN) {
        printf("String too long");
        return 7;
    }

    strcpy(A, Astr.c_str());
    strcpy(B, Bstr.c_str());

    uint16_t lA = Astr.length(), lB = Bstr.length();
    if(!lA || !lB) {
        printf("%d\n", lA|lB);
        return 0;
    }
    uint16_t nbA2, nbB2, bA2, bB2, nbA1, nbB1, bA1, bB1, nbA0, nbB0, bA0, bB0; //block, number of blocks
    uint16_t iA2, iB2, iA1, iB1, jA2, jB2, jA1, jB1; //start, end indices of block

    nbA2 = MIN(M, lA);
    nbB2 = MIN(M, lB);
    for(bA2 = 0; bA2 <= nbA2; bA2++) {
        iA2 = lA * (bA2-1)/nbA2,  jA2 = lA * bA2/nbA2;
        for(bB2 = 0; bB2 <= nbB2; bB2++) {
            if(!(bA2|bB2)) {
                d2[0][0] = 0;
                continue;
            }
            iB2 = lB * (bB2-1)/nbB2,  jB2 = lB * bB2/nbB2;
            d2[bA2][bB2] = ~0;
            if(bB2)
                SETMIN(d2[bA2][bB2], d2[bA2][bB2-1] + (jB2-iB2));
            if(bA2)
                SETMIN(d2[bA2][bB2], d2[bA2-1][bB2] + (jA2-iA2));

            if(bA2 && bB2) {
                nbA1 = MIN(M, jA2-iA2);
                nbB1 = MIN(M, jB2-iB2);
                for(bA1 = 0; bA1 <= nbA1; bA1++) {
                    iA1 = iA2 + (jA2-iA2) * (bA1-1)/nbA1, jA1 = iA2 + (jA2-iA2) * bA1/nbA1;
                    for(bB1 = 0; bB1 <= nbB1; bB1++) {
                        if(!(bA1|bB1)) {
                            d1[0][0] = 0;
                            continue;
                        }
                        iB1 = iB2 + (jB2-iB2) * (bB1-1)/nbB1, jB1 = iB2 + (jB2-iB2) * bB1/nbB1;
                        d1[bA1][bB1] = ~0;
                        if(bB1)
                            SETMIN(d1[bA1][bB1], d1[bA1][bB1-1] + (jB1-iB1));
                        if(bA1)
                            SETMIN(d1[bA1][bB1], d1[bA1-1][bB1] + (jA1-iA1));

                        if(bA1 && bB1) {
                            nbA0 = jA1-iA1;
                            nbB0 = jB1-iB1;
                            for(bA0 = 0; bA0 <= nbA0; bA0++) {
                                for(bB0 = 0; bB0 <= nbB0; bB0++) {
                                    if(!(bA0|bB0)) {
                                        d0[0][0] = 0;
                                        continue;
                                    }
                                    d0[bA0][bB0] = ~0;
                                    if(bB0)
                                        SETMIN(d0[bA0][bB0], d0[bA0][bB0-1] + 1);
                                    if(bA0)
                                        SETMIN(d0[bA0][bB0], d0[bA0-1][bB0] + 1);
                                    if(bA0 && bB0)
                                        SETMIN(d0[bA0][bB0], d0[bA0-1][bB0-1] + (A[iA1 + nbA0 - 1] != B[iB1 + nbB0 - 1]));
                                }
                            }
                            SETMIN(d1[bA1][bB1], d1[bA1-1][bB1-1] + d0[nbA0][nbB0]);
                        }
                    }
                }

                SETMIN(d2[bA2][bB2], d2[bA2-1][bB2-1] + d1[nbA1][nbB1]);
            }
        }
    }
    printf("%d\n", d2[nbA2][nbB2]);

    if(argc == 2)
        return 0;

    int changecost, total = 0;
    for(bA2 = nbA2, bB2 = nbB2; bA2||bB2; ) {
        iA2 = lA * (bA2-1)/nbA2,  jA2 = lA * bA2/nbA2;
        iB2 = lB * (bB2-1)/nbB2,  jB2 = lB * bB2/nbB2;
        if(bB2 && d2[bA2][bB2-1] + (jB2-iB2) == d2[bA2][bB2]) {
            total += changecost = (jB2-iB2);
            char tmp = B[jB2];
            B[jB2] = 0;
            printf("%d %d deleted {%s}\n", changecost, total, B + iB2);
            B[jB2] = tmp;
            --bB2;
        } else if(bA2 && d2[bA2-1][bB2] + (jA2-iA2) == d2[bA2][bB2]) {
            total += changecost = (jA2-iA2);
            char tmp = B[jA2];
            A[jA2] = 0;
            printf("%d %d inserted {%s}\n", changecost, total, A + iA2);
            A[jA2] = tmp;
            --bA2;
        } else {
            total += changecost = d2[bA2][bB2] - d2[bA2-1][bB2-1];
            char tmpa = A[jA2], tmpb = B[jB2];
            B[jB2] = A[jA2] = 0;
            printf("%d %d changed {%s} to {%s}\n", changecost, total, B + iB2, A + iA2);
            A[jA2] = tmpa, B[jB2] = tmpb;
            --bA2, --bB2;
        }
    }


    return 0;
}

Cảm ơn bạn đã là người trả lời đầu tiên! Điểm của bạn là bao nhiêu?

@Lembik OK, tôi đã tính điểm, giả sử nó chỉ dựa trên một ví dụ.
frageum

Điều đó thật tuyệt. Bạn có nghĩ rằng nó có thể đạt được điểm cao hơn nhiều không?

3

Con trăn, 100

Tôi đã quản lý để tính toán khoảng cách chỉnh sửa một cách hoàn hảo trong giới hạn bộ nhớ được phân bổ. Đáng buồn thay, mục này vi phạm hai quy tắc của thách thức, trong thư nếu không phải trong tinh thần.

Đầu tiên, tôi chưa thực sự lưu trữ dữ liệu của mình trong 1000 ints 32 bit. Đối với chuỗi 10000 ký tự, chương trình của tôi tạo hai mảng 10000 phần tử sẽ chỉ chứa +1, 0 hoặc -1. Với 1,585 bit cho mỗi số thứ ba, có thể đóng gói 20000 trits đó thành 31700 bit, để lại 300 bit là quá đủ cho 7 số nguyên 16 bit còn lại của tôi.

Thứ hai, tôi chưa thực hiện chế độ cần thiết để hiển thị các chỉnh sửa. Thay vào đó, tôi đã thực hiện một chế độ in ra ma trận chỉnh sửa đầy đủ. Hoàn toàn có thể tính toán đường dẫn chỉnh sửa từ ma trận đó, nhưng tôi không có thời gian ngay bây giờ để thực hiện nó.

#!/usr/bin/env python

import sys

# algorithm originally from
# https://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows

print_rows = False
if len(sys.argv) > 2:
    print_rows = True

def LevenshteinDistance(s, t):
    # degenerate cases
    if s == t:
        return 0
    if len(s) == 0:
        return len(t)
    if len(t) == 0:
        return len(s)

    # create two work vectors of integer distance deltas

    # these lists will only ever contain +1, 0, or -1
    # so they COULD be packed into 1.585 bits each
    # 15850 bits per list, 31700 bits total, leaving 300 bits for all the other variables

    # d0 is the previous row
    # initialized to 0111111... which represents 0123456...
    d0 = [1 for i in range(len(t)+1)]
    d0[0] = 0        
    if print_rows:
        row = ""
        for i in range(len(t)+1):
            row += str(i) + ", "
        print row

    # d1 is the row being calculated
    d1 = [0 for i in range(len(t)+1)]

    for i in range(len(s)-1):
        # cummulative values of cells north, west, and northwest of the current cell
        left = i+1
        upleft = i
        up = i+d0[0]
        if print_rows:
            row = str(left) + ", "
        for j in range(len(t)):
            left += d1[j]
            up += d0[j+1]
            upleft += d0[j]
            cost = 0 if (s[i] == t[j]) else 1
            d1[j + 1] = min(left + 1, up + 1, upleft + cost) - left
            if print_rows:
                row += str(left+d1[j+1]) + ", "

        if print_rows:
            print row

        for c in range(len(d0)):
            d0[c] = d1[c]

    return left+d1[j+1]

with open(sys.argv[1]) as f:
    lines = f.readlines()

perfect = lines[0]
string1 = lines[1]
string2 = lines[2]
distance = LevenshteinDistance(string1,string2)
print "edit distance: " + str(distance)
print "score: " + str(int(perfect)*100/distance) + "%"

ví dụ đầu vào:

2
101100
011010

ví dụ đầu ra verbose:

0, 1, 2, 3, 4, 5, 6,
1, 1, 1, 2, 3, 4, 5,
2, 1, 2, 2, 2, 3, 4,
3, 2, 1, 2, 3, 2, 3,
4, 3, 2, 1, 2, 3, 3,
5, 4, 3, 2, 1, 2, 3,
6, 5, 4, 3, 2, 2, 2,
edit distance: 2
score: 100%
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.