테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

더 나아가기 문제

개념마다 확인(뜻을 맞게 알았나) → 추적(손으로 한 단계씩) → 응용(처음 보는 상황) 순서로 푼다. 풀이 보기를 누르기 전에 종이에 먼저 적어 보자.

목차 (5)
  1. LRU 캐시
  2. 스킵 리스트
  3. HyperLogLog
  4. P와 NP
  5. 근사 알고리즘

LRU 캐시

확인

칸이 3개인 LRU 캐시의 리스트가 앞(가장 최근)부터 C, B, A다. 여기에 put(D)를 하면 무엇이 빠지는가? 그리고 노드에 값뿐 아니라 키까지 적어 두는 이유는 무엇인가?

LRU 리스트 C, B, A. A가 가장 오래 안 쓴 것이다리스트CBA
지금 리스트. 왼쪽이 head 쪽(가장 최근), 오른쪽이 tail 쪽이다.
풀이 보기

A가 빠지고 리스트는 D, C, B가 된다. tail 바로 앞 노드가 가장 오래 안 쓴 것이다.

put(D) 뒤의 LRU 리스트 D, C, B. A는 내보냈다전CBAput(D) 후DCB
put(D) 뒤. A를 내보내고 D를 맨 앞에 넣었다.

내보낼 때는 리스트에서 노드를 떼는 것으로 끝나지 않고 맵에서도 지워야 한다. 그런데 손에 든 것은 tail 앞 노드뿐이라, 그 노드가 어떤 키였는지 알아야 맵에서 O(1)에 지울 수 있다. 키가 없으면 맵 전체를 뒤져야 해서 O(n)이 된다.

추적

칸이 2개인 LRU 캐시에 아래 요청을 차례로 보낸다. get이 적중한 횟수와 마지막 리스트 순서, 내보낸 키를 차례로 적어라(get이 실패하면 캐시는 바뀌지 않는다).

요청 put A, put B, get A, put C, get B, put D, get A, get C명령put0put1get2put3get4put5get6get7키A0B1A2C3B4D5A6C7
요청 여덟 개. 위가 명령, 아래가 키다.
풀이 보기

적중 2번(get A, get C), 내보낸 키는 B → A, 마지막 리스트는 C, D다. 세 번째 요청 get(A)가 A를 앞으로 올렸기 때문에 put(C)에서는 B가 빠진다. 그래서 get(B)는 실패한다.

LRU 캐시 단계 그림: 칸 두 개에 요청 여덟 개를 보내 B와 A가 차례로 빠지고, get A와 get C만 적중한다해시 맵이중 연결 리스트: head 쪽이 가장 최근, tail 쪽이 가장 오래 안 쓴 것headtail빈 캐시ABCDput(A)ABCDAput(B)ABCDBAget(A) → 적중ABCDABput(C) → B 내보냄ABCDCA−Bget(B) → 실패ABCDCAput(D) → A 내보냄ABCDDC−Aget(A) → 실패ABCDDCget(C) → 적중ABCDCD이번 요청이 건드린 노드맵에 없는 키−B= 방금 내보낸 키
1 / 9

빈 캐시. 칸은 2개다. 리스트 앞이 가장 최근에 쓴 것, 뒤가 가장 오래 안 쓴 것이다.

칸이 2개인 LRU 캐시. 쓴 노드는 맨 앞으로, 꽉 차면 맨 뒤를 뺀다.

응용

칸이 3개인 LRU 캐시가 A, B, C, D, A, B, C, D 순서로 반복 접근을 받는다. 없으면 원본에서 읽어 put한다. 적중은 몇 번인가? 칸을 4개로 늘리면 어떻게 되는가?

접근 순서 A, B, C, D, A, B, C, D접근A0B1C2D3A4B5C6D7
네 키를 차례로 되풀이하는 접근(스캔).
풀이 보기

칸 3개면 적중 0번, 칸 4개면 둘째 바퀴 네 번이 모두 적중한다. 칸 3개에서는 매번 다음에 쓸 키가 바로 가장 오래 안 쓴 키라서 하필 그것을 내보낸다.

LRU 캐시 단계 그림: 칸 세 개에 네 키를 되풀이해 넣으면 넣을 때마다 다음에 쓸 키가 빠진다해시 맵이중 연결 리스트: head 쪽이 가장 최근, tail 쪽이 가장 오래 안 쓴 것headtail빈 캐시ABCDput(A)ABCDAput(B)ABCDBAput(C)ABCDCBAput(D) → A 내보냄ABCDDCB−Aput(A) → B 내보냄ABCDADC−Bput(B) → C 내보냄ABCDBAD−Cput(C) → D 내보냄ABCDCBA−Dput(D) → A 내보냄ABCDDCB−A이번 요청이 건드린 노드맵에 없는 키−B= 방금 내보낸 키
1 / 9

