본문 바로가기

알고리즘/문제 풀이

[프로그래머스] Lv2 아날로그 시계 java 이해하기 쉬운 풀이!

1. 문제 설명 📌

문제 링크

아날로그 시계의 초침이 시침 혹은 분침과 겹칠 때마다 알람을 울릴 건데, 주어진 시작 시간부터 끝시간 내에 알림이 몇 번 울렸는지 횟수를 반환하는 함수를 작성하는 문제. 시계의 초,분,시침은 연속적으로 움직인다. 따라서 겹치는 시기가 0.001초 단위일 수도, 0.00001초 단위일수도 있다. 이를 다 생각해서 겹치는 횟수를 구해라! (Lv2 맞나?? Lv3로 격상해야할 듯...)

2. 접근 방식 🗃️

KEY WORD: SIMULATION
시계 침들의 겹침 현상을 최대한 코드로 구현해야 한다. 하지만 연속적으로 이루어지는 움직임 속에서 겹치는 순간을 포착한다는 것은 불가능한 일이다. 따라서 겹친다의 기준을 다음과 같이 정한다.

(1) 겹친다의 기준

각도 상 초침이 시침 혹은 분침보다 뒤쳐졌으나, 1초가 지난 후에 초침이 시침 혹은 분침을 앞서갔다. 이것이 증명된다면 1초라는 찰나 안에 초침이 시침 혹은 분침과 겹친 경우가 있다는 말이 된다. 이제 우리는 초침이 뒤쳐졌다가 1초 사이에 앞서간 경우겹친다와 동일어로 간주하고 문제를 풀 것이다. 그림으로 설명하면 다음과 같다.

(2) 세부적인 구현

이제 세부적으로 하나 하나 어떻게 구현할지 알아보겠다. 먼저 구현할 내용을 순서대로 알아보면 다음과 같다.

  1. 시작 시간 당시의 초침, 분침, 시침의 아날로그 시계 상 각도를 구한다.
  2. 1초마다 시침, 초침, 분침을 이동시킨다.
  3. 해당 1초 사이에 초침이 시침 혹은 분침과 겹친 적이 있는지 확인한다.

이제 한 단계씩 해보자.

(3) 각도 구하기

아날로그 시계는 원으로 360도이다. 여기서 숫자가 표시된 부분은 총 12개 임으로 30도에 하나씩 그려져 있을 것이다.

  • 초침: 초침은 60초에 360도를 돌아야 한다. 따라서 1초에 6도씩 움직인다.
  • 분침: 분침은 60분에 360도를 돌아야 한다. 60분은 3600초임으로, 1초에 0.1도씩 움직여야 한다.
  • 시침: 시침은 12시간에 360도를 돌아야 한다. 따라서 1시간에 30도를 움직인다. 이는 3600초에 30도를 움직인다는 뜻 이므로, 1초에 1/120도를 움직인다.

표로 각 시간 단위 별 움직이는 시간을 나타내면 다음과 같다.

단위 sec min hour
초침 6도 - -
분침 0.1도 6도  
시침 (1/120)도 0.5도 30도

이제 주어진 시간을 활용해 각 침별 각도를 계산해보자. 여기서 주의해야할 점은 1초마다 분침과 시침도 미세하게 움직인다는 것이다. 처음 문제를 풀 때, 11시 59분 59초라면 11시만을 이용해 시침의 각도를 구하고, 59분만을 이용해 분침을 구하는 오류를 범했다. 해당 오류는 모든 침은 시간에 흐름에 따라 연속적으로 움직인다.는 것을 간과해서 생긴 것이다. 따라서 시침 계산 시에는 시,분,초 모두 고려하고, 분침 계산 시에도 분과 초 모두 고려해야 한다.

이제 시작 시간을 예로 들어서 그에 맞는 각 침별 각도를 구해보자. 17시 30분 32초가 주어졌다고 가정할 때,
시침 = (17%12)*30 + 30*(0.5) + 32*(1/120d)
분침 = 30*(6) + 32*(0.1)
초침 = 32 * 6
이 될 것이다. 시침 계산 시 먼저 주어진 시각에 %12를 한 이유는 시침은 유일하게 주어진 단위(0

23시) 내에 두 바퀴를 도는 침이기 떄문이다. (1

12 한 바퀴, 13

24(0시) 한 바퀴) 따라서 군인 시간(0

23시)으로 들어온 입력을 민간인 시간(1~12) 단위로 바꿔야지 제대로 된 각도를 구할 수 있다. 위의 예시에서 나온 풀이를 공식화 하면 다음과 같다.

