코딩 테스트/프로그래머스

[Python] 프로그래머스 lv.2 조이스틱

위시리 2025. 3. 31. 18:25

https://school.programmers.co.kr/learn/courses/30/lessons/42860

 

프로그래머스

SW개발자를 위한 평가, 교육, 채용까지 Total Solution을 제공하는 개발자 성장을 위한 베이스캠프

programmers.co.kr

 

문제 분석

  • 최소한으로 조이스틱을 움직여서 주어진 알파벳을 만들 때, 움직인 횟수 return
  • 맨 처음에는 주어진 알파벳의 글자 수만큼 A로만 이루어져 있다.

 

코드 설계

  1. 수정이 필요한 글자의 idx 확인 (최단 이동거리를 위해)
  2. 이동 거리에 대한 최솟값
  3. 알파벳 수정에 대한 최솟값

 

정답 코드

실패

def solution(name):
    ans = 0
    init = list('A' * len(name))
    modi_idx = []

    # 수정이 필요한 글자 위치 찾기
    for i in range(len(name)) :
        if init[i] != name[i] :
            modi_idx.append(i)

    be_idx = 0

    for idx in modi_idx :
        # 최소 글자 이동 고려 (v)
        if idx-be_idx < (be_idx + len(name) - idx + 1) : # 정방향 vs. 역방향
            ans += abs(idx-be_idx)
        else :
            ans += (be_idx + len(name) + 1) - idx
        
        # 최소 수정 횟수 / 목표 알파벳 : name[idx]
        if ord(name[idx]) - ord('A') < ord('Z') - ord(name[idx]) + 1 :
            ans += abs(ord(name[idx]) - ord('A'))
            
        else :
            ans += ord('Z') - ord(name[idx]) + 1

        init[idx] = name[idx]

        be_idx = id

    return ans

 

1. 다른 정답 코드 : https://velog.io/@jqdjhy/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%ED%8C%8C%EC%9D%B4%EC%8D%AC-%EC%A1%B0%EC%9D%B4%EC%8A%A4%ED%8B%B1-Greedy

  • 연속되는 A가 있을 때, 그것의 왼쪽이나 오른쪽부터 시작하며 알파벳을 변경하는 것이 효율적이다.
  • 이전처럼 다음 인덱스와 비교하면서 정방향과 역방향 만을 비교하면 모든 'A'가 연속된 경우 최적의 이동 경로를 보장하지 못할 가능성이 있다.
  • 따라서 중간에 A가 있을 경우 우회하는 방법까지 고려
    연속되는 A의 길이가 가장 긴 부분을 안가는 것이 효율적
  • → 완전 탐색 (Brute Force)

 

def solution(name) :
    ans = 0

    # 기본 최소 좌우이동 횟수는 길이-1
    min_move = len(name)-1

    for i, char in enumerate(name) :
        # 변경에 대한 최솟값
        ans += min(ord(char)-ord('A'), ord('Z')-ord(char)+1)

        # 해당 알파벳 다음부터 연속된 A 문자열 찾기
        next = i+1
        while next < len(name) and name[next] == 'A' :
            next += 1

        min_move = min([min_move, 2 * i + len(name) - next, i + 2 * (len(name)-next)])

    # 알파벳 변경(상하이동) 횟수에 좌우 이동 횟수 주가
    ans += min_move
    return ans

 

왜 단순히 각 문자 간의 거리를 최소화하는 것이 아니라, 사이에 있는 연속된 'A'의 개수를 고려해야 하는가?

 

🚀 핵심 개념: A를 지나치는 것이 비효율적일 수 있다!

조이스틱 이동에서 중요한 점은 수정해야 할 문자의 위치에 도달하는 최소한의 이동을 찾는 것이다.
그런데, 단순히 "다음 수정해야 할 문자"까지의 거리만 고려하면 안 되는 이유는 다음과 같다.

 

"중간에 있는 'A'는 움직이지 않아도 되는 공간이다."
즉, 불필요한 'A'를 지나치는 대신 뒤로 돌아가는 게 더 효율적일 수 있음!

 

🛠 예제 분석 1: "JAAAN" (연속된 'A'가 있는 경우)

단순 정방향 이동 (비효율적)

  1. J를 만들고
  2. A → A → A 지나가면서 오른쪽으로 이동 (불필요한 이동 발생)
  3. N을 만들고 종료

➡ 이동 횟수: 4회 (J에서 N까지 3칸 이동)