빈 캐시. 칸은 3개다. 리스트 앞이 가장 최근에 쓴 것, 뒤가 가장 오래 안 쓴 것이다.

칸 3개, 키 4개를 되풀이할 때. put마다 다음 차례의 키를 내보내 적중이 한 번도 없다.

작업 집합이 칸보다 하나만 커도 LRU는 적중률 0%로 무너진다. 이런 순환·스캔 접근에서는 가장 최근 것을 내보내는 MRU나 무작위 교체가 오히려 낫고, 2Q·ARC 같은 변형은 한 번만 쓰인 것을 따로 두어 이 문제를 줄인다.

스킵 리스트

확인

노드 16개를 넣은 스킵 리스트에서 층을 정하는 동전의 앞면 확률이 1/2이다. k층 이상에 오르는 노드 수의 기댓값을 1층부터 5층까지 구하라. 노드 하나가 갖는 층 수의 기댓값은 얼마인가?

층 1부터 5까지와 비어 있는 기대 노드 수층12345기대 노드 수?????
층마다 기대 노드 수를 채워 보라.
풀이 보기

16, 8, 4, 2, 1개이고, 노드 하나의 기대 층 수는 2다. k층 이상에 오르려면 앞면이 k − 1번 이어져야 하므로 확률이 1/2^(k−1)이고, 16을 곱하면 된다.

층 1부터 5까지의 기대 노드 수 16, 8, 4, 2, 1층12345기대 노드 수168421
층이 하나 오를 때마다 절반씩 준다.

기대 노드 수가 1이 되는 층이 log₂ 16 + 1 = 5층이라 층 수는 log n 근처다. 기대 층 수는 1 + 1/2 + 1/4 + … = 2라 포인터는 평균 2n개, 공간은 O(n)이다.

추적

아래 스킵 리스트에서 27을 찾는다. 지나는 노드를 차례로 적고, 오른쪽 이동과 층 내려가기가 각각 몇 번인지 세어라. 맨 아래층만 걸으면 오른쪽 이동은 몇 번인가?

키 2, 5, 9, 14, 20, 27, 33, 41과 층 수 2, 1, 3, 1, 2, 1, 4, 1키205192143204275336417층 수2011321324154617
키와 노드마다의 층 수. 머리는 가장 높은 4층까지 있다.
풀이 보기

머리(4층) → 9(3층) → 20(2층) → 27(1층)이고, 오른쪽 이동 2번, 내려가기 3번이다. 층마다 다음 노드 33이 27보다 커서 내려가고, 1층에서 다음 노드가 바로 27이라 찾는다. 맨 아래층만 걸으면 27보다 작은 노드 다섯 개를 지나 오른쪽 이동이 5번이다.

스킵 리스트 탐색 단계 그림: 네 층짜리 리스트에서 9와 20을 거쳐 층을 내려가며 27에 닿는다찾는 값 271층끝2층끝3층끝4층끝225999142020273333333341지나온 길지금 위치오른쪽 이동 2번 (맨 아래층만 걸으면 5번)
1 / 7

찾는 값: 27. 가장 높은 층의 머리에서 출발한다.

스킵 리스트에서 27 찾기. 층마다 33을 만나면 내려간다.

응용

같은 스킵 리스트에 30을 넣는다. 동전을 던져 30의 층 수는 2로 정해졌다. 30을 찾는 경로에서 층마다 마지막으로 머문 노드는 무엇이고, 어느 노드의 몇 층 포인터를 바꿔야 하는가?

키 2, 5, 9, 14, 20, 27, 33, 41과 층 수 2, 1, 3, 1, 2, 1, 4, 1. 27과 33 사이에 30을 넣는다키205192143204275336417층 수2011321324154617
넣기 전의 키와 층 수. 30은 27과 33 사이에 들어간다.
풀이 보기

층마다 마지막 노드는 4층 머리, 3층 9, 2층 20, 1층 27이고, 바꿀 포인터는 27의 1층과 20의 2층이다. 30의 층 수가 2라 아래 두 층만 고친다. 30의 1층·2층 포인터는 둘 다 33을 가리키고, 27의 1층과 20의 2층은 30을 가리키게 된다.

