본문 바로가기
알고리즘

[프로그래머스lv2] 아날로그 시계 JS [구현]

by limew 2023. 12. 26.

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

 

문제 설명

시침, 분침, 초침이 있는 아날로그시계가 있습니다. 시계의 시침은 12시간마다, 분침은 60분마다, 초침은 60초마다 시계를 한 바퀴 돕니다. 따라서 시침, 분침, 초침이 움직이는 속도는 일정하며 각각 다릅니다. 이 시계에는 초침이 시침/분침과 겹칠 때마다 알람이 울리는 기능이 있습니다. 당신은 특정 시간 동안 알람이 울린 횟수를 알고 싶습니다.

다음은 0시 5분 30초부터 0시 7분 0초까지 알람이 울린 횟수를 세는 예시입니다.

  • 가장 짧은 바늘이 시침, 중간 길이인 바늘이 분침, 가장 긴 바늘이 초침입니다.
  • 알람이 울리는 횟수를 세기 시작한 시각은 0시 5분 30초입니다.
  • 이후 0시 6분 0초까지 초침과 시침/분침이 겹치는 일은 없습니다.
  • 약 0시 6분 0.501초에 초침과 시침이 겹칩니다. 이때 알람이 한 번 울립니다.
  • 이후 0시 6분 6초까지 초침과 시침/분침이 겹치는 일은 없습니다.
  • 약 0시 6분 6.102초에 초침과 분침이 겹칩니다. 이때 알람이 한 번 울립니다.
  • 이후 0시 7분 0초까지 초침과 시침/분침이 겹치는 일은 없습니다.

0시 5분 30초부터 0시 7분 0초까지는 알람이 두 번 울립니다. 이후 약 0시 7분 0.584초에 초침과 시침이 겹쳐서 울리는 세 번째 알람은 횟수에 포함되지 않습니다.

다음은 12시 0분 0초부터 12시 0분 30초까지 알람이 울린 횟수를 세는 예시입니다.

  • 알람이 울리는 횟수를 세기 시작한 시각은 12시 0분 0초입니다.
  • 초침과 시침, 분침이 겹칩니다. 이때 알람이 한 번 울립니다. 이와 같이 0시 정각, 12시 정각에 초침과 시침, 분침이 모두 겹칠 때는 알람이 한 번만 울립니다.
  • 이후 12시 0분 30초까지 초침과 시침/분침이 겹치는 일은 없습니다.

12시 0분 0초부터 12시 0분 30초까지는 알람이 한 번 울립니다.

알람이 울리는 횟수를 센 시간을 나타내는 정수 h1, m1, s1, h2, m2, s2가 매개변수로 주어집니다. 이때, 알람이 울리는 횟수를 return 하도록 solution 함수를 완성해주세요.

제한사항
  • 0 ≤ h1, h2 ≤ 23
  • 0 ≤ m1, m2 ≤ 59
  • 0 ≤ s1, s2 ≤ 59
  • h1시 m1분 s1초부터 h2시 m2분 s2초까지 알람이 울리는 횟수를 센다는 의미입니다.
    • h1시 m1분 s1초 < h2시 m2분 s2초
    • 시간이 23시 59분 59초를 초과해서 0시 0분 0초로 돌아가는 경우는 주어지지 않습니다.


 

첫번째 풀이

초침이 한 바퀴 돌때 시침과 분침을 각각 지나쳐 2번 알람이 울린다.

1분동안 2번 울리므로

60분동안 2번 * 60분 = 120번 울린다

 

이 논리대로라면 

0시~1시 사이120번

1시~2시 사이 120번

2시~3시 사이 120번

...

22시~23시 사이 120번

23시~0시 사이 120번

 

0시~24시 24시간 동안 120*24 = 2880번이 울린다.

 

하지만 자세히 생각해보면

  • 분침과 초침은 59분 => 00분으로 갈때마다 만나지 않는다 (매 시각 이 간격마다 울리는 횟수를 하나씩 줄인다 -24)
  • 시침과 초침은 11시 => 12시, 23시 => 0시로 갈 때마다 만나지 않는다 (-2)
  • 시침, 초침, 분침이 다 같이 겹친 경우 0시0분0초, 12시0분0초에는 카운트를 1로 친다 (-2)

