Skip to content

Conversation

@bona1122
Copy link
Collaborator

@bona1122 bona1122 commented Feb 15, 2025

[집합]

문제를 풀면서 Set과 서로소집합(Disjoint Set) 알고리즘에 대해 배운 내용과 인사이트를 정리했습니다.

Set의 주요 특징과 활용

  1. 기본 특징
    • 중복을 허용하지 않는 자료구조이다.
    • 삽입, 삭제, 검색이 O(1) 시간복잡도를 가진다.(내부적으로 해시 테이블을 사용하기 때문)
  2. 주요 메서드
    const set = new Set()
    
    set.add(value)    // 원소 추가
    set.delete(value) // 원소 삭제
    set.clear()       // 모든 원소 삭제
    
    set.has(value)    // 검색 -> 포함 여부 확인
    set.values()          // value들의 iterator 반환
    
    set.size          // 원소 개수
  3. 활용
  • 중복값 제거가 필요한 경우
  • 빠른 검색이 필요한 경우

서로소집합 알고리즘과 Union, Find

  • 서로소집합: 교집합이 없는 집합들로 나누어진 원소들을 다루는 알고리즘
  • Find 연산: 특정 원소가 속한 집합의 부모노드를 찾는 연산
  • Union 연산: 두 개의 집합을 하나로 합치는 연산. (두 집합의 부모노드를 연결하는 방식)
  • 각 집합은 트리 구조를 이용해 구현하며, 배열을 이용해서 부모노드를 추적하는 방식으로 관리한다.

서로소 집합 초기화 - parent 배열

처음에는 각 원소가 자기 자신만을 원소로 가지는 서로소 집합으로 시작 -> 원소의 부모는 자기자신

const n = 5 // 원소의 개수
const parent = Array.from({ length: n }, (_, idx) => idx)

Find 연산 구현 방식

const find = (x, parent) => {
  if (parent[x] === x) return x
  return find(parent[x], parent)
}

경로압축을 이용한 Find 최적화

  • 경로압축: find 연산 시 거쳐간 모든 노드가 직접 루트를 가리키도록 하는 방식 => 이후의 연산에서 경로 탐색 비용을 감소시킬 수 있다.
  • 재귀버전이 이해하기 쉽지만, 스택이 깊어져서 문제가 생기는 경우도 있어 반복문 버전도 알아두면 좋을 것 같습니다.
// 재귀 버전
const find = (x, parent) => {
  if (parent[x] === x) return x
  return (parent[x] = find(parent[x], parent))
}

// 반복문 버전
const find = (x, parent) => {
  while (parent[x] !== x) {
    parent[x] = parent[parent[x]]
    x = parent[x]
  }
  return x
}

Union 연산 구현 방식

  • 단순 병합 방식: 구현이 간단하지만 트리밸런싱 문제가 발생할 수 있다. (경로압축방식을 사용한 find와 함께 사용하면 이러한 문제가 보완된다.)
const union = (a, b, parent) => {
  a = find(a, parent)
  b = find(b, parent)
  if (a !== b) parent[b] = a // 항상 첫 번째 루트(a)를 부모로 선택
}
  • 크기 비교 방식: 간단한 방식으로 어느 정도 트리 밸런싱 효과를 얻을 수 있다.
    • '친구 네트워크' 같은 문자열 비교에는 부적절하다.
const union = (a, b, parent) => {
  a = find(a, parent)
  b = find(b, parent)
  if (a < b) parent[b] = a
  else parent[a] = b
}
  • Rank 기반 방식: 트리 높이를 관리하여 효율적인 트리 구조를 만들어, 트리밸런싱 문제를 해결할 수 있다.
const union = (a, b, parent, rank) => {
  a = find(a, parent)
  b = find(b, parent)
  if (rank[a] < rank[b]) {
    parent[a] = b
  } else {
    parent[b] = a
    if (rank[a] === rank[b]) rank[a]++
  }
}

📌 푼 문제


📝 간단한 풀이 과정

