Обмежена оптимізація пам'яті


9

Відстань редагування (або Левенштейна) між двома рядками - це мінімальна кількість вставок, вилучень та підстановок з одним символом, необхідних для перетворення однієї рядки в іншу. Якщо два рядки мають довжину n кожна, добре відомо, що це можна зробити за O (n ^ 2) за допомогою динамічного програмування. Наступний код Python виконує цей обчислення для двох рядків s1і s2.

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]

У цьому завданні ви повинні максимально наблизитись до обчислення відстані редагування, але з суворим обмеженням пам'яті. Ваш код може визначати один масив, що містить 1000 32-бітових цілих чисел, і це буде єдиним тимчасовим сховищем, яке ви використовуєте під час обчислення. Усі змінні та структури даних повинні міститись у цьому масиві. Зокрема, ви б не змогли реалізувати алгоритм, наведений вище, для рядків довжиною 1000, оскільки це вимагатиме від вас зберігання принаймні 1 000 000 номерів. Якщо у вашій мові звичайно немає 32-бітових цілих чисел (наприклад, Python), вам просто потрібно переконатися, що ви ніколи не зберігаєте в масиві число, більше 2 ^ 32-1.

Ви можете читати дані, використовуючи будь-яку стандартну бібліотеку на ваш вибір, не турбуючись про обмеження пам'яті в цій частині. Для того, щоб зробити конкуренцію справедливою для основної частини вашого коду, ви можете використовувати лише ті операції, які функціонально еквівалентні операціям на мові програмування С і не можуть використовувати жодні зовнішні бібліотеки.

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

Що я маю реалізувати?

Ваш код повинен читатись у файлі у такому форматі. Він буде мати три лінії. Перший рядок - справжня відстань редагування. Другий - рядок 1, а третій - рядок 2. Я перевіряю його за допомогою зразків даних за адресою https://bpaste.net/show/6905001d52e8, де рядки мають довжину 10 000, але вони не повинні спеціалізуватися на цих даних. Він повинен вивести найменшу відстань редагування, яку він може знайти між двома рядками.

Вам також потрібно буде довести, що відстань редагування насправді відповідає дійсному набору змін. Ваш код повинен мати перемикач, який перетворює його в режим, який може використовувати більше пам'яті (скільки завгодно) і виводить операції редагування, які дають відстань для редагування.

Оцінка

Ваш рахунок буде найвищим (optimal edit distance/divided by the edit distance you find) * 100. Щоб почати все, зауважте, що ви можете отримати оцінку, просто порахувавши кількість невідповідностей між двома рядками.

Ви можете використовувати будь-яку мову, яка вам подобається, вільно доступну та просту в установці в Linux.

Перерва на чай

У разі тай-брейку я запускаю ваш код на моїй машині Linux і виграє найшвидший код.


Буде for(int i=0;i<=5;i++)дозволено, оскільки він зберігає дані в i?
Бета-розпад

2
@BetaDecay Так, хоча для більш чіткого дотримання правил ви б зробили щось на кшталт { uint32_t foo[1000]; for (foo[0] = 0; foo[0] < 5; ++foo[0]) printf("%d ", foo[0]); } Це передбачається, що ваш масив із 32-бітових цілих чисел буде викликаний foo.

Який сенс мати справжню відстань для редагування у файлі? Чи насправді програма повинна її читати? Або (що здається більш розумним) це саме ви там, щоб побачити, наскільки успішна програма була?
feersum

@feersum Рівно. Це просто там, щоб ви могли легко побачити, який ваш рахунок легко.

bpaste.net/show/6905001d52e8 дає мені 404 сторінки!
сергіол

Відповіді:


4

C ++, оцінка 92,35

Алгоритм оцінки: Алгоритм знаходить перше місце, коли два рядки відрізняються, а потім намагається виконати всі можливі N перестановки операцій (вставити, видалити, замінити - символи, які відповідають, пропускаються без використання операції). Він оцінює кожен можливий набір операцій, виходячи з того, наскільки далі цей набір операцій успішно збігається з двома рядками, плюс наскільки він змушує зближуватися довжини рядків. Після визначення набору найвищих балів з N операцій застосовується перша операція в наборі, знайдено наступне невідповідність, і процес повторюється до досягнення кінця рядка.

Програма випробовує всі значення N від 1-10 і вибирає рівень, який дав найкращі результати. N = 10, як правило, найкраще зараз, коли метод підрахунку враховує довжину рядка. Більш високі значення N, ймовірно, були б навіть кращими, але це займає експоненціально більше часу.

Використання пам'яті: Оскільки програма суто ітеративна, їй потрібно дуже мало пам'яті. Для відстеження стану програми використовується лише 19 змінних. Вони встановлюються #defines, щоб діяти як глобальні змінні.

Використання: програма використовується так само, як і feersum: перший параметр вважається файлом, а будь-які додаткові параметри вказують на те, що слід показувати правки. Програма завжди друкує орієнтовну відстань редагування та бал.

Вихід з верифікації: Вихід верифікації відформатований у три рядки:

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

Верхній рядок - цільовий рядок, середній - операції, а нижній - рядок, що редагується. Пробіли в рядку операції означають, що символи відповідають. 'R' означає, що рядок редагування має його символ у цій позиції, замінений символом цільової рядка. 'I' вказує на те, що в рядку редагування введено символ цільової рядка. 'D' вказує на те, що рядок редагування має символ у цій позиції видалений. У рядках редагування та цілі вставлені пробіли, коли інший має вставлений або видалений символ, щоб вони вирівнялися.

#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

Програма призначена для роботи з довільними текстовими рядками. Вони можуть бути різної довжини до тих пір, поки вони не перевищують 13824 символів. Він використовує 1897 16-бітних цілих чисел, що еквівалентно 949 32-бітовим цілим числам. Спочатку я писав це на C, але потім зрозумів, що немає функції для читання рядка.

Першим аргументом командного рядка має бути ім’я файлу. Якщо існує другий аргумент, друкується резюме змін. Перший рядок у файлі ігнорується, а другий та третій - це рядки.

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

#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;
}

Дякую за те, що ви були першим відповідачем! Яка ваша оцінка?

@Lembik Гаразд, я підрахував оцінку, припускаючи, що вона заснована лише на одному прикладі.
feersum

Це чудово. Як ви вважаєте, чи можна отримати набагато вищий бал?

3

Пітона, 100

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

По-перше, я фактично не зберігав свої дані в 1000 32-бітних ints. Для рядків 10000 символів моя програма створює два масиви 10000 елементів, які містять лише +1, 0 або -1. При 1.585 бітах на потрійне число можна було б упакувати ці 20000 трит в 31700 біт, залишивши 300 біт як більше, ніж достатньо для моїх 7-ти інших 16-бітних цілих чисел.

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

#!/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) + "%"

Приклад введення:

2
101100
011010

Приклад багатослівного виводу:

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%
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.