본문 바로가기
SWE/코테

Edit Distance - 최소 편집 방법 추적하기 / DP

by S나라라2 2018. 10. 29.
반응형


최소 편집 (Edit Distance) 문제에서 최소 편집 '횟수'를 구하는 것에서 확장하여

최소 편집 '방법'을 '추적'하는 문제이다.


문제 풀이


최소 편집 횟수를 구하는 table의 [r][c] 위치에서부터 [0][0]을 목적지로 가장 작은 숫자를 타고 올라가면

최적의 방법을 추적할 수 있다. 


Pseudo Code

1. 3개의 값(좌, 상, 대각선) 중, 가장 작은 값을 찾는다. 

(1) 가장 작은 값 = up 인 경우, DELETE 연산을 적용했다.

(2) 가장 작은 값 = left 인 경우, INSERT 연산을 적용했다.

(3) 가장 작은 값 = diagonal인 경우, 3-(1) 그 값이 현재값과 같은 경우, COPY를 적용했다.

                                               3-(2) 그 값이 현재값과 다른 경우, SUBSTITUTE를 적용했다.

table[r-1][c-1]의 위치에서부터 [0][0]까지 1번 동작을 반복하며 stack에 적용한 연산의 종류를 넣는다.


2. stack이 empty상태가 될 때까지 있는 값을 출력한다.


위의 방법을 토대로 계산하면, 아래 그림의 빨간 네모를 따라서 이동하게 된다.

COPY COPY COPY DELETE COPY SUBSTITUTE SUBSTITUTE



내가 이해한 방법은 아래 그림과 함께 이해하기.


table[3][2] = "str" -> "st" = 1

좌, 상, 대각선 중에서 가장 작은 값 : 0 -> up = delete

*why*

table[r-1][c]에서 table[r][c]로 내려왔다는 건, 

str1과 str2가 어떤 상태에서 s2는 그대로이고 s1만 문자가 추가되었다는 뜻이다. 

그런데 s1에서 s2를 만들고 싶은거니까 가장 비슷했던 상황(table[r-1][c])에서 s1의 추가된 글자만 delete하는 연산을 하면 되는 것이다.


table[2][3] = "st" -> "sto" = 1

좌, 상, 대각선 중에서 가장 작은 값 : 0 -> left = INSERT

*why*

이것도 delete와 같은 맥락으로, 둘이 가장 가까웠던(비슷했던) 상태(table[r][c-1])에서

오른쪽으로 이동했다는 건,

s1은 변함이 없고 s2만 문자가 추가된 것이다.

그러면 s1을 s2로 바꾸고 싶은거니까 s1에 s2에 추가된 문자를 동일하게 INSERT하면 되는 것이다.


table[3][3] = "str" -> "sto" = 1

좌, 상, 대각선 중에서 가장 작은 값 : 0 -> diagonal, 현재값(1)과 다름 = SUBSTITUTE

*why*

str1과 str2이 가장 비슷했던 상태는 왼쪽 대각선이고, s1도 문자가 추가, s2도 문자가 추가된 것이다.

그런데 str1과 str2에 추가된 문자가 서로 다르므로(현재값과 다른 값을 가지고 있음) '대체' 연산이 적용된 것을 알 수 있다.


table[4][3] = "stron" -> "sto" : 1

좌, 상, 대각선 중에서 가장 작은 값 : 1 -> diagonal, 현재값(1)과 같음 = COPY

*why*

가장 작은 숫자인 대각선과 동일한 값이라는 것은 str1과 str2가 대각선의 위치에서 현재 위치로 이동하면서

s1도 증가하고 s2도 증가하였지만 아무 연산도 해주지 않았다는 것이다.

즉, 둘이 같은 값이 추가된 것이다.



*Edit Distance를 추적하는 방법이 딱 한 가지만 있을까?*

예를 들어, 끝에서 [0][0]방향으로 추적해가는 와중에, table[4][3]은 가장 작은 숫자 두 가지를 맞닥뜨리게 된다. 