따라서 0시0분0초에서 23시59분 59초까지 울리는 횟수는 2880-24-2-2 = 2852번이다.

 

시작시각에서 끝시각 사이 울리는 횟수는 어떻게 구할까?

시작시각 ~ 끝시각 사이 울린 횟수를 구하기는 까다로워 다음과 같이 구한다.

시작시각 ~ 끝시각 사이 울린 횟수 = 0시0분0초부터 끝시각까지 울린 횟수 - 0시0분0초부터 시작시각까지 울린 횟수

 

그럼 0시0분0초부터 특정 시각까지 울리는 횟수는 어떻게 구할까

0시0분0초에서 1시00분30초까지 울리는 횟수를 구한다고 가정하면

1시0분까지 울리는 횟수 + 30초간 울리는 횟수를 더하면 된다.

 

30초간 울리는 횟수는 0시0분0초를 각도 0이라고 봤을 때 시침, 분침, 초침의 각도에 따라 판단할 수 있다.

초침의 각도 >= 시침의 각도이면 초침은 이미 시침을 지나친것이므로 종이 1번 울렸다
초침의 각도 >= 분침의 각도이면 마찬가지로 종이 1번 울렸다.

 

그럼 각 각도는 어떻게 구할까

시침은

1시간당 360도 / 12시간 = 30도 회전한다

1분당 30도 / 60 = 0.5도 회전한다

1초당 0.5 / 60 = 0.0083도 회전한다

=> 시침의 각도 = ( 30 *시 + 0.5*분 +  0.0083 *초  ) %360 도 (마지막에 360도로 나누는 이유는 360도 = 0도로 변환하기 위해서이다.)

 

분침은

1분당 360도 / 60분 = 6도 회전한다

1초당 6 / 60 = 0.1도 회전한다

=> 분침의 각도 = (6*분 + 0.1*초) % 360 도

 

초침은

1초당 360도 / 60 = 6도 회전한다

=> 초침의 각도 = (6*초) % 360 도

 

 

참고이 분의 글을 참고했다.

 

function solution(h1, m1, s1, h2, m2, s2) {
    function getCount(h, m, s) {
        let bell = -1; // 0시0분0초는 1번만 울린다
        
        bell += (h*60+m)*2; // 초침이 한바퀴 돌때 시침, 분침 총 2번 만난다
        bell -= h; // 59분 -> 00분일때 분침은 초침과 만나지 않는다
        if (h >= 12) bell -= 2; // 11시59분59초->12시경우 분,초침과 만나지 않고 12시에 1번 만난다
        
        const hDegree = (h*30+m*0.5+s*(0.5/60))%360;
        const mDegree = (m*6 + 0.1*s) % 360;
        const sDegree = (6*s) % 360;
      
        if (sDegree >= hDegree) bell++; // 초침이 시침을 지나쳤다
        if (sDegree >= mDegree) bell++; // 초침이 분침을 지나쳤다
        
        return bell;
    }
    // 0시0분0초부터 시작, 끝 시각까지 횟수
    let totalBell = getCount(h2, m2, s2) - getCount(h1, m1, s1);
    // 시작시간이 0시거나 12시이면 +1
    if ((h1 === 0 || h1 === 12) && m1 === 0 && s1 === 0) totalBell++;
    return totalBell;
}

 

느낀점

 

어려운 수학문제인 것 처럼 느껴졌다. 테스트케이스를 보며 시계가 돌아가는 걸 손으로 그려봐도 문제에 나온 예제조차 이해가 잘 가지 않았다. 몇 시간 고민하다가 결국 다른 분의 풀이를 봤다. 대략의 방법은 알 수 있었지만 내 맘에 쏙 들게 이해가 완벽하게 되진 않았다. 솔직히 이런 문제가 코테에 나올까 의문스럽기도 했다. 알고리즘, 자료구조를 쓰지않는 단순한 구현이라고 생각하기 때문이다. 변별력을 기르는데는 도움이 되겠지만 난 이것보다 다른 문제를 정확히 풀 것 같다.

 


 

