코딩테스트 JS Union-Find: 연결 성분 수와 크기 구하기

2026.09.10·수정 2026.09.13·약 10분·작성: 해비·블로그 소개

[창작 문제] 이동 실험실의 연결 일지

장치 연결 요청마다 남은 네트워크 수와 요청 첫 장치의 네트워크 크기를 기록합니다. 중복 요청과 자기 연결은 새로운 합병이 아닙니다.

유니온 파인드의 개념을 표현한 표지 일러스트
주제를 표현한 개념 표지입니다. 정확한 동작과 값의 변화는 아래 코드와 표에서 확인하세요.

이 문제는 BlogFlow 자료구조 학습용으로 독립 창작했습니다. 외부 문제의 지문이나 테스트를 옮기지 않았습니다.

문제

이동 실험실은 n개의 장치를 처음에는 서로 분리해 둡니다. 관리자가 links의 순서대로 두 장치를 연결합니다. 각 요청을 처리한 직후 [전체 연결 성분 수, 요청 첫 장치가 속한 성분의 크기]를 일지에 남기세요. 이미 연결된 두 장치를 다시 연결하거나 같은 장치를 양쪽에 적은 요청도 일지 한 줄은 남겨야 합니다.

관련 구현 설명: JavaScript Union-Find: 경로 압축과 크기 합치기로 연결 상태 관리하기

입력·출력과 제약

JSON 객체 {n, links}를 표준 입력으로 받습니다. links의 각 원소 [a,b]는 두 장치 번호입니다.

요청 순서대로 두 정수를 담은 배열을 모아 JSON 배열 하나로 출력합니다. links가 비면 []입니다.

0 ≤ n ≤ 100,000, 0 ≤ links.length ≤ 100,000. 모든 번호는 정수이며 0 ≤ a,b < n입니다. n=0이면 links는 빈 배열입니다.

입력은 위 계약을 만족한다고 가정합니다. 온라인 채점기는 제공하지 않으며 로컬 Node.js에서 JSON 입력으로 실행합니다.

예시

예시 1 입력:

{"n":5,"links":[[0,1],[2,3],[1,2],[0,3],[4,4]]}

예시 1 출력:

[[4,2],[3,2],[2,4],[2,4],[2,1]]

예시 2 입력:

{"n":0,"links":[]}

예시 2 출력:

[]

힌트

성공한 합병 횟수만 전체 성분 수에서 빼세요. 성분 크기는 요청한 원소의 현재 대표를 통해 읽습니다.

정답과 해설

정답 코드와 해설 펼치기

Node.js 24 CommonJS용 전체 풀이입니다. union-find-problem.cjs로 저장하세요. 클래스 선언, solve 함수, 표준 입력 처리가 모두 들어 있어 가이드 파일을 별도로 가져오지 않습니다. 예시 입력을 input.json에 저장하고 터미널에서 실행합니다.

Windows PowerShell:
Get-Content -Raw -Encoding UTF8 .input.json | node .union-find-problem.cjs

macOS / Linux:
node union-find-problem.cjs < input.json
class UnionFind {
  constructor(n) {
    if (!Number.isInteger(n) || n < 0 || n > 1_000_000) {
      throw new RangeError('n은 0~1,000,000 정수여야 합니다.');
    }
    this.parent = Array.from({ length: n }, (_, i) => i);
    this.sizes = Array(n).fill(1);
    this.count = n;
  }
  check(x) {
    if (!Number.isInteger(x) || x < 0 || x >= this.parent.length) {
      throw new RangeError('원소 번호가 범위를 벗어났습니다.');
    }
  }
  find(x) {
    this.check(x);
    let root = x;
    while (this.parent[root] !== root) root = this.parent[root];
    while (this.parent[x] !== x) {
      const next = this.parent[x];
      this.parent[x] = root;
      x = next;
    }
    return root;
  }
  union(a, b) {
    let ra = this.find(a), rb = this.find(b);
    if (ra === rb) return false;
    if (this.sizes[ra] < this.sizes[rb]) [ra, rb] = [rb, ra];
    this.parent[rb] = ra;
    this.sizes[ra] += this.sizes[rb];
    this.count--;
    return true;
  }
  same(a, b) { return this.find(a) === this.find(b); }
  sizeOf(x) { return this.sizes[this.find(x)]; }
}

function solve({ n, links }) {
  const d = new UnionFind(n);
  return links.map(([a, b]) => {
    d.union(a, b);
    return [d.count, d.sizeOf(a)];
  });
}

module.exports = { solve };

if (require.main === module) {
  const input = JSON.parse(require('node:fs').readFileSync(0, 'utf8'));
  console.log(JSON.stringify(solve(input)));
}

