본문으로 건너뛰기
김신건의 로그

로프 (Rope)

· 수정 · 📖 약 4분 · 1,361자/단어 #algorithm #data-structure #rope #string #treap
rope, 로프, 문자열 트리, rope data structure

정의

로프 (Rope) 는 문자열을 이진 트리로 표현해 split (분할), concat (연결) 을 O(log N) 에 처리하는 자료구조. 각 리프는 짧은 문자열 조각, 내부 노드는 서브트리의 총 길이 를 메타데이터로 저장.

1995년 Xerox PARC 의 Hans-J. Boehm, Russ Atkinson, Michael Plass 가 대용량 텍스트 편집기를 위해 고안. C++ SGI STL rope<char>, Java StringBuilder 일부 구현, 텍스트 에디터 (VS Code, Sublime) 내부에서 사용.

일반 문자열 (배열) 은 중간 삽입/삭제가 O(N) 이지만 Rope 는 균형 트리 (주로 Treap) 로 O(log N) 보장.

문제 상황과 동기

길이 N=10^6 문자열 “abcd…xyz” 의 k 번째 위치에 “insert” 를 삽입하려면:

  • naive (array): k 이후를 모두 밀어내고 복사. O(N). 10^6 글자면 초당 60fps 편집 불가능.
  • rope: 트리 높이 log N (약 20) 깊이만 순회. split(k) -> concat(left, “insert”, right). O(log N).

핵심 통찰: 문자열 = 연속 메모리 가정을 버리고 트리 = 조각의 계층 연결 로 바꾸면 랜덤 액세스는 느려지지만 편집은 극적으로 빨라진다. Implicit Treap (key 없이 크기만 유지) 응용.

PS 에서는 “쿼리 Q 번 중간 삽입/삭제” 문제에서 Splay Tree 또는 Rope 로 해결.

시각화

핵심 아이디어

Invariant

각 노드는 왼쪽 서브트리의 총 길이 left_size 를 저장. k 번째 문자 접근:

find(k, node):
    if k < node.left_size:
        return find(k, node.left)
    else if k < node.left_size + node.str.length:
        return node.str[k - node.left_size]
    else:
        return find(k - node.left_size - node.str.length, node.right)

O(log N) 깊이 순회.

Split / Concat

split(k): k 번째 위치에서 문자열을 두 트리로 분할. concat(A, B): 두 트리를 연결해 하나의 트리로.

균형 트리 (Treap, Red-Black Tree, AVL Tree) 에서 split/concat 는 O(log N). Implicit Treap 가 가장 간결 (random priority 로 균형).

실무 최적화

  • Leaf chunking: 리프에 8~64 글자씩 묶어 저장. 트리 높이 줄이고 캐시 효율 상승.
  • Lazy propagation: concat 후 즉시 균형 잡지 않고 일정 깊이 초과 시만 재조정.
  • UTF-8 인코딩: 바이트 단위가 아닌 문자 단위 인덱싱 지원.

알고리즘

Implicit Treap 기반 Rope

struct Node:
    str: string            # 이 노드의 문자열 조각
    left_size: int         # 왼쪽 서브트리 총 길이
    priority: int          # Treap 우선순위
    left, right: Node*