스킵 리스트 탐색 단계 그림: 30을 찾으면 9, 20, 27을 거쳐 맨 아래층에서 33을 만나 없다고 끝난다찾는 값 301층끝2층끝3층끝4층끝225999142020273333333341지나온 길지금 위치오른쪽 이동 3번 (맨 아래층만 걸으면 6번)
1 / 8

찾는 값: 30. 가장 높은 층의 머리에서 출발한다.

스킵 리스트에서 30 찾기. 없다고 끝나는 자리가 곧 넣을 자리다. 층마다 내려가기 직전의 노드를 기억해 둔다.

넣기는 찾기 한 번에 포인터 몇 개를 바꾸는 것이라 찾기와 같은 평균 O(log n)이다. 균형 트리처럼 회전하거나 다른 노드의 층을 고칠 일이 없다.

HyperLogLog

확인

통이 4개(앞 2비트로 고름)인 HyperLogLog에 해시가 아래 다섯 개인 값을 넣는다(설명을 위해 8비트로 줄였다). 값마다 통 번호와 rank(나머지 6비트에서 첫 1이 나온 자리)를 구하고, 마지막 레지스터 네 개를 적어라.

해시 비트 01001101, 11100000, 01000110, 00010010, 11000001값 10011020314150617값 21011120304050607값 30011020304151607값 40001021304051607값 51011020304050617
값 다섯 개의 해시 비트. 파랑이 통을 고르는 앞 2비트, 빨강이 첫 1 앞의 0들이다.
풀이 보기

(통, rank)는 (1, 3), (3, 1), (1, 4), (0, 2), (3, 6)이고, 레지스터는 [2, 4, 0, 6]이다. rank는 0의 개수 + 1이다. 통마다 가장 큰 rank만 남기므로 통 1은 3과 4 중 4, 통 3은 1과 6 중 6이다. 통 2에는 아무 값도 오지 않아 0이다.

레지스터 2, 4, 0, 6레지스터20410263
통마다 가장 큰 rank. 칸 아래 숫자가 통 번호다.

같은 값을 다시 넣으면 해시가 같아 같은 통에 같은 rank가 나오므로 레지스터가 바뀌지 않는다. 중복이 저절로 무시되는 이유다.

추적

통 4개의 레지스터가 [2, 0, 3, 1]이다. 추정식 E = α·m² / Σ 2^(−r)로 원시 추정값을 구하라(m = 4, α = 0.7213 / (1 + 1.079 / m)). 원시 추정값이 2.5m 이하이고 빈 통이 있으면 m·ln(m / 빈 통 수)로 바꾼다. 최종 추정값은 얼마인가?

레지스터 2, 0, 3, 1. 통 1이 비어 있다레지스터20013213
통 네 개의 레지스터.
풀이 보기

원시 추정값은 약 4.85이고, 빈 통 보정으로 최종 추정값은 4·ln 4 ≈ 5.55다.

  • Σ 2^(−r) = 1/4 + 1 + 1/8 + 1/2 = 1.875
  • α = 0.7213 / (1 + 1.079 / 4) ≈ 0.5681
  • 원시 E = 0.5681 × 16 / 1.875 ≈ 4.85
  • 4.85 ≤ 2.5 × 4 = 10이고 빈 통이 1개 → 4 · ln(4 / 1) ≈ 5.55

값이 적으면 빈 통이 많아 조화 평균 식이 크게 틀어진다. 그때는 "공을 m개 통에 던졌을 때 빈 통이 이만큼 남으려면 몇 개를 던졌어야 하나"로 세는 linear counting이 더 정확하다.

응용

서버 두 대가 같은 해시로 통 16개짜리 HyperLogLog를 따로 채웠다. 두 서버를 합친 방문자 수를 추정하려면 어떻게 하는가? 합친 레지스터와 추정값(m = 16이면 α = 0.673), 표준 오차를 구하라.

서버 1의 레지스터 16개와 서버 2의 레지스터 16개서버 110312213244516273819210111512213114315서버 220112233142526171849210211112313214115
서버 두 대의 레지스터. 칸 아래 숫자가 통 번호다.
풀이 보기

통마다 큰 값을 골라 합치고, 추정값은 약 61.9명, 표준 오차는 1.04/√16 = 26%다.

합친 레지스터 2, 3, 2, 3, 2, 4, 2, 2, 3, 4, 2, 2, 5, 3, 2, 3서버 110312213244516273819210111512213114315서버 220112233142526171849210211112313214115합침20312233244526273849210211512313214315
통마다 큰 쪽을 고른 합집합의 레지스터.