links.map의 각 순회가 일지 한 줄에 대응합니다. union이 false여도 sizeOf(a)는 계산해야 합니다. 처음 두 요청으로 크기 2인 성분 두 개가 생기고 세 번째 요청으로 크기 4가 됩니다. 네 번째는 같은 성분 안의 중복 요청이므로 [2,4], 다섯 번째는 혼자 남은 장치 4의 자기 연결이므로 [2,1]입니다.

parent가 만드는 구조는 여러 개의 뿌리 있는 나무입니다. 루트만 parent[root]===root이고, 나머지는 같은 성분 안의 조상을 가리킵니다. 루트에서만 sizes[root]가 정확한 성분 크기입니다. 합쳐진 옛 루트의 sizes는 남아 있어도 더 이상 의미가 없으므로 sizeOf는 반드시 find를 먼저 호출합니다.

크기 합치기는 작은 나무의 루트를 큰 나무의 루트 아래에 붙입니다. 어떤 원소의 깊이가 1 증가했다면 그 원소가 속한 성분의 크기는 적어도 2배가 됩니다. 크기는 n을 넘지 않으므로 깊이는 O(log n)입니다. 합치기를 원소 a,b에 직접 적용하지 않고 대표 ra,rb에 적용해야 성분 일부만 떼어 붙이는 실수를 피할 수 있습니다.

초기화 O(n), q개 요청의 총 시간 O(n+qα(n)) 상환, 구조 O(n)와 반환 일지 O(q) 공간입니다. 개별 요청의 최악은 O(log n)입니다.

표본과 경계를 직접 확인하려면 다음 코드를 union-find-check.cjs로 저장하고 같은 폴더에서 node union-find-check.cjs를 실행하세요. assert가 맞으면 출력 없이 종료하고, 다르면 예외와 함께 실패합니다.

const assert = require('node:assert/strict');
const { solve } = require('./union-find-problem.cjs');

assert.deepEqual(
  solve({
  "n": 5,
  "links": [
    [
      0,
      1
    ],
    [
      2,
      3
    ],
    [
      1,
      2
    ],
    [
      0,
      3
    ],
    [
      4,
      4
    ]
  ]
}),
  [[4,2],[3,2],[2,4],[2,4],[2,1]],
);
assert.deepEqual(
  solve({
  "n": 0,
  "links": []
}),
  [],
);

연결 학습과 공식 자료

JavaScript Union-Find: 경로 압축과 크기 합치기로 연결 상태 관리하기에서 연산별 이유와 전체 복잡도를 이어서 확인할 수 있습니다. 다른 형식의 공식 연습은 AtCoder A – Disjoint Set Union를 참고하세요. 외부 문제의 정답이나 지문은 이 페이지에 복제하지 않았습니다.

공식 자료 확인일: 2026년 9월 10일. 구현은 이 글의 JavaScript 계약에 맞춰 독립적으로 작성했습니다. 다른 언어 라이브러리의 API와 숫자 범위가 그대로 적용되는 것은 아닙니다.

풀이 전에 확인할 순서

  1. 입력값과 출력값을 한 문장으로 다시 적습니다.
  2. 반복할 대상과 비교·저장할 값을 정합니다.
  3. 필요한 자료구조와 시간복잡도를 예상합니다.
  4. 코드를 보기 전에 손으로 작은 예제를 계산합니다.
힌트: 코딩테스트 JS Union-Find: 연결 성분 수와 크기 구하기

정답 코드를 바로 따라 쓰기보다, 본문에서 값이 갱신되는 조건과 반복 범위를 먼저 찾으세요. 반복 한 번마다 반드시 유지되어야 하는 값이 무엇인지 적으면 풀이의 중심 변수를 고르기 쉽습니다.

테스트 확인

  • 가능한 가장 작은 입력
  • 같은 값이나 문자가 반복되는 입력
  • 정답이 처음 또는 마지막 위치에서 결정되는 입력
  • 입력 제한에 가까운 경우의 실행 시간

확인 결과: 본문의 예제뿐 아니라 위 경계 사례에서도 예상값과 실제 출력이 같아야 풀이가 완료됩니다.

이 글이 도움이 되었나요?

조회 중

코딩테스트 JavaScript 학습 순서

필수 49개 · 전체 49개

읽음 기록 관리

