백준 1065번: 한수 - 등차수열 판별 문제

문제

어떤 양의 정수 X의 각 자리가 등차수열을 이룬다면, 그 수를 한수(等差數, Arithmetic Number)라고 합니다. 등차수열은 연속된 두 개의 수의 차이가 일정한 수열을 말합니다.

N이 주어졌을 때, 1보다 크거나 같고, N보다 작거나 같은 한수의 개수를 출력하는 프로그램을 작성하세요.

입력

  • 첫째 줄에 1018보다 작거나 같은 자연수 N이 주어집니다.

출력

  • 첫째 줄에 1보다 크거나 같고, N보다 작거나 같은 한수의 개수를 출력합니다.

예제

입력 출력 설명
110 99 1~99는 모두 한수, 100~110 중 한수는 없음
1 1 1은 한수
210 105  
1000 144  
500 119  

먼저, 문제 이해하기

“등차수열”이라는 개념이 낯설 수 있습니다. 예를 들어볼까요?

한수인 경우

123 → 각 자리: 1, 2, 3
     → 차이: 2-1=1, 3-2=1 ✅ (공차가 1로 일정)

135 → 각 자리: 1, 3, 5
     → 차이: 3-1=2, 5-3=2 ✅ (공차가 2로 일정)

7 → 각 자리: 7
   → 한 자리 수는 모두 한수 ✅

55 → 각 자리: 5, 5
   → 차이: 5-5=0 ✅ (공차가 0으로 일정)

한수가 아닌 경우

101 → 각 자리: 1, 0, 1
     → 차이: 0-1=-1, 1-0=1 ❌ (공차가 다름)

123456 → 각 자리: 1, 2, 3, 4, 5, 6
        → 차이: 모두 1 ✅ (실제로는 한수)

핵심 통찰

1. 1자리 수와 2자리 수는 항상 한수

왜 그럴까요?

  • 1자리 수: 비교할 다른 자리가 없으므로 자동으로 등차수열
  • 2자리 수: 두 개의 수는 항상 등차수열 (공차는 둘의 차이)
// 1~99는 모두 한수
// 따라서 N이 99 이하면 답은 N
if (N <= 99) {
  return N;
}

2. 3자리 수부터 검사 필요

3자리 수는 100부터 시작합니다. 예를 들어 123의 경우:

  • 첫 번째 공차: 2 - 1 = 1
  • 두 번째 공차: 3 - 2 = 1
  • 두 공차가 같으므로 한수 ✅

하지만 100의 경우:

  • 첫 번째 공차: 0 - 1 = -1
  • 두 번째 공차: 0 - 0 = 0
  • 두 공차가 다르므로 한수 아님 ❌

접근 방법

Step 1: 한수 판별 함수 만들기

function isHansu(num) {
  // 1자리, 2자리는 모두 한수
  if (num < 100) {
    return true;
  }

  // 각 자리 숫자를 배열로 추출
  const digits = num.toString().split('').map(Number);
  // 예: 123 → "123" → ["1","2","3"] → [1,2,3]

  // 첫 번째 공차 계산
  // ㄴ 첫전째와 두번째 자리의 공차
  const difference = digits[1] - digits[0];

  // 모든 연속된 자리의 공차가 같은지 확인
  // ㄴ 반복문이 i = 2 부터 시작하는 이유:
  //    ㄴ digits[0]과 digits[1]의 공차는 이미 difference에 저장됨
  //    ㄴ 이제 digits[1]과 digits[2], digits[2]와 digits[3, ..확인 해야 함
  for (let i = 2; i < digits.length; i++) {
    if (digits[i] - digits[i - 1] !== difference) {
      return false; // 공차가 다르면 한수 아님
    }
  }

  return true; // 모든 공차가 같으면 한수
}

동작 과정 예시 (123):

1. digits = [1, 2, 3]
2. difference = 2 - 1 = 1
3. i=2: digits[2] - digits[1] = 3 - 2 = 1 (difference와 같음 ✅)
4. 반복문 종료 → true 반환

동작 과정 예시 (101):

1. digits = [1, 0, 1]
2. difference = 0 - 1 = -1
3. i=2: digits[2] - digits[1] = 1 - 0 = 1 (difference인 -1과 다름 ❌)
4. false 반환

Step 2: 1부터 N까지 세기

function countHansu(n) {
  let count = 0;

  for (let i = 1; i <= n; i++) {
    if (isHansu(i)) {
      count++;
    }
  }

  return count;
}

전체 코드 (Node.js)

// 백준 1065번: 한수
const fs = require('fs');
const input = fs.readFileSync('/dev/stdin').toString().trim();
const N = parseInt(input);

/**
 * 주어진 숫자가 한수인지 확인
 * @param {number} num - 확인할 숫자
 * @returns {boolean} - 한수이면 true, 아니면 false
 */
function isHansu(num) {
  // 1자리 수와 2자리 수는 모두 한수
  if (num < 100) {
    return true;
  }

  // 숫자를 문자열로 변환하여 각 자리 숫자 추출
  const digits = num.toString().split('').map(Number);

  // 첫 번째 공차 계산
  const difference = digits[1] - digits[0];

  // 모든 연속된 자리수의 차이가 같은지 확인
  for (let i = 2; i < digits.length; i++) {
    if (digits[i] - digits[i - 1] !== difference) {
      return false;
    }
  }

  return true;
}

/**
 * 1부터 N까지의 한수 개수 세기
 * @param {number} n - 상한값
 * @returns {number} - 한수의 개수
 */
function countHansu(n) {
  let count = 0;

  for (let i = 1; i <= n; i++) {
    if (isHansu(i)) {
      count++;
    }
  }

  return count;
}

console.log(countHansu(N));

예제 실행

예제 1: N = 110

한수인 숫자들:
1~99: 모두 한수 (99개)
100: 1,0,0 → 공차: -1, 0 → 한수 아님
101: 1,0,1 → 공차: -1, 1 → 한수 아님
102: 1,0,2 → 공차: -1, 2 → 한수 아님
...
110: 1,1,0 → 공차: 0, -1 → 한수 아님

결과: 99개

예제 2: N = 210

1~99: 99개
100~199:
  - 111: 1,1,1 → 공차: 0, 0 ✅
  - 123: 1,2,3 → 공차: 1, 1 ✅
  - 135: 1,3,5 → 공차: 2, 2 ✅
  - 등등... (6개)

200~210:
  - 210: 2,1,0 → 공차: -1, -1 ✅
  - (0개 추가, 210만 해당)

결과: 99 + 5 + 1 = 105개

시간 복잡도

전체 복잡도: O(N × D)

  • N: 입력값
  • D: 숫자의 자릿수 (최대 18자리)

분석

for (let i = 1; i <= n; i++) {          // O(N)
  if (isHansu(i)) {
    const digits = i.toString()...      // O(D) - 자릿수만큼
    for (let j = 2; j < digits.length)  // O(D)
  }
}

실제 성능:

  • N = 1,000 → 약 1,000 × 4 = 4,000번 연산
  • N = 1,000,000 → 약 1,000,000 × 7 = 7,000,000번 연산

자릿수가 최대 18자리이고, JavaScript는 매우 빠르므로 충분히 시간 내에 해결 가능합니다.

최적화 팁

방법 1: 100 미만 처리

function countHansu(n) {
  // N이 99 이하면 모두 한수
  if (n < 100) {
    return n;
  }

  // 100 이상부터만 검사
  let count = 99; // 1~99는 모두 한수

  for (let i = 100; i <= n; i++) {
    if (isHansu(i)) {
      count++;
    }
  }

  return count;
}

개선 효과:

  • N이 큰 경우, 불필요한 100번의 한수 판별 제거
  • 시간 복잡도: 여전히 O(N × D)

방법 2: 한수 생성 방식 (획기적 개선)

핵심 아이디어: 모든 숫자를 검사하지 말고, 한수를 직접 생성!

복잡도 개선

  • 기존: O(N × D) → N이 10^18이면 불가능
  • 개선: O(D² × 19) → 자릿수(D)와 공차(-9~9)에만 의존
/**
 * 한수를 직접 생성하는 방식
 * 시간 복잡도: O(D² × 19) where D는 최대 자릿수
 */
function countHansuOptimized(n) {
  if (n < 100) return n;

  let count = 99; // 1~99는 모두 한수
  const maxDigits = n.toString().length;

  // 3자리부터 maxDigits자리까지
  for (let digits = 3; digits <= maxDigits; digits++) {
    // 첫 자리 (1~9, 0으로 시작 불가)
    for (let first = 1; first <= 9; first++) {
      // 공차 (-9 ~ 9)
      for (let diff = -9; diff <= 9; diff++) {
        const num = generateHansu(first, diff, digits);

        // 유효한 한수이고 N 이하인 경우만 카운트
        if (num !== null && num <= n) {
          count++;
        }
      }
    }
  }

  return count;
}

/**
 * 첫 자리, 공차, 자릿수로 한수 생성
 * @returns {number|null} 유효하면 한수, 아니면 null
 */
function generateHansu(first, diff, digitCount) {
  let num = first;
  let current = first;

  for (let i = 1; i < digitCount; i++) {
    current += diff;

    // 각 자리는 0~9 범위여야 함
    if (current < 0 || current > 9) {
      return null;
    }

    num = num * 10 + current;
  }

  return num;
}

동작 예시

// 3자리 한수 생성
// first=1, diff=2, digits=3
// → 1 → 1+2=3 → 3+2=5 → 135 ✅

// first=1, diff=5, digits=3
// → 1 → 1+5=6 → 6+5=11 (범위 초과) → null ❌

// first=9, diff=-1, digits=4
// → 9 → 8 → 7 → 6 → 9876 ✅

성능 비교

N 기존 O(N×D) 개선 O(D²×19)
1,000 ~4,000 연산 ~1,000 연산
1,000,000 ~7,000,000 연산 ~2,000 연산
10^18 불가능 ~6,000 연산

방법 3: 공간 복잡도 개선 (O(D) → O(1))

배열을 사용하지 않고 숫자 연산으로 처리:

function isHansuSpaceOptimized(num) {
  if (num < 100) return true;

  // 마지막 두 자리로 첫 공차 계산
  let prev = num % 10;
  num = Math.floor(num / 10);
  let current = num % 10;
  const diff = current - prev;

  // 나머지 자리 검사
  while (num >= 10) {
    prev = current;
    num = Math.floor(num / 10);
    current = num % 10;

    if (current - prev !== diff) {
      return false;
    }
  }

  return true;
}

개선 효과:

  • 공간 복잡도: O(D) → O(1)
  • 배열 생성/변환 오버헤드 제거
  • 문자열 변환 불필요

함정과 주의사항

함정 1: 1자리, 2자리 수 처리 빠뜨리기

// ❌ 잘못된 코드
function isHansu(num) {
  const digits = num.toString().split('').map(Number);
  const difference = digits[1] - digits[0];

  for (let i = 2; i < digits.length; i++) {
    // ...
  }
}

// 문제: 1자리 수는 digits[1]이 undefined!
// 7 → digits = [7] → digits[1] = undefined → difference = NaN

함정 2: 공차가 0인 경우

// 111은 한수일까?
// 각 자리: 1, 1, 1
// 공차: 0, 0 → 모든 공차가 0으로 같으므로 한수! ✅

공차가 0이어도 등차수열입니다. 111, 222, 333 등은 모두 한수입니다.

함정 3: 문자열 처리

// ❌ 잘못된 코드
const digits = num.toString().split(''); // ["1", "2", "3"]
const difference = digits[1] - digits[0]; // "2" - "1" = 1 (우연히 작동)

// 하지만 명시적으로 숫자 변환하는 것이 좋음
const digits = num.toString().split('').map(Number); // [1, 2, 3]

테스트 코드

// 테스트 함수
function test() {
  const testCases = [
    { input: 110, expected: 99 },
    { input: 1, expected: 1 },
    { input: 210, expected: 105 },
    { input: 1000, expected: 144 },
    { input: 500, expected: 119 },
  ];

  testCases.forEach(({ input, expected }) => {
    const result = countHansu(input);
    const pass = result === expected ? '' : '';
    console.log(`${pass} Input: ${input}, Expected: ${expected}, Got: ${result}`);
  });
}

test();

다른 언어로 구현하기

Python

def is_hansu(num):
    if num < 100:
        return True

    digits = [int(d) for d in str(num)]
    difference = digits[1] - digits[0]

    for i in range(2, len(digits)):
        if digits[i] - digits[i-1] != difference:
            return False

    return True

def count_hansu(n):
    return sum(1 for i in range(1, n+1) if is_hansu(i))

n = int(input())
print(count_hansu(n))

정리

핵심 요약

  1. 한수 정의: 각 자리가 등차수열을 이루는 수
  2. 핵심 통찰: 1자리, 2자리는 모두 한수
  3. 알고리즘: 각 숫자를 자릿수로 분해하여 공차 확인
  4. 시간 복잡도: O(N × D), 충분히 빠름

배운 점

  • 등차수열의 개념을 코드로 구현하는 방법
  • 숫자를 자릿수로 분해하는 테크닉
  • Edge case (1자리, 2자리) 처리의 중요성

관련 문제

참고 자료

댓글