Σ 2^(−r) = 2.78125라 E = 0.673 × 256 / 2.78125 ≈ 61.9다. 빈 통이 없어 보정은 하지 않는다. 같은 방문자는 두 서버에서 같은 통에 같은 rank를 내므로, 큰 값을 고르면 한 대가 모든 방문을 본 것과 똑같은 레지스터가 된다. 서버별 추정값(약 36.5와 34.0)을 더하면 겹친 방문자를 두 번 센다.

통 16개는 연습용이라 오차가 크다. Redis처럼 통 16384개를 쓰면 1.04/√16384 ≈ 0.81%다.

P와 NP

확인

부분집합 합 문제: 수 [12, 4, 9, 7, 15, 2]에서 몇 개를 골라 합이 23이 되게 할 수 있는가? 누군가 증거(고른 위치)로 ① 0·2·5번, ② 1·3·4번, ③ 3·3·2번을 건넸다. 각각 받아들이는가?

수 12, 4, 9, 7, 15, 2수12041927315425
수 여섯 개. 칸 아래 숫자가 위치다.
풀이 보기

①만 받아들인다. ① 12 + 9 + 2 = 23으로 맞다. ② 4 + 7 + 15 = 26이라 틀렸다. ③ 7 + 7 + 9 = 23이지만 3번 원소를 두 번 썼으므로 부분집합이 아니다.

증거 0, 2, 5번은 합 23으로 통과, 1, 3, 4번은 합 26으로 거절① 0·2·5번12041927315425② 1·3·4번12041927315425
증거별 확인. 진한 칸이 받아들인 증거, 빨간 칸이 거절한 증거다.

확인은 위치가 겹치지 않고 범위 안인지 보고 많아야 n번 더하면 끝나는 다항 시간이다. "증거를 받으면 빨리 확인할 수 있다"가 NP의 정의이므로, 증거에 대한 규칙(서로 다른 원소)까지 확인기가 빠짐없이 검사해야 한다.

추적

같은 수 [12, 4, 9, 7, 15, 2]에서 합이 23인 부분집합을 모두 시험해 찾는다. 시험하는 부분집합은 몇 개이고, 답은 무엇인가? 수가 60개면 몇 개를 시험해야 하는가?

수 12, 4, 9, 7, 15, 2. 각 칸은 고르거나 고르지 않는다수12041927315425고름??0?1?2?3?4?5
각 수를 고르거나 고르지 않는 두 갈래가 여섯 번 이어진다.
풀이 보기

2⁶ = 64개를 시험하고, 답은 {12, 4, 7}과 {12, 9, 2} 두 개다. 수가 60개면 2⁶⁰ ≈ 1.15 × 10¹⁸개라, 초당 10억 개를 시험해도 30년 넘게 걸린다.

합이 23인 부분집합: 0, 1, 3번(12, 4, 7)과 0, 2, 5번(12, 9, 2)12 + 4 + 71204192731542512 + 9 + 212041927315425
64개 중 합이 23인 두 부분집합.

확인은 n에 비례하는데 이 방법의 풀기는 n이 하나 늘 때마다 두 배가 된다. 수의 크기가 작으면 합을 표로 채우는 동적 계획법(O(n × 목표 합))이 있지만, 목표 합이 입력 길이에 대해 지수적으로 커질 수 있어 다항 시간 풀이는 아니다. 부분집합 합은 NP-완전이다.

응용

정점 덮개 결정 문제: "아래 그래프에 정점 3개 이하로 된 덮개가 있는가?" 누군가 증거로 {1, 2, 4}를 건넸다. 이 증거를 확인하라. 그리고 답이 "있다"인지 "없다"인지 판단하고 근거를 대라.

정점 여섯 개, 간선 여덟 개의 그래프에서 정점 1, 2, 4를 고른 모습012345
삼각형 두 개(0·1·2와 3·4·5)를 간선 1–3, 2–4로 이은 그래프. 테두리 정점이 건네받은 증거다.
풀이 보기

증거 {1, 2, 4}는 간선 3–5를 덮지 못해 거절한다. 그리고 크기 3 이하의 덮개는 없다(최소 덮개는 4개).

증거 1, 2, 4가 덮지 못한 간선 3–5를 빨강으로 표시한 그래프012345
빨간 간선 3–5는 양 끝 모두 증거에 없다.

삼각형은 정점 하나로 세 간선을 다 덮을 수 없어 적어도 두 개가 필요하다. 두 삼각형은 정점을 공유하지 않으니 덮개는 적어도 2 + 2 = 4개다. {0, 1, 3, 4}가 그런 덮개다. 크기 3짜리 부분집합 20개를 모두 시험해도 덮개는 없다.