대각선인 table[3][2]와 위인 table[3][3] 모두 1의 값을 가지고 있다.


두 가지 경우 모두 생각해보자.


(1) 대각선에 위치한 table[3][2]를 지나갈 경우?

현재 위치(table[4][3])에 있는 값(1)과 동일한 값을 가지므로, COPY 연산을 적용한 것을 알 수 있음.

그리고 table[3][2]는 table[2][2]를 지나갈 것임.

따라서 COPY DELETE 연산을 수행했음.

(2) 위에 위치한 table[3][3]을 지나갈 경우?

DELETE연산을 수행했고, table[3][3]에서 table[2][2]로 갈 것인데 값이 다르므로

SUBSTITUTE 연산을 적용한 것을 알 수 있음.

따라서 DELETE SUBSTITUTE 연산을 수행했음


두 가지 경우 각각의 Edit Distance를 계산하면 (1)의 경우는 1, (2)의 경우는 2임. 

결과적으로 (2)의 경우를 따를 경우 전체 edit distance가 다른 값이 나오는 것을 확인할 수 있고, (2)의 경우는 잘못된 것임.


따라서 좌, 상, 대각선 중 가장 작은 값을 찾을 때, 

대각선이 가장 작을 경우를 우선순위로 둬야한다!!!!!!!


그리고 최적 편집 방법이 꼭 한 가지만 존재하는건지는 아직 확실치않다!!

지금 예시에서는 한 가지 방법만 존재함.




**구현할 때 유의사항

현재 위치에서 좌, 상, 대각선 3개 중에서 가장 작은 값을 고를 때, 

대각선이 가장 작은 경우를 먼저 고려해줘야한다. 그래야 '최소' 편집 방법이 나올 수 있다. 

왜냐하면 대각선이 copy일 수 있고, copy가 가장 작은 편집 거리 값(0)을 가지고 있기 때문이다.



C언어 구현코드

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

// 열거형
// 첫번째 변수부터 모든 변수가 0,1,2,3,...의 값을 차례로 가진다.
enum OPERATION {
	COPY = 0,
	INSERT,
	DELETE,
	SUBSTITUTE,
	NON
};

int **createTable(int _num_r, int _num_c) {
	int **res_table = (int **)malloc(_num_r*(sizeof(int*)));
	for (int i = 0; i < _num_r; i++) {
		res_table[i] = (int *)malloc(_num_c*(sizeof(int)));
	}
	return res_table;
}

int getMinimum(int a, int b, int c) {
	if (a <= b && a <= c) return a;
	else if (b <= a && b <= c) return b;
	else if (c <= a && c <= b) return c;
}

void populateTable(int **_table, char *_s1, char *_s2, int _num_row, int _num_col) {

	// 1행 채우기
	for (int i = 0; i < _num_col; i++) {
		_table[0][i] = i;
	}
	// 1열 채우기
	for (int i = 0; i < _num_row; i++) {
		_table[i][0] = i;
	}

	// 나머지 칸들 채우기
	for (int i = 1; i < _num_row; i++) {
		for (int j = 1; j < _num_col; j++) {
			if (_s1[i-1]==_s2[j-1]) { // 마지막 글자가 같다면,
				_table[i][j] = _table[i - 1][j - 1];
			}
			else {
				_table[i][j] = getMinimum(_table[i][j-1], _table[i-1][j], _table[i-1][j-1]) + 1; // 좌, 상, 대각선
			}
		}
	}
}

enum OPERATION *stack = 0; // stack을 전역변수로 생성해준다.
int stack_top = - 1;

void CreateStack(int _num_row, int _num_col) {
	
	// stack의 최대 사이즈는 [num_row-1][num_col-1]에서 [0][0]로 가는 가장 긴 거리이다.
	// 가장 긴 거리는 table의 테두리르 타고 올라가는 것으로,
	// (행-1) +(열-1)이다.
	stack = (enum OPERATION *)malloc(sizeof(enum OPERATION)*(_num_row + _num_col - 2));
	stack_top = -1;
}