split(node, k) -> (L, R):
    if node == null:
        return (null, null)
    if k <= node.left_size:
        (L', R') = split(node.left, k)
        node.left = R'
        node.left_size = node.left_size - k
        return (L', node)
    else:
        (L', R') = split(node.right, k - node.left_size - len(node.str))
        node.right = L'
        return (node, R')

merge(L, R) -> Node:
    if L == null: return R
    if R == null: return L
    if L.priority > R.priority:
        L.right = merge(L.right, R)
        return L
    else:
        R.left = merge(L, R.left)
        R.left_size += size(L)
        return R

insert(root, k, str):
    (L, R) = split(root, k)
    return merge(merge(L, new_node(str)), R)

Treap 의 priority 로 기대 O(log N) 균형.

구현

// Implicit Treap 기반 간략 Rope (삽입/출력만)
#include <iostream>
#include <string>
#include <cstdlib>
using namespace std;
struct Node {
  string s; int sz, pri; Node *l, *r;
  Node(string _s): s(_s), sz(_s.size()), pri(rand()), l(0), r(0) {}
};
int size(Node* n) { return n ? n->sz : 0; }
void upd(Node* n) { if (n) n->sz = size(n->l) + (int)n->s.size() + size(n->r); }
pair<Node*, Node*> split(Node* n, int k) {
  if (!n) return {0, 0};
  int lsz = size(n->l);
  if (k <= lsz) {
      auto [L, R] = split(n->l, k);
      n->l = R; upd(n); return {L, n};
  } else if (k < lsz + (int)n->s.size()) {
      Node *L = new Node(n->s.substr(0, k - lsz));
      Node *R = new Node(n->s.substr(k - lsz));
      L->l = n->l; L->r = 0; upd(L);
      R->l = 0; R->r = n->r; upd(R);
      return {L, R};
  } else {
      auto [L, R] = split(n->r, k - lsz - (int)n->s.size());
      n->r = L; upd(n); return {n, R};
  }
}
Node* merge(Node* L, Node* R) {
  if (!L) return R; if (!R) return L;
  if (L->pri > R->pri) { L->r = merge(L->r, R); upd(L); return L; }
  else { R->l = merge(L, R->l); upd(R); return R; }
}
Node* insert(Node* root, int k, string s) {
  auto [L, R] = split(root, k);
  return merge(merge(L, new Node(s)), R);
}
void print(Node* n) {
  if (n) { print(n->l); cout << n->s; print(n->r); }
}
int main() {
  string base; int q; cin >> base >> q;
  Node* root = new Node(base);
  while (q--) {
      int k; string s; cin >> k >> s;
      root = insert(root, k, s);
  }
  print(root); cout << "\n";
}
stdin
abc 2
3 XY
1 Z
결과
aZbcXY

복잡도

항목
랜덤 액세스 (k번째)O(log N)
split (k 위치 분할)O(log N)
concat (연결)O(log N)
insert / deleteO(log N) - split + merge
공간O(N) + O(log N) 트리 오버헤드

일반 string 대비:

  • 랜덤 액세스: O(1) -> O(log N) (느려짐)
  • 중간 삽입: O(N) -> O(log N) (빨라짐)

변형 / 활용

변형설명
SGI STL ropeC++ rope<char>, deprecated but historical. split/concat 지원
Persistent Rope각 버전의 문자열을 보존. 편집 이력 (undo/redo) 지원
Implicit TreapRope 의 일반화. 배열 연산 (reverse, rotate, insert) O(log N)
Gap Buffer텍스트 에디터의 경량 대안. 커서 근처만 O(1), 멀리 이동 O(N)
Piece TableVS Code / Emacs 의 실제 사용. 원본 버퍼 불변, 편집은 조각 리스트로

함정

1. 랜덤 액세스 느림

배열 O(1) 대비 트리 O(log N) 은 캐릭터 단위 루프가 잦은 코드에서 큰 손실. Rope 는 큰 조각 단위 편집 이 많을 때만 유리.

2. left_size 갱신 실수

split / merge 시 left_size (또는 sz) 를 upd() 안 하면 k 번째 계산이 틀린다. Treap 구현에서 가장 흔한 버그.

3. 메모리 파편화

작은 조각 노드가 많이 생기면 메모리 오버헤드 (32~64 byte/node) 가 크다. 리프 chunking 으로 완화.

4. Persistent 버전 복잡도

path copying 으로 immutable rope 를 만들면 각 편집이 O(log N) 노드 복사. 메모리 증가 주의.

BOJ 연습 문제

번호제목정답률링크
BOJ 13159배열8.7%kokoa-lab
BOJ 14245XOR23.1%kokoa-lab
BOJ 5446용량 부족18.5%kokoa-lab

참고

이 글의 용어 (5개)
레드-블랙 트리 (Red-Black Tree)algorithm
정의 레드-블랙 트리 (Red-Black Tree) 는 각 노드에 색 (빨강/검정) 을 부여하고 5 가지 규약을 지켜 높이 O(log N) 을 보장하는 자가 균형 . 삽입/삭제/…
세그먼트 트리 (Segment Tree)algorithm
정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
자료구조 (Data Structures)algorithm
정의 자료구조 (Data Structure) 는 데이터를 효율적으로 저장하고 접근하기 위한 조직화 방법. 문제 풀이에서는 시간 복잡도와 공간 복잡도의 trade-off 를 정확히…
BBST (Splay Tree, Treap)algorithm
정의 BBST (Balanced Binary Search Tree) 는 균형이 amortized / expected 로 보장되는 이진 탐색 트리. PS 에서는 split / me…
Implicit Treap: 배열 인덱스 기반 Treapalgorithm
정의 Implicit Treap 은 Treap 에서 키 대신 서브트리 크기 (위치) 로 인덱싱하는 변형입니다. 배열의 삽입/삭제/이동/구간 반전 등을 O(log N) 에 지원합니…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기