두번째 풀이

 

스터디 멤버님의 풀이다. 개인적으로 첫번째 풀이보다 이해가 더 잘 된다. 

 

1. 초마다 검사를 해줘야하니 총 걸린시간을 초단위로 바꾸자.
2. 시침, 분침, 초침의 초당 움직이는 각도를 구하자.

  • 시침, 초침과 분침, 초침이 겹쳤는지를 확인하려면 1초마다 각도를 확인하면 됩니다.
  • 현재 시간 기준으로 초침의 각도가 시침의 각도보다 작고, 1초 뒤에 시침의 각도보다 크거나 같다면 겹쳐진 것이므로 카운트를 1 증가합니다 분침도 마찬가지입니다.
  • 다만 3가지가 겹쳤을 경우에는 1번만 카운트 해야 하므로 1을 빼줍니다.

 

시간을 아예 초단위로 변경후 while문 안에서 1초마다 증가하며 초침이 시침/분침을 만나면 카운트를++해준다

 

function solution(h1, m1, s1, h2, m2, s2) {
  //초당 움직이는 초침, 분침, 시침 각도
  const SECONDS_ANGLE = 360 / 60;
  const MINUTES_ANGLE = 360 / 60 / 60;
  const HOURS_ANGLE = 360 / 12 / 60 / 60;

  let cnt = 0;

  //시작 시간 초단위
  let start_time = h1 * 60 * 60 + m1 * 60 + s1;

  //끝나는 시간 초단위
  let end_time = h2 * 60 * 60 + m2 * 60 + s2;

  const start_hour_angle = (start_time * HOURS_ANGLE) % 360;
  const start_minute_angle = (start_time * MINUTES_ANGLE) % 360;
  const start_seconds_angle = (start_time * SECONDS_ANGLE) % 360;

  //시작 시간에 초침과 시침이 겹쳐있으면 카운트
  if (start_seconds_angle === start_hour_angle) cnt++;
  //시작 시간에 초침과 분침이 겹쳐있으면 카운트
  if (start_seconds_angle === start_minute_angle) cnt++;
  //시작 시간에 시침, 분침, 초침 3가지가 겹쳐있는 경우 -1
  if (
    start_seconds_angle === start_hour_angle &&
    start_seconds_angle === start_minute_angle
  )
    cnt--;

  //1초마다 겹쳤는지 확인
  //현재 초침의 각도가 시침, 분침 각도보다 작다가 1초 뒤에 크거나 같으면 겹쳐진 것
  while (start_time < end_time) {
    //현재 시간 시침, 분침, 초침 각도
    const current_hours_angle = (start_time * HOURS_ANGLE) % 360;
    const current_minutes_angle = (start_time * MINUTES_ANGLE) % 360;
    const current_seconds_angle = (start_time * SECONDS_ANGLE) % 360;

    //1초 뒤
    const next_time = start_time + 1;

    //1초 뒤 시침, 분침, 초침 각도
    //360도일 때 모듈러 연산을 하면 0이 되므로 0일 경우에는 360으로 설정해줌
    const next_hours_angle = (next_time * HOURS_ANGLE) % 360 || 360;
    const next_minutes_angle = (next_time * MINUTES_ANGLE) % 360 || 360;
    const next_seconds_angle = (next_time * SECONDS_ANGLE) % 360 || 360;

    //초침이 시침 뒤에 있었는데 1초 뒤에 시침 앞으로 가면 카운트+1
    if (
      current_seconds_angle < current_hours_angle &&
      next_seconds_angle >= next_hours_angle
    )
      cnt++;
    //초침이 분침 뒤에 있었는데 1초 뒤에 분침 앞으로 가면 카운트+1
    if (
      current_seconds_angle < current_minutes_angle &&
      next_seconds_angle >= next_minutes_angle
    )
      cnt++;
    //1초 뒤에 시침 분침 초침 3개가 겹치면 -1(0시, 12시)
    if (
      next_seconds_angle === next_hours_angle &&
      next_seconds_angle === next_minutes_angle
    )
      cnt--;

    //시작 시간 업데이트
    start_time = next_time;
  }

  return cnt;
}