전체 과정 목차 (49개)
  1. 필수 길잡이 · 코딩테스트 JS 자료구조 로드맵: 배열, 해시, 스택, 투 포인터 순서
  2. 필수 학습 · 세 수 중 최솟값 JavaScript 조건문 풀이 정리
  3. 필수 학습 · 삼각형 판별하기 JavaScript 풀이
  4. 필수 학습 · 연필 개수 JavaScript 풀이
  5. 필수 학습 · 1부터 N까지 합 출력하기 JavaScript 풀이
  6. 필수 학습 · 최솟값 구하기 JavaScript 풀이|배열 순회와 비교 갱신 원리
  7. 필수 학습 · 홀수 JavaScript 풀이: 조건 판별과 결과 처리 정리
  8. 필수 학습 · 10부제 JavaScript 풀이: 끝자리 비교로 위반 차량 수 세기
  9. 필수 학습 · A를 #으로 JavaScript 풀이: 문자열 순회와 치환
  10. 필수 학습 · 문자 찾기 JavaScript 풀이: 문자열 순회로 개수 세기
  11. 필수 학습 · 대문자 찾기 JavaScript 풀이
  12. 필수 학습 · 대문자로 통일 JavaScript 풀이
  13. 필수 학습 · 대소문자 변환 JavaScript 풀이
  14. 필수 학습 · 일곱 난쟁이 JavaScript 풀이: 두 명을 제외하는 완전탐색
  15. 필수 학습 · 코딩테스트 JS Map 풀이: 학급 회장 득표수 세기
  16. 필수 학습 · 코딩테스트 JS 스택 풀이: 올바른 괄호 검증하기
  17. 필수 학습 · 코딩테스트 JS 스택 풀이: 괄호문자 제거하기
  18. 필수 학습 · 코딩테스트 JS 스택 풀이: 크레인 인형뽑기 처리법
  19. 필수 학습 · 코딩테스트 JS 스택 풀이: 후위식 연산 계산하기
  20. 필수 학습 · 코딩테스트 JS 스택 풀이: 쇠막대기 레이저 절단 개수 세기
  21. 필수 학습 · 코딩테스트 JS 투 포인터 풀이: 두 정렬 배열 합치기
  22. 필수 학습 · 코딩테스트 JS 투 포인터 풀이: 공통 원소 추출하기
  23. 필수 학습 · 코딩테스트 JS 슬라이딩 윈도우 풀이: 최대 매출 구간 합 계산하기
  24. 필수 학습 · JavaScript 투 포인터: 합이 M인 연속 부분수열 개수
  25. 필수 학습 · 코딩테스트 JS 해시 풀이: 모든 아나그램 찾기
  26. 필수 학습 · 가장 긴 문자열 JavaScript 풀이
  27. 필수 학습 · 가운데 문자 출력 JavaScript 풀이
  28. 필수 학습 · 중복문자제거 JavaScript 풀이
  29. 필수 학습 · 코딩테스트 JS 고급: 최소 힙으로 다익스트라 최단 경로 구하기
  30. 필수 학습 · 코딩테스트 JS Union-Find: 연결 성분 수와 크기 구하기 현재 글
  31. 필수 학습 · 코딩테스트 JS Trie: 접두사에 맞는 단어 수 세기
  32. 필수 학습 · 코딩테스트 JS Fenwick Tree: 값 갱신과 구간 합 처리
  33. 필수 학습 · 코딩테스트 JS 세그먼트 트리: 단일 대입과 구간 합
  34. 필수 학습 · 코딩테스트 JS LRU 캐시: 지도 타일 재사용 기록
  35. 필수 학습 · 코딩테스트 JS AVL 트리: 기준 이상 최솟값 찾기
  36. 필수 학습 · 코딩테스트 JS 큐: 상담 창구 대기열 명령 처리
  37. 필수 학습 · 코딩테스트 JS 연결 리스트: 재생 대기 목록 관리
  38. 필수 학습 · JavaScript 원형 덱 연습: 최근 기록 창과 되돌리기
  39. 필수 학습 · JavaScript 해시 테이블 연습: 정규화 문자열 빈도와 등장 순서
  40. 필수 학습 · JavaScript 트리 순회 연습: 깊이별 노드 묶기
  41. 필수 학습 · JavaScript BST 연습: 닫힌 구간의 중복 키 보고서
  42. 필수 학습 · JavaScript 최소 힙 연습: 동률 순서를 지키는 작업 스케줄러
  43. 필수 학습 · JavaScript 그래프 연습: 연결 구역 크기를 작은 순서로 출력하기
  44. 필수 학습 · 중복단어제거 JavaScript 풀이
  45. 필수 학습 · TypeScript 이진 탐색 연습: 숫자 카드 존재 여부 확인
  46. 필수 학습 · 큰 수 출력하기 JavaScript 풀이
  47. 필수 학습 · 보이는 학생 JavaScript 풀이
  48. 필수 학습 · 가위바위보 JavaScript 풀이
  49. 필수 학습 · 점수계산 JavaScript 풀이

새 글 받아보기

RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.

RSS 피드 구독하기

댓글 남기기