"있다"는 증거 하나로 빨리 확인되지만, "없다"는 이번처럼 운 좋게 짧은 논증이 있을 때만 쉽게 보인다. 일반적으로 "없다"를 빨리 확인할 증거가 있는지(NP = co-NP인지)도 아직 모른다.

근사 알고리즘

확인

경로 그래프 0–1–2–3–4–5에서 2-근사 알고리즘이 간선 (0, 1), (2, 3), (4, 5)를 골라 정점 여섯 개를 덮개로 냈다. 최적 덮개를 구하지 않고도 최적이 몇 개 이상인지 말할 수 있는가? 그 근거는?

경로 0–1–2–3–4–5에서 근사 알고리즘이 간선 0–1, 2–3, 4–5를 골라 정점 여섯 개를 덮개로 냈다012345
굵은 간선이 근사 알고리즘이 고른 간선이고, 테두리 정점이 근사 덮개다.
풀이 보기

최적은 3개 이상이다. 고른 세 간선은 끝을 공유하지 않으므로, 어떤 덮개든 간선마다 서로 다른 정점을 하나씩은 넣어야 한다. 그래서 근사 덮개 6개는 최적의 2배를 넘지 않는다.

경로 그래프의 최적 덮개 0, 2, 4012345
최적 덮개 중 하나 {0, 2, 4}. 고른 세 간선에 정점이 하나씩 있다.

실제 최적도 3개다({0, 2, 4}나 {1, 3, 4} 등). 이 그래프에서는 근사가 정확히 2배다. 최적값을 모르는 채 하한(고른 간선 수)과 비교해 비율을 보장하는 것이 근사 증명의 틀이다.

추적

2 × 3 격자 그래프의 간선을 (0, 1), (1, 2), (0, 3), (1, 4), (2, 5), (3, 4), (4, 5) 순서로 훑으며 2-근사를 돌린다. 고르는 간선과 덮개를 구하고, 모두 시험해 찾은 최적과 비교하라.

2 × 3 격자 그래프: 정점 여섯 개, 간선 일곱 개012345
2 × 3 격자. 위 줄이 0·1·2, 아래 줄이 3·4·5다.
풀이 보기

고른 간선은 (0, 1), (2, 5), (3, 4)이고 덮개는 정점 6개 전부다. 최적은 3개({0, 2, 4})라 비율은 2다. (1, 2)는 1이, (0, 3)·(1, 4)는 0·1이 이미 덮개에 있어 건너뛰고, (4, 5)도 4·5가 들어간 뒤라 건너뛴다.

2 × 3 격자에서 2-근사는 간선 0–1, 2–5, 3–4의 양 끝 여섯 정점을, 최적은 정점 0, 2, 4를 고른다2-근사(극대 매칭)012345덮개 6개 = 고른 간선 3개 × 2최적(모두 시험)012345덮개 3개근사가 고른 간선(서로 끝을 공유하지 않음)덮개에 넣은 정점
왼쪽은 2-근사(굵은 간선이 고른 간선), 오른쪽은 모든 부분집합을 시험한 최적이다.

응용

2-근사의 "2배"는 더 줄일 수 없는 분석인가? 근사 결과가 최적의 정확히 2배가 되는 그래프를 하나 들어 보라. 가운데 정점 0에 잎 다섯 개가 붙은 별 모양 그래프는 어떤가?

가운데 정점 0에 정점 1부터 5가 붙은 별 모양 그래프012345
별 모양 그래프. 간선은 (0, 1)부터 (0, 5)까지 차례로 훑는다.
풀이 보기

별 모양에서 근사는 {0, 1}(2개), 최적은 {0}(1개)으로 정확히 2배다. 첫 간선 (0, 1)의 양 끝을 넣으면 나머지 간선은 모두 0이 덮는다. 간선 하나뿐인 그래프도 같은 이유로 2배다.

별 모양 그래프에서 2-근사는 정점 0과 1을, 최적은 정점 0 하나만 고른다2-근사(극대 매칭)012345덮개 2개 = 고른 간선 1개 × 2최적(모두 시험)012345덮개 1개근사가 고른 간선(서로 끝을 공유하지 않음)덮개에 넣은 정점
별 모양 그래프의 2-근사와 최적.

이렇게 비율이 정확히 2가 되는 입력이 있으므로 이 알고리즘의 분석은 더 줄일 수 없다(tight). 정점 덮개 전체로 보면 2보다 확실히 작은 상수 비율을 내는 다항 시간 알고리즘은 아직 아무도 찾지 못했다.