효율적인 이동 (뒤로 돌아가기)

  1. J를 만들고
  2. 왼쪽으로 이동(◀)하여 N`을 먼저 만든 후
  3. 다시 J로 돌아옴

➡ 이동 횟수: 2회 (J → N → J)

🚨 결론:
"다음 문자까지의 거리"만 고려하면 불필요한 이동을 할 가능성이 있음!
➡ 연속된 'A'를 피하는 것이 더 적은 이동 횟수를 만들 수도 있다.

 

🛠 예제 분석 2: "JANAAAAN"

단순 정방향 이동 (비효율적)

  1. J → A → N → A → A → A → A → N (7칸 이동)

➡ 이동 횟수: 7회

효율적인 이동 (뒤로 돌아가기)

  1. J → A → N 만든 후
  2. 왼쪽(◀)으로 바로 이동해서 N을 만든다.
  3. 중간에 AAAA를 건너뛸 수 있음

➡ 이동 횟수: 4회

 

🚨 결론 : 연속된 'A'가 많을수록, 단순 정방향 이동보다 '우회'가 유리할 수 있다!

🔥 결론: 연속된 'A'가 많을수록 최적 경로가 달라질 수 있다!

단순히 이전 문자와 다음 문자 사이의 거리만 비교하면 불필요한 이동이 발생할 수 있음
A가 연속된 구간을 우회하는 방법을 고려해야 최소 이동이 보장됨! 🚀

 


 

 

1️⃣ 기본적인 좌우 이동 개념

일반적으로, 조이스틱은 오른쪽으로 이동하면서 문자를 변경하는 것이 기본적인 방식이다.
즉, 모든 문자를 방문해야 한다고 가정하면, 기본 이동 횟수는 len(name) - 1 (오른쪽으로 한 칸씩 이동)

min_move = len(name) - 1

하지만, 연속된 'A'가 있는 경우 굳이 오른쪽으로 끝까지 가지 않고 뒤로 돌아가는 것이 유리할 수도 있음
따라서, 연속된 'A'의 위치를 고려하여 최소 이동 횟수를 조정한다.

2️⃣ min_move = min([ ... ]) 식 분석

min_move = min([min_move, 2 * i + len(name) - next, i + 2 * (len(name)-next)])

여기서 i는 현재 검사 중인 문자 위치, next는 연속된 'A' 다음 첫 번째 문자 위치입니다.

각 항목이 의미하는 바를 하나씩 살펴볼게요.

(1) min_move 기본값: len(name) - 1

min_move = len(name) - 1
  • 그냥 왼쪽에서 오른쪽으로 끝까지 가는 경우 (가장 단순한 이동 방법)
  • "JEROEN"의 경우 "J → E → R → O → E → N"으로 가면 이동 횟수는 5

(2) 2 * i + len(name) - next

2 * i + len(name) - next

🚀 설명:

  • i까지 갔다가 다시 되돌아간 후, 반대 방향으로 진행하는 경우
  • 즉, 처음 → i까지 진행 → 다시 처음으로 돌아감(index = 0) : i*2 → 뒤쪽 문자로 이동
  • "JANAAAAAN" 같은 경우를 고려한 방식

🛠 예제: "JANAAAAAN"

위치 문자

0 J
1 A
2 N
3 A
4 A
5 A
6 A
7 N
  • 처음부터 J → A → N (i=2)까지 이동
  • 다시 되돌아와서 (2*i)
  • 'A'가 끝난 후 (next=7 이후)부터 이동

이동 횟수:
2 * i + len(name) - next = 2 * 2 + 8 - 7 = 4 + 1 = 5

(3) i + 2 * (len(name) - next)

i + 2 * (len(name) - next)

🚀 설명:

  • i까지 가고 한 번에 끝까지 가는 대신, 뒤쪽에서 먼저 완성한 후 되돌아오는 경우
  • "JANAAAAAN"처럼 뒤쪽에 유효한 문자가 있는 경우 유리할 수 있음

🛠 예제: "JANAAAAAN"

위치 문자

0 J
1 A
2 N
3 A
4 A
5 A
6 A
7 N
  • J → A → N (i=2)까지 이동
  • 곧바로 끝까지 이동 (next=7 이후)
  • 다시 N이 있는 곳까지 되돌아옴 (2 * (len(name) - next))

이동 횟수:
i + 2 * (len(name) - next) = 2 + 2 * (8 - 7) = 2 + 2 = 4

3️⃣ 정리

이동 방법은 총 3가지:
1️⃣ 기본적으로 왼쪽에서 오른쪽으로 이동 (len(name) - 1)
2️⃣ 중간까지 갔다가 다시 처음으로 되돌아오는 경우 (2 * i + len(name) - next)
3️⃣ 끝까지 갔다가 다시 돌아오는 경우 (i + 2 * (len(name) - next))

세 가지 경우 중 가장 작은 값을 min_move로 업데이트
결과적으로, 연속된 'A'를 우회하는 최적의 경로를 찾을 수 있음! 🚀

 

 

다른 정답 코드 2 : https://sweetburble.tistory.com/226

def solution(name):
    count = 0
    N = len(name)

    for ch in name: # 2. 먼저 커서가 어디있든, A에서 최소로 바꿔야 그 글자가 되는지 모두 계산한다
        if (ch != 'A'):
            min_up_down = min(ord(ch) - ord('A'), 26 + ord('A') - ord(ch))
            count += min_up_down
    
    move = N - 1 # 1. 커서를 좌우로 움직이는 것은, 아무리 최악이어도 문자열을 한번 순회하는 것뿐이다 -> N - 1
    for left in range(N):
        idx = left + 1
        while (idx < N) and (name[idx] == 'A'): # right는 오른쪽 끝에서, left 오른쪽을 기준으로 처음 A가 아닌 알파벳의 위치만큼 빼줘야 한다!
            idx += 1
            
        right = N - idx
        back_distance = min(left, right) # 한쪽 방향으로 이동 후, 반대 방향으로 이동하는 최소거리를 구한다 -> 그러므로 left나 right 중 최솟값을 한번 더 더하는 것이다.
        
        move = min(move, left + right + back_distance)
 
    count += move
    return count