주어진 시각 = h1m1s1
시침 = (h1%12)*30 + m1*(0.5) + s1*(1/120d)
분침 = (m1*6) + (s1*0.1)
초침 = (s1*6)

(4) 1초마다 모든 침 이동

이건 쉽다. 아까 각 침별로 초마다 얼마나 이동해야 하는지 구했다. 1초 이동시 시침은 1/120도, 분침은 0.1도 초침은 6도씩 움직이면 된다.

(5) 겹치는 행위가 있었는지 체크

시침의 각도를 hDegree, 분침의 각도를 mDegree, 초침의 각도를 sDegree라 할때,
이동 전 hDegree > sDegree 였는데, 계산 후 hDegree < sDegree가 된 경우를 찾으면 된다. 분침과 시침의 관계에서도 마찬가지이다.

(6) 예외 처리

시,분,초침이 모두 겹친 경우 알림은 1번만 울려야 한다. 그래서 중복을 제거 해줘야 한다. 나같은 경우는 분침이 시침보다 뒤쳐졌다가 1초 후 분침이 시침을 앞지른 경우 가 겹치는 행위 내에 포함되면, 초침이 시침과 겹치는 행위분침이 시침과 겹치는 행위가 중복된 것임으로 이를 이용해 중복을 제거했다.

시작 시간에서 겹치는 경우끝 시간에서 겹치는 경우도 체크 해주면 모든 예외처리는 끝난다.

3. 코드 소개 🔎

사실 아이디어가 다인 문제라 코드 자체는 쉽다.

class Solution {

    static int cnt = 0;

    double h_d;
    double m_d;
    double s_d;


    public int solution(int h1, int m1, int s1, int h2, int m2, int s2) {
        int acc = 0;
        calculateDegree(h1,m1,s1);
        int start = h1*3600 + m1*60 + s1;
        int end = h2*3600 + m2*60 + s2;

        while(start < end){
            start++;
            acc += timePast();
        }

        calculateDegree(h1,m1,s1);
        if(Double.compare(h_d, m_d) == 0 || Double.compare(m_d,s_d) == 0) acc++;
        calculateDegree(h2,m2,s2);
        if(Double.compare(h_d, m_d) == 0 || Double.compare(m_d,s_d) == 0) acc++;

        return acc;
    }


    public void calculateDegree(int h1, int m1, int s1) {
        this.h_d = (h1%12)*30 + m1*(0.5) + s1*0.0083;
        this.m_d = (m1*6) + (s1*0.1);
        this.s_d = (s1*6);
    }


    public int timePast() {
        int ans = 0;
        double prevH = this.h_d;
        double prevM = this.m_d;
        double prevS = this.s_d;

        this.h_d += 0.0083333333333333;
        this.m_d += 0.1;
        this.s_d += 6;

        if(Double.compare(prevH, prevS) > 0 && Double.compare(h_d,s_d) <= 0) ans++;
        if(Double.compare(prevM, prevS) > 0 && Double.compare(m_d,s_d) <= 0) ans++;
        if((Double.compare(prevH, prevM) > 0 && Double.compare(prevH,prevS) > 0) 
           && (Double.compare(h_d, m_d) <= 0 && Double.compare(h_d, s_d) <= 0)) ans--;

        if(this.h_d >=360) this.h_d %= 360; 
        if(this.m_d >=360) this.m_d %= 360;
        if(this.s_d >=360) this.s_d %= 360;

        return ans;
    }
}

4. 배운 것들 🎯

소수점을 유지하는 방법에 대해

java에서는 나눗셈 부호인 /을 이용하면 몫만 반환된다. 이 때문에 우리가 시침의 이동을 구할 때, 1/120의 결과가 0.833333으로 반환되지 않고 결과로 0으로 반환된다. 이를 해결하려면 소수점을 남기고 싶은 나눗셈 계산 뒤에 d라는 문자를 추가하여 해당 계산이 double 타입 계산임을 명시하면 된다. 이러면 double 타입으로 계산이 처리되어 소수점이 유지된다.