대칭 차집합

  • A에만 있는 요소, B에만 있는 요소를 합친 개수를 구하는 문제여서
  • Set을 이용해서 합집합 개수를 구하는 것을 활용하였습니다.
  • 교집합 개수 = 총 개수 - 합집합 갯수 를 통해 교집합에 속하는 개수를 구하고
  • A개수 + B개수 - 2*교집합 개수로 답을 도출했습니다.
// https://www.acmicpc.net/problem/1269

const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

// A-B 와 B-A 의 합집합 구하기.
// A에만 있는거, B에만 있는거 총 개수 구하기

const [an, bn] = input[0].split(" ").map(Number)
const a = input[1].split(" ").map(Number)
const b = input[2].split(" ").map(Number)
const inter = an + bn - new Set([...a, ...b]).size // 총 개수 - 합집합 갯수 = 교집합 개수

log(an + bn - 2 * inter)

서로 다른 부분 문자열의 개수

  • 부분문자열을 모두 확인하기 위해 2중 for문으로 시작인덱스, 끝인덱스를 모두 탐색하는 방식으로 풀이했습니다.
  • 서로 다른 부분 문자열 개수를 구하는 것이므로 중복값을 없애기 위해 set,add를 활용해서 풀었습니다.
// https://www.acmicpc.net/problem/11478

const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim()
const log = console.log

// 서로 다른 부분 문자열의 개수 출력하기
// 길이는 1,000 이하.
const set = new Set()
for (let i = 0; i < input.length; i++) {
  for (let j = i; j < input.length; j++) {
    const sliced = input.slice(i, j + 1)
    set.add(sliced)
  }
}
log(set.size)

소인수분해

  • 이 문제도, 중복값을 없애기 위해 소인수 목록을 구할 때, set을 이용했습니다.
  • 합성수가 항상 자신의 제곱근 이하의 소인수를 최소 하나 가지고 있다는 수학적 성질을 활용하여, 제곱근까지만 반복하도록 하였습니다.
// https://school.programmers.co.kr/learn/courses/30/lessons/120852

// n이 주어질때, n의 소인수를 오름차순으로 담은 배열 리턴하기
function solution(n) {
  const factors = new Set()
  for (let i = 2; i * i <= n; i++) { 
    while (n % i === 0) { // 동일한 소인수로 여러번 나누기 가능
      factors.add(i)
      n /= i
    }
  }
  // 마지막으로 남은 수가 1보다 크면 그 자체가 소수이므로 추가
  if (n > 1) factors.add(n)
  return Array.from(factors)
}

문자열 집합

  • 주어진 문자열들이 특정 집합에 포함되어 있는지 확인하는 문제였기에,
  • Set, has, add 메서드를 활용하여 집합에서 검색을 하는 방식으로 구현했습니다.
// https://www.acmicpc.net/problem/14425

const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

const [N, M] = input[0].split(" ").map(Number)
const S = new Set() 

// N개의 문자열을 집합 S에 추가
for (let i = 1; i <= N; i++) {
  S.add(input[i])
}

let count = 0
// M개의 문자열 각각이 집합 S에 포함되어 있는지 확인
for (let i = N + 1; i <= N + M; i++) {
  if (S.has(input[i])) count++
}

log(count)

집합의 표현

  • 기본적인 서로소집합 알고리즘 문제여서, union, find 를 활용했습니다.
const readline = require("readline")
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
})

const input = []
rl.on("line", (line) => {
  input.push(line)
}).on("close", () => {
  const [n, m] = input[0].split(" ").map(Number)
  const parent = Array.from({ length: n + 1 }, (_, idx) => idx)

  const findParent = (x, parent) => {
    if (parent[x] !== x) {
      parent[x] = findParent(parent[x], parent)
    }
    return parent[x]
  }
  const union = (a, b) => {
    a = findParent(a, parent)
    b = findParent(b, parent)
    if (a < b) parent[b] = a
    else parent[a] = b
  }

  let result = ""
  for (let i = 1; i <= m; i++) {
    let [op, a, b] = input[i].split(" ").map(Number)
    if (op === 0) {
      union(a, b)
    } else {
      if (findParent(a, parent) === findParent(b, parent)) result += "YES\n"
      else result += "NO\n"
    }
  }

  console.log(result.trim())
  process.exit()
})

