조이스틱으로 알파벳 이름을 완성하세요. 맨 처음엔 A로만 이루어져 있습니다.
ex) 완성해야 하는 이름이 세 글자면 AAA, 네 글자면 AAAA
조이스틱을 각 방향으로 움직이면 아래와 같습니다.
▲ - 다음 알파벳
▼ - 이전 알파벳 (A에서 아래쪽으로 이동하면 Z로)
◀ - 커서를 왼쪽으로 이동 (첫 번째 위치에서 왼쪽으로 이동하면 마지막 문자에 커서)
▶ - 커서를 오른쪽으로 이동 (마지막 위치에서 오른쪽으로 이동하면 첫 번째 문자에 커서)
예를 들어 아래의 방법으로 "JAZ"를 만들 수 있습니다.
- 첫 번째 위치에서 조이스틱을 위로 9번 조작하여 J를 완성합니다.
- 조이스틱을 왼쪽으로 1번 조작하여 커서를 마지막 문자 위치로 이동시킵니다.
- 마지막 위치에서 조이스틱을 아래로 1번 조작하여 Z를 완성합니다.
따라서 11번 이동시켜 "JAZ"를 만들 수 있고, 이때가 최소 이동입니다.
만들고자 하는 이름 name이 매개변수로 주어질 때, 이름에 대해 조이스틱 조작 횟수의 최솟값을 return 하도록 solution 함수를 만드세요.
제한사항
입출력 예
name | return |
---|---|
"JEROEN" | 56 |
"JAN" | 23 |
우리는 처음에 A로만 이루어져있던 문자를 인자로 들어온 알파벳 이름으로 바꿔주는게 목표이다.
이름 길이 만큼 A로 이루어져 있는데, 예를 들면 이름을 JAZ으로 바꿔야 할 때 처음에 AAA로 이루어진 문자열이 표시되고 이를 이름으로 바꿔줘야 한다.
🕹️ -> 이 조이스틱으로 커서를 조작해야 하는데 조작횟수를 최소한으로 하여 이름을 만들어야 한다.
이름을 만드는데 필요한 조작은 두가지이다.
알파벳을 바꿔줄 때 조이스틱을 위로 움직여 알파벳 순으로 움직이거나, 아래로 움직여 알파벳 반대순으로 움직인다.
조이스틱을 오른쪽으로 움직여 다음 알파벳으로 넘어가고, 왼쪽으로 움직여 마지막 알파벳으로 이동한다.
예를 들어, 우리가 바꿔줘야할 이름이 JAZ
이라면,
A
에서 J
까지 아홉 번 조작(BCDEFGHIJ)Z
으로 바꿔줄 때 A
에서 Z
로 알파벳 반대순으로 이동하여 한 번 조작여기서 두 번째 A
로 이동하지 않고 마지막으로 이동한 이유는 A
는 알파벳 조작을 해줄 필요가 없기 때문에, A
를 마주쳤을때 A
를 통과하거나 뒤로 돌아갈 수 있는 선택지가 주어진다.
만약, 첫 번째 알파벳에서 두 번째 알파벳인 A
를 통과하여 세번째로 간다면, 이동하는데 두 번의 조작이 필요하지만 첫 번째 알파벳에서 A
를 거치지 않고 바로 마지막 알파벳으로 돌아간다면 한 번의 조작만 필요하다.
그림으로 설명해보자면 이런식이다.
조이스틱의 움직임을 화살표로 표시했고, 파란 박스는 cursor(커서), 초록색 +{숫자}
로 조작 횟수를 나타내보았다. ↓
그림 출처: 본인
접근을 해보긴 해봤는데...
// 첫번째 문자부터 차례대로 조작한다.
// 첫번째 문자가 A인 경우 반대편으로 간다.
// 오른쪽으로 가는게 최선인 경우는 현재 커서가 문자열의 앞부분에 있을 경우.(i < Math.floor(name.length / 2))
// 왼쪽으로 가는게 최선인 경우는 현재 커서가 문자열의 뒷부분에 있을 경우. i >= Math.floor(name.length / 2)
// 26개 알파벳중에 13(78N) 이전 알파벳이면 위로 가고 이후 알파벳이면 아래로 간다.
나중에 정답을 보니 접근을 잘못 한 듯 하다.
function solution(name) {
var answer = 0;
let min = name.length - 1;
for (let i = 0; i < name.length; i++) {
let currentAlphabet = name.charCodeAt(i);
if (currentAlPhabet < 78) {
answer += currentAlphabet % 65;
} else {
answer += 91 - currentAlphabet;
}
let nextIndex = i + 1;
while (nextIndex < name.length && name.charCodeAt(nextIndex) === 65) {
nextIndex += 1;
}
min = Math.min(
min,
i * 2 + name.length - nextIndex, // 먼저 오른쪽으로 가기
i + (name.length - nextIndex) * 2 // 처음부터 반대로 가기
);
}
answer += min;
return answer;
}
알파벳 이동 횟수 + 커서 이동 횟수
를 누적하여 반환하는 것이다.A
가 없다면 문자열 길이 - 1
가 최소 횟수이다. (첫번째 문자에 커서가 있는 상태이기 때문에 마이너스 1을 해야 함)let min = name.length -1
A
가 포함되어있을 때는 커서가 A
를 거치는 식이 빠를지, 오른쪽으로 조작하여 A
직전까지 갔다가 돌아가는게 빠를지, 아예 처음부터 왼쪽으로(뒤로) 갔다가 앞으로 돌아오는게 빠를지 비교를 해야한다.N
을 기준으로, N
보다 작으면 (A~M
) 알파벳순으로 조작하는게 빠르고, N
보다 크거나 같으면 (N~Z
) 알파벳 반대순으로 조작하는게 빠르다.for (let i = 0; i < name.length; i++) {
let currentAlPhabet = name.charCodeAt(i);
// 반복문을 돌면서 현재 커서가 위치한 알파벳이 N보다 작으면 [커서가위치한알파벳]과 [A]사이를 얼만큼 이동해야 하는지를 계산해준다.
if (currentAlPhabet < 78) { // 78은 N의 아스키코드
// 나눈 후 나머지로 계산해줘도 되고, else문안의 코드처럼 빼기를 해주어도 된다.
answer += currentAlPhabet % 65; // 65는 A의 아스키 코드
} else {
// 현재 커서가 위치한 알파벳이 N보다 크면 [Z]와 [커서가위치한알파벳]사이를 얼만큼 이동해야 하는지를 계산해준다.
answer += 91 - currentAlPhabet; // 91은 Z의 아스키 코드
}
}
A
친구가 오는지 확인하고, A 친구의 바로 뒤의 오는 인덱스를 알아낸다.let nextIndex = i + 1; // A의 뒤 인덱스를 찾을 nextIndex
// 현재 커서가 위치한 알파벳의 다음 알파벳이 A인지 확인한다. A면 nextIndex에 1을 더해준다.
// 만약 JAZ라면 0번째 인덱스인 J 뒤에 A가 하나 있기 때문에 nextIndex는 2가 되어있을테고 2번째 인덱스는 Z이다.
// 이 nextIndex는 나중에 [문자열길이] - [nextIndex]를 하여 마지막 A 뒤에 오는 알파벳 수를 알아낼 수 있다.
while (nextIndex < name.length && name.charCodeAt(nextIndex) === 65) {
nextIndex += 1;
}
이제 최소로 조작하는 횟수를 찾아내야하는데, [처음에 A
를 거쳐서 쭉 이동했던 횟수]와 [ 오른쪽으로 이동했을 때의 횟수], [처음부터 반대로 갔을 때의 횟수] 이 세가지를 비교한다.
min = Math.min(
min, // 그냥 오른쪽으로 쭉 가는 횟수
i * 2 + name.length - nextIndex, // 먼저 오른쪽으로 가기
i + (name.length - nextIndex) * 2 // 처음부터 반대로 가기
);
이게 대체 뭔 소리냐하면!
예시를 들어보겠다.
이 사진에서 MONAAAJOE
비교를 해보자. A 친구들 세명이 있다고 치고 우선 이들을 투명인간 취급해준다.
M
에서 A
직전까지 가는데 두 번, 뒤로 돌아가기 위해서 다시 M까지 가는데 두 번, 뒤로 돌아가서 A
직전까지 가는데(JOE
) 세 번의 조작이 필요하다. 총 일곱 번의 조작이 필요하다.반대로, M
에서 바로 마지막 문자로 이동하여 세번째 A
의 직전인 J
로 가보자. 그러면 세 번의 조작이 필요하다. 다시 MON
을 세기 위해서 처음 문자인 M
으로 돌아오는데만 세 번의 조작이 필요하다. 그리고 다시 첫번째 A
직전까지 세면 두 번의 조작이 필요하여 총 여덟번의 조작이 필요하다.
마지막으로, A
를 거쳐서 오른쪽으로 쭉 커서를 조작하면 여덟번의 조작이 필요하다. 그래서 결론은 MONAAAJOE
는 앞에서 먼저 돌고, 마지막 문자열로 가야 최소한의 횟수로 조작할 수 있다.
M -> O -> N -> O -> M -> E -> O -> J 가 커서 조작 횟수가 적다!
여기까지 비교하는 예시였다.
오른쪽으로 먼저 커서를 조작했을 때
MON
에서는 M
에서 N
으로, 다시 N
에서 M
으로 돌아오는데까지 4번의 조작이 필요하다.A
의 직전 알파벳인 N
의 인덱스는 2이다. 이 인덱스 곱하기 2를 하면 A
직전까지 갔다가 다시 돌아오는 횟수를 구할 수 있다. (왔다 -> 한번, 갔다 -> 두번
그래서 곱하기 2)A
친구들 뒤에있는 알파벳의 수를 하면 결과가 나온다.i * 2 + name.length - nextIndex
// `A` 앞의 알파벳 수 => i
// 왔다갔다 해야하는 횟수 => 2
// `A` 뒤의 알파벳 수 => name.length - nextIndex
처음부터 거꾸로 이동하여 조작했을 때
A
뒤에 있는 알파벳까지 돌고, 다시 문자열의 첫번째 알파벳으로 돌아오는 횟수는:A
뒤에 있는 알파벳의 수에 2를 곱하여 왔다갔다 하는 수를 구할 수 있다.A
친구들 뒤에 있는 알파벳에 다녀오는 수는 구했으니 A
친구들 앞에 있는 알파벳을 더해주면 결과가 나온다.i + (name.length - nextIndex) * 2
// `A` 앞의 알파벳 수 => i
// `A` 뒤의 알파벳 수 => name.length - nextIndex
// 왔다갔다 해야하는 횟수 => 2
이상으로, 프로그래머스 자그마치 레벨 2의 Greedy 탐욕법 문제, 조이스틱의 (내 나름대로) 해석이였다...
코드 + 주석
function solution(name) {
var answer = 0;
// 뒤로 돌아가지 않고 조작했을 때의 최소 횟수는 [문자 length - 1]
// 처음 알파벳부터 다음 알파벳으로 넘어가는 조작 횟수부터 시작하니 length - 1
// 예를 들어 길이가 3인 문자면 1->2로 이동하는 할 때 +1, 2->3으로 이동할때 +1이기 떄문에 length - 1을 해줘야 함.
let min = name.length - 1;
for (let i = 0; i < name.length; i++) {
let currentAlPhabet = name.charCodeAt(i);
// 현재알파벳이 N(26개 알파벳에서 13번째 알파벳)보다 작으면(A~M) 뒤로 돌아갈때가 앞으로 가는것보다 빠름.
// 이동하는 만큼의 조작 횟수를 answer에 저장. (여기서 마이너스를 해도 되고 나눈 후의 나머지로(%) 계산 해도 된다.)
if (currentAlPhabet < 78) {
answer += currentAlPhabet % 65;
} else {
// N보다 크거나 같으면(N~Z)
// A부터 시작해서 조작해야하는데 뒤로 돌아가기 때문에 A를 제외하고 Z부터 세기 때문에 Z - 현재알파벳을 계산.
answer += 91 - currentAlPhabet;
}
// i의 다음 인덱스가 A이면 하나의 혹은 연속된 A 다음에 오는 알파벳의 인덱스를 가리킨다.
let nextIndex = i + 1;
// 현재알파벳이 마지막 알파벳이 될 때까지 && 다음알파벳으로 A가 나올때까지 nextIndex += 1
// nextIndex가 A가 아니면 넘어가고, nextIndex에 A가 나온다면 nextIndex += 1을 하여 A의 다음 인덱스도 A인지 확인한다.
// -> 다음 문자열들에서 A를 찾는 작업
while (nextIndex < name.length && name.charCodeAt(nextIndex) === 65) {
nextIndex += 1;
}
// length - nextIndex는 뒤로 쭉 갔을 때의 길이(A를 통과해서 갔을 때).
min = Math.min(
min,
i * 2 + name.length - nextIndex, // 먼저 오른쪽으로 가기
// 이 경우는 A의 앞에 있는 알파벳들이 뒤에 있는 알파벳의 수보다 적을 경우 최소가 된다.
// 앞에서 갔다가 뒤돌아오는 횟수 (A 뭉떵이를 기준으로 앞에 알파벳 < 뒤에 알파벳일 때 앞에서 A 직전까지 갔다가 다시 돌아오기 때문에 곱하기 2를 해준다.)
// 예를 들어 CDAAJJJJ이면 JJJJ보다 CD가 짧기 때문에 D로 갔다가 다시 뒤돌아서 마지막인 J로 가야한다.
// C에서 D까지 조작해서 +1, 다시 D에서 C로 되돌아갈 때 + 1, 따라서 i * 2를 해줘야 한다.
// 여기서 [문자열의 길이]에서 [마지막에 위치한 A의 바로 뒤에 있는 문자의 인덱스]를 빼주면 A 뒤에 있는 알파벳들의 길이를 구할 수 있다. 예시에서는 JJJ이기 때문에 3.
i + (name.length - nextIndex) * 2 // 처음부터 반대로 가기
// 이 경우는 A의 앞에 있는 알파벳들보다 뒤에 있는 알파벳들의 수가 적을 경우 최소가된다.
// 예를 들어, JJJJAACD 일 때, A의 앞에 문자열이 뒤보다 많기 때문에
// 네번째 J까지 갔다가 다시 앞으로 돌아가는것(8회)보다 처음부터 CD를 돌고 다시 JJJJ로 돌아가는게 횟수가 적다(7회)
);
}
answer += min;
return answer;
}