// 좌, 상, 대각선 중에서 가장 작은 값을 찾는 함수.
enum OPERATION findSmallestNeighbor(int **_table, int _r, int _c) {
	int left, up, diag;
	
	// boundary check를 해줘야한다.
	// 좌
	if (_c-1 >= 0) {
		left = _table[_r][_c - 1];
	}
	else { // boundary를 넘어갈 경우,
		// 절대 선택되지 못하도록 가장 큰 값으로 설정한다.
		left = INT_MAX;
	}
	//상
	if (_r-1>=0) {
		up = _table[_r - 1][_c];
	}
	else {
		up = INT_MAX;
	}
	// 대각선
	if (_r-1>=0 && _c-1>=0) {
		diag = _table[_r - 1][_c - 1];
	}
	else {
		diag = INT_MAX;
	}

	// left, up, diag 중에서 가장 작은 값을 찾는다.
	if (diag <= left && diag <= up) { // 대각선에 가장 높은 우선권을 줘야함!
		if (_table[_r][_c] == diag) {
			return COPY;
		}
		else {
			return SUBSTITUTE;
		}
	}else if (left <= up && left<= diag) {
		return INSERT;
	}
	else if (up <= left && up <= diag) {
		return DELETE;
	} 
}

void stack_push(enum OPERAION _operation) {
	stack_top++;
	stack[stack_top] = _operation;
}

enum OPERATION stack_pop() {
	if (stack_top==-1) { // empty
		return NON;
	}
	enum OPERATION temp = stack[stack_top];
	stack_top--;
	return temp;
}

void showOperationSequence(int **_table, int _num_row, int _num_col) {

	// 1. operation을 담을 stack을 만든다.
	CreateStack(_num_row, _num_col);

	// 2. [num_row-1][num_col-1] 부터 [0][0]까지 추적한다.
	int r = _num_row - 1;
	int c = _num_col - 1;
	while (1) {
		if (r == 0 && c == 0) {  // 0,0일 경우 table을 추적 오나료.
			break;
		}

		// 3개의 값(좌, 상, 대각선) 중 가장 작은 값을 확인하는 함수.
		enum  OPERATION result = findSmallestNeighbor(_table, r, c);
		switch (result) {
		case COPY:
			stack_push(COPY);
			r--;
			c--;
			break;
		case INSERT:
			stack_push(INSERT);
			c--;
			break;
		case DELETE:
			stack_push(DELETE);
			r--;
			break;
		case SUBSTITUTE:
			stack_push(SUBSTITUTE);
			r--;
			c--;
			break;
		}
	}

	// 3. stack이 empty 상태일 때까지, 출력한다.
	while (1) {
		enum OPERATION res = stack_pop();
		if (res == NON) {
			break;
		}
		switch (res) {
		case COPY:
			printf("operation COPY \n");
			break;
		case INSERT:
			printf("operation INSERT \n");
			break;
		case DELETE:
			printf("operation DELETE \n");
			break;
		case SUBSTITUTE:
			printf("operation SUBSTITUTE \n");
			break;
		}
	}
}

int main(void) {
	
	// s1(행) -> s2(열)
	char* s1 = "strong";
	char* s2 = "storm";

	int num_row = strlen(s1) + 1; // 처음의 " "(공백)을 포함하여 +1을 해준다.  // 7
	int num_col = strlen(s2) + 1;                                              // 6
	
	int **table = createTable(num_row, num_col); // 동적 배열을 생성해서 return 해준다.

	populateTable(table, s1, s2, num_row, num_col); // table을 채우는 함수.

	printf("\n----------------------Edit Distance----------------------\n");

	printf("Edit distacne between %s and %s is %d\n",s1,s2,table[num_row-1][num_col-1]);
	
	showOperationSequence(table, num_row, num_col);
	
}


실행결과






아래 실행 결과는 up, left, diag 중에서 가장 작은 값을 찾을 때,

diag에 우선권을 주지 않았을 때의 결과이다. (틀린 결과)

operation으로 edit distance를 계산해보면, 4(틀린 값)가 나오는 것을 확인할 수 있다.


반응형