친구 네트워크

  • 문자열을 키로 갖는 서로소집합을 구현하기 위해 객체를 사용하였다.
  • 집합의 크기를 구하는 문제여서, 집합의 크기를 추적하기 위해 size배열로 관리하였다.
  • 단순 크기 비교를 도입했었는데, 문자열을 비교하게 되니 부적절하여 rank 방식을 도입해봤습니다. 하지만 이 문제의 경우에는 rank 방식보다 (단순구현+경로압축)만으로도 미세하게 더 시간이 효율적이었습니다😅
const filePath =
  process.platform === "linux"
    ? "/dev/stdin"
    : require("path").join(__dirname, "input.txt")
const input = require("fs").readFileSync(filePath).toString().trim().split("\n")
const log = console.log

// 1. 재귀 버전
const find = (x, parent) => {
  if (parent[x] === x) return x
  return (parent[x] = find(parent[x], parent))
}
// 2. 반복문 버전
const find2 = (x, parent) => {
  while (parent[x] !== x) {
    parent[x] = parent[parent[x]]
    x = parent[x]
  }
  return x
}

// 1. 문자열 크기 비교 방식의 union
// - 작은 값을 부모로 선택하여 트리 밸런싱 시도
// - 문제점: 문자열 비교(a < b)는 사전순 비교라서 예측 불가능한 결과 발생
const union1 = (a, b, parent, size) => {
  a = find(a, parent)
  b = find(b, parent)

  if (a < b) {
    parent[b] = a
    size[a] += size[b]
    return size[a]
  } else {
    parent[a] = b
    size[b] += size[a]
    return size[b]
  }
}
// 2. 단순 병합 방식의 union -> 항상 첫 번째 루트(a)를 부모로 선택
// -> 작은 값을 부모로 하는 방식보다 트리밸런싱이 비효율적이지만 find에서 경로압축방식을 함께 사용 시 보완된다.
const union2 = (a, b, parent, size) => {
  a = find(a, parent)
  b = find(b, parent)
  if (a !== b) {
    parent[b] = a
    size[a] += size[b]
  }
  return size[a]
}

// 3. rank 기반 union -> 트리높이를 저장한 rank 배열을 활용하여 트리밸런싱 문제 해결
const union3 = (a, b, parent, size, rank) => {
  a = find(a, parent)
  b = find(b, parent)

  if (a !== b) {
    if (rank[a] < rank[b]) {
      // b의 rank가 더 크면 b를 루트로
      parent[a] = b
      size[b] += size[a]
      return size[b]
    } else {
      // a의 rank가 더 크거나 같으면 a를 루트로
      parent[b] = a
      size[a] += size[b]
      if (rank[a] === rank[b]) {
        rank[a]++ // rank가 같을 때만 증가
      }
      return size[a]
    }
  }
  return size[a]
}

let test = +input[0]
let currentLine = 1

while (test--) {
  const F = +input[currentLine]
  let parent = {}
  let size = {}
  let rank = {}
  const result = []

  for (let i = 1; i <= F; i++) {
    const [a, b] = input[currentLine + i].split(" ")

    for (let name of [a, b]) {
      if (parent[name] === undefined) {
        parent[name] = name
        size[name] = 1
        rank[name] = 0
      }
    }

    result.push(union3(a, b, parent, size, rank))
  }

  log(result.join("\n"))
  currentLine += F + 1
}

@bona1122 bona1122 self-assigned this Feb 15, 2025
Copy link
Collaborator

@Moonjonghoo Moonjonghoo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여러방식으로 풀이해서 개념이해하기 저도편했습니다!
한주고생하셨습니다.

@JooKangsan JooKangsan merged commit 4e18165 into codingTestStd:main Feb 20, 2025
0 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants