백준 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자리는 모두 한수
- 알고리즘: 각 숫자를 자릿수로 분해하여 공차 확인
- 시간 복잡도: O(N × D), 충분히 빠름
배운 점
- 등차수열의 개념을 코드로 구현하는 방법
- 숫자를 자릿수로 분해하는 테크닉
- Edge case (1자리, 2자리) 처리의 중요성
관련 문제
- 백준 4673번: 셀프 넘버 - 비슷한 구현 문제
- 백준 2869번: 달팽이는 올라가고 싶다 - 수학적 사고
- 백준 1193번: 분수찾기 - 수열 패턴
댓글