안녕하세요. chogahui05입니다. 함수 잘 만드는 거. 상당히 중요합니다. 구현을 잘 하기 위해서는 모듈화가 중요합니다. 기능을 어떻게 잘 함수로 뺄 지가 중요합니다. 우리가 쉽게 생각할 수 있는 strlen이나, strcat을 생각해 봅시다. 만약에 그 기능들이 모두 main 함수 안에 구현이 되어 있다면 어떨까요? 생각만 해도 끔찍할 겁니다. 물론 1~2줄밖에 되지 않겠지만요.
그러면 문제를 보고 기능을 어떻게 잘게 나누느냐가 중요하다는 것인데요. 오늘은, 백준 문제들 중에서, 어떻게 기능을 나눌 수 있는지만 간단하게 연습해 보도록 하겠습니다. 강의용 예제 코드는 제 github의 BOJ 폴더의 17495_example.cpp입니다. 당연하게도 4444 문제를 찍기 위해서 급하게 작성된 코드이기 때문에, 코드 가독성이라던지, 모듈화라던지, 이런 것들이 심각하게 좋지 않습니다.
17495번 피아노 연주를 봅시다. 음들이 xy나 xy# 형식으로 주어집니다. 이 때, y는 0이상 9이하의 자연수입니다. 문제에서 요구하는 것은, 왼손의 초기 위치와 오른 손의 초기 위치가 주어졌을 때, 어떻게 이동해야 이동 거리가 최소가 되는지를 구하는 것입니다. 다이나믹 프로그래밍을 써야 할 거 같은데, 일단 그거는 나중에 생각하기로 합시다.
그러면 직관적으로 아래 기능이 필요함을 알 수 있습니다.
음에 대한 정보를 문자열로 받았을 때, 이것이 기준 음으로부터 몇 번째 음일까? 기준음은 "C0"로 잡았다고 했을 때요. "C1"은 기준음으로부터 한 옥타브, "C5"는 기준음으로부터 5옥타브만큼 올라간 겁니다. 따라서, 2번째 문자를 먼저 가져올 겁니다.
만약에 이 값이 '2'라면, 한 옥타브 당 몇 개의 음을 건너 뛰는지에다가 2를 곱하면 되겠네요. 기준 음으로부터 2옥타브 올라간 것이기 때문입니다. 문제에 따르면 한 옥타브당 12개의 음이 있네요. 다음에 봐야 할 것은 1번째 알파벳입니다.
CDEFGAB 순으로 각각 도레미파솔라시를 나타내는데요. xy는 기준인 Cy로부터 몇 개의 음만큼 떨어져 있는지도 중요합니다. 예를 들자면, D2는 C2에서 2만큼 떨어져 있고, E2는 4, F2는 5만큼 떨어져 있습니다. po[ch]를 (ch+'A')y가 Cy로부터 몇 개의 음만큼 떨어져 있는지를 나타내는데요.
주어져 있는 그림을 잘 보면 C는 0, D는 2, E는 4, F는 5, G는 7, A는 9, B는 11만큼 떨어져 있습니다. 7개밖에 되지 않기 때문에, 배열로 하드 코딩하면 편할 겁니다.
다음에, '#'이 있는지 없는지는 어떻게 판단하면 좋을까요? 인자로 받은 문자열의 길이가 3이라면 입력이 무조건 '#'이 들어옵니다. xy#는 xy보다는 1개 음만큼 더 떨어져 있습니다. 기준음인 C0로부터. 따라서, len이 3이면, 결과값에 1을 더해주면 됩니다. 이들을 모두 적용하면 함수를 아래와 같이 작성할 수 있습니다.
go(str)은, str에서 "C0"까지 몇 개의 음만큼 떨어져 있는지를 리턴합니다. str은 무조건 "C0"보다 높거나 같은 음이기 때문에, 요렇게 코딩하셔도 큰 무리는 없습니다. 그런데 119줄이라. 뭔가 꽤 길어보이는데 무슨 문제라도 있는 것일까요?
calc 함수가 비정상적으로 긴 게 눈에 보이긴 합니다. 그런데 이것은 그냥 넘어가도록 하겠습니다. le 함수하고 ri 함수를 보도록 합시다. 이 둘은 어떤 함수일까요?
Le 함수를 봅시다. 일단 f와 t가 0이 아니라면, pat[f]와 pat[t]의 음 높이를 얻어와서 그것의 차를 계산합니다. 만약에 f가 0이라면, pat[t]와 left의 음 높이 차를 리턴하고요.
그런데, Ri 함수도 거의 똑같은 일을 한다는 것을 알 수 있어요. 단지 다른 것은 94번째 줄, 106번째 줄만 다를 뿐입니다. 거의 비슷한 기능을 수행하는데, 왜 2개의 함수로 뺄 수 밖에 없었을까요?
뭔가 개 복잡하긴 한데요. left와 right는 처음에 왼손과 오른손이 짚고 있었던 음을 의미합니다. 그리고 pat[i]는 악보의 i번째 음을 의미합니다. 문제의 조건에 따르면 pat[i]는 왼손이나 오른손이나 모두 순서대로 짚을 수 있는 음들입니다. 그런데 코드를 대충 슥 훑어봐도 아시다 시피, 저는 요렇게 설계를 했습니다.
그러면 결론적으로 문제가 되는 것은, 왼손이 처음 위치, left에 있다가 p[3]으로 움직였거나, 오른손이 처음 위치 right에 있다가 p[10]으로 움직인 상황이 될 겁니다. 이 때에는 p[next] - p[cur]과 같은 연산을 할 수 없기 때문입니다. 그런데, 굳이 왼손이 f번째 음에서 t번째 음으로 이동했을 때 거리와, 오른손이 f번째 음에서 t번째 음으로 이동했을 때 거리를 구하는 함수를 따로 뺄 필요가 없습니다. 왜일까요? 기준 음만 추가로 넘기면 되기 때문입니다.
즉, dist 함수 하나로 통합이 가능하다는 겁니다. 보면 왼손의 0번째 음과, 오른손의 0번째 음, 그러니까 처음 음의 위치만 다릅니다. 1번째, 2번째 음은, 왼손으로 누르나, 오른손으로 누르나 음 높이가 같고요. 그렇기 때문에, 3번째 인자로, 어느 손이 0번째 음에서, (예를 들어서 왼손의 경우 처음에 왼손이 위치한 음의 높이, 오른손의 경우, 처음에 오른손이 위치한 음의 높이) i번째 음으로 이동했을 때만 문제가 되기 때문에 gijun이라는 추가 변수를 넣은 것입니다.
그런데 이것도 뭔가 줄일 여지는 있어 보입니다. a>b이면 a-b를 리턴하고, 그게 아니라면 b-a를 리턴한다. 이것은 abs 함수가 하는 일입니다. 레퍼런스 함수에서 충분히 처리할 수 있는 일은, 구현되어 있는 함수의 힘을 빌리는 것도 좋은 선택입니다. abs 함수는 math.h에 구현이 되어 있습니다.
따라서 dist 함수는 위와 같이 줄일 수 있습니다. 이들을 반영한 코드는 제 github의 17495_example_2.cpp에 있습니다. 물론, 이것만 해서는 깔끔한 코드가 되지는 않습니다. 워낙 지저분하게 짜 놓았기 때문입니다. 어떤 아이디어로 조금이나마 더 깔끔한 코드를 만들었는지 정도만 보고 가셔도 좋을 듯 싶습니다.
'구현' 카테고리의 다른 글
bit 연산 : on/off 상태가 20개 언저리면 고려해 보자 (0) | 2019.11.10 |
---|---|
quento 퍼즐 문제 : 백 트래킹으로 풀어봅시다. (4) | 2019.11.02 |
부호 바꾸기 트릭 : 대소를 바꾼다. (4) | 2019.10.19 |
달팽이 배열 알고리즘 : 더미 데이터로 쉽게 구현해 봅시다. (5) | 2019.10.10 |
배열 회전 알고리즘 : 읽는 방법만 생각하면 어렵지 않아요. (12) | 2019.10.07 |
최근댓글