블록 기반 구조와 블록 리스트의 활용

블록 분할 기법

길이가 n인 배열을 약 √n개의 블록으로 나누면, 각 블록의 크기는 최대 √n이 되며, 임의의 구간 연산은 최대 √n개의 완전한 블록과 두 개의 부분 블록으로 표현 가능하다.

시간 복잡도는 기존의 O(n²)에서 O(n√n)으로 개선된다.

기본 구현 틀

#include <iostream>
#include <cmath>
#include <cstdio>

using namespace std;

typedef long long LL;
const int MAX_N = 100010;
const int BLOCK_SIZE = 350;

int n, q;
int block_count;
LL block_add[BLOCK_SIZE], block_sum[BLOCK_SIZE];
int arr[MAX_N];

// 블록 인덱스 반환
int get_block(int pos) {
    return pos / block_count;
}

// 범위에 값을 추가하는 함수
void update_range(int left, int right, int delta) {
    if (get_block(left) == get_block(right)) {
        // 같은 블록 내에서는 직접 처리
        for (int i = left; i <= right; ++i) {
            arr[i] += delta;
            block_sum[get_block(i)] += delta;
        }
    } else {
        int i = left, j = right;
        // 양 끝 블록 처리
        while (get_block(i) == get_block(left)) {
            arr[i] += delta;
            block_sum[get_block(i)] += delta;
            ++i;
        }
        while (get_block(j) == get_block(right)) {
            arr[j] += delta;
            block_sum[get_block(j)] += delta;
            --j;
        }
        // 중간 블록들에 일괄 업데이트
        for (int k = get_block(i); k <= get_block(j); ++k) {
            block_sum[k] += block_count * delta;
            block_add[k] += delta;
        }
    }
}

// 범위의 합 계산
LL query_sum(int left, int right) {
    LL result = 0;
    if (get_block(left) == get_block(right)) {
        for (int i = left; i <= right; ++i)
            result += arr[i] + block_add[get_block(i)];
    } else {
        int i = left, j = right;
        while (get_block(i) == get_block(left))
            result += arr[i] + block_add[get_block(i)], ++i;
        while (get_block(j) == get_block(right))
            result += arr[j] + block_add[get_block(j)], --j;
        for (int k = get_block(i); k <= get_block(j); ++k)
            result += block_sum[k];
    }
    return result;
}

int main() {
    scanf("%d%d", &n, &q);
    block_count = static_cast<int>(sqrt(n));

    for (int i = 1; i <= n; ++i) {
        scanf("%d", &arr[i]);
        block_sum[get_block(i)] += arr[i];
    }

    char op[2];
    int l, r, d;
    while (q--) {
        scanf("%s%d%d", op, &l, &r);
        if (*op == 'C') {
            scanf("%d", &d);
            update_range(l, r, d);
        } else {
            printf("%lld\n", query_sum(l, r));
        }
    }

    return 0;
}

블록 기반 연결 리스트

각 블록에 전후 링크를 포함하여 연결 리스트처럼 관리한다. 삽입/삭제 시에도 효율적인 구조 유지가 가능하다. 그러나 시간 복잡도를 보장하기 위해 정기적으로 merge 연산을 수행하여 작은 블록들을 통합해야 한다.

문제 예시: [NOI2003] 텍스트 편집기

#include <iostream>
#include <cstring>
#include <cstdio>

using namespace std;

const int MAX_NODES = 2010;
const int MAX_CHARS = 2000;

struct BlockNode {
    char data[MAX_CHARS + 1];
    int size, prev, next;
} nodes[MAX_NODES];

char input_buffer[2000010];
int free_list[MAX_NODES], free_top;
int cursor_node, cursor_pos;

// 커서 위치 이동
void move_cursor(int pos) {
    cursor_node = 0;
    cursor_pos = 0;
    while (pos > nodes[cursor_node].size) {
        pos -= nodes[cursor_node].size;
        cursor_node = nodes[cursor_node].next;
    }
    cursor_pos = pos - 1;
}

// 새 블록 삽입 (기존 노드 오른쪽)
void insert_after(int target, int new_idx) {
    nodes[new_idx].next = nodes[target].next;
    nodes[nodes[target].next].prev = new_idx;
    nodes[target].next = new_idx;
    nodes[new_idx].prev = target;
}

// 블록 삭제 및 메모리 재활용
void remove_node(int idx) {
    nodes[nodes[idx].prev].next = nodes[idx].next;
    nodes[nodes[idx].next].prev = nodes[idx].prev;
    nodes[idx].prev = nodes[idx].next = nodes[idx].size = 0;
    free_list[++free_top] = idx;
}

// 커서 뒤에 문자 삽입
void insert_text(int count) {
    if (cursor_pos < nodes[cursor_node].size - 1) {
        int split_node = free_list[free_top--];
        int src_start = cursor_pos + 1;
        for (int i = src_start; i < nodes[cursor_node].size; ++i)
            nodes[split_node].data[nodes[split_node].size++] = nodes[cursor_node].data[i];
        nodes[cursor_node].size = cursor_pos + 1;
        insert_after(cursor_node, split_node);
    }

    int current = cursor_node;
    for (int i = 0; i < count;) {
        int new_node = free_list[free_top--];
        while (nodes[new_node].size < MAX_CHARS && i < count)
            nodes[new_node].data[nodes[new_node].size++] = input_buffer[i++];
        insert_after(current, new_node);
        current = new_node;
    }
}

// 커서 뒤의 문자 삭제
void delete_text(int count) {
    if (nodes[cursor_node].size - 1 - cursor_pos >= count) {
        for (int i = cursor_pos + count + 1, j = cursor_pos + 1; i < nodes[cursor_node].size; ++i, ++j)
            nodes[cursor_node].data[j] = nodes[cursor_node].data[i];
        nodes[cursor_node].size -= count;
    } else {
        count -= nodes[cursor_node].size - cursor_pos - 1;
        nodes[cursor_node].size = cursor_pos + 1;
        while (nodes[cursor_node].next && count >= nodes[nodes[cursor_node].next].size) {
            int del_node = nodes[cursor_node].next;
            count -= nodes[del_node].size;
            remove_node(del_node);
        }
        int tail_node = nodes[cursor_node].next;
        for (int i = 0, j = count; j < nodes[tail_node].size; ++i, ++j)
            nodes[tail_node].data[i] = nodes[tail_node].data[j];
        nodes[tail_node].size -= count;
    }
}

// 커서부터 특정 수의 문자 출력
void output_chars(int count) {
    if (nodes[cursor_node].size - 1 - cursor_pos >= count) {
        for (int i = cursor_pos + 1, j = 0; j < count; ++i, ++j)
            putchar(nodes[cursor_node].data[i]);
    } else {
        for (int i = cursor_pos + 1; i < nodes[cursor_node].size; ++i)
            putchar(nodes[cursor_node].data[i]);
        count -= nodes[cursor_node].size - cursor_pos - 1;
        int cur = cursor_node;
        while (nodes[cur].next && count >= nodes[nodes[cur].next].size) {
            int node = nodes[cur].next;
            for (int i = 0; i < nodes[node].size; ++i)
                putchar(nodes[node].data[i]);
            count -= nodes[node].size;
            cur = node;
        }
        int last = nodes[cur].next;
        for (int i = 0; i < count; ++i)
            putchar(nodes[last].data[i]);
    }
    puts("");
}

// 커서 이동: 왼쪽
void move_left() {
    if (cursor_pos == 0) {
        cursor_node = nodes[cursor_node].prev;
        cursor_pos = nodes[cursor_node].size - 1;
    } else {
        --cursor_pos;
    }
}

// 커서 이동: 오른쪽
void move_right() {
    if (cursor_pos < nodes[cursor_node].size - 1) {
        ++cursor_pos;
    } else {
        cursor_node = nodes[cursor_node].next;
        cursor_pos = 0;
    }
}

// 인접한 작고 비어 있지 않은 블록 통합
void merge_blocks() {
    for (int i = nodes[0].next; i != 0; i = nodes[i].next) {
        while (nodes[i].next && nodes[i].size + nodes[nodes[i].next].size < MAX_CHARS) {
            int right_node = nodes[i].next;
            for (int j = nodes[i].size, k = 0; k < nodes[right_node].size; ++j, ++k)
                nodes[i].data[j] = nodes[right_node].data[k];
            if (cursor_node == right_node) {
                cursor_node = i;
                cursor_pos += nodes[i].size;
            }
            nodes[i].size += nodes[right_node].size;
            remove_node(right_node);
        }
    }
}

int main() {
    for (int i = 1; i < MAX_NODES; ++i)
        free_list[++free_top] = i;

    int operations;
    scanf("%d", &operations);

    // 초기 빈 노드 삽입
    nodes[0].next = free_list[free_top--];
    nodes[0].size = 1;
    nodes[0].data[0] = '>';
    nodes[nodes[0].next].prev = 0;
    cursor_node = 0;
    cursor_pos = 0;

    char command[10];
    while (operations--) {
        scanf("%s", command);
        if (strcmp(command, "Move") == 0) {
            int pos;
            scanf("%d", &pos);
            move_cursor(pos + 1);
        } else if (strcmp(command, "Insert") == 0) {
            int cnt;
            scanf("%d", &cnt);
            int idx = 0;
            while (cnt--) {
                input_buffer[idx] = getchar();
                if (input_buffer[idx] >= 32 && input_buffer[idx] <= 126)
                    ++idx, --cnt;
            }
            insert_text(idx);
            merge_blocks();
        } else if (strcmp(command, "Delete") == 0) {
            int cnt;
            scanf("%d", &cnt);
            delete_text(cnt);
            merge_blocks();
        } else if (strcmp(command, "Get") == 0) {
            int cnt;
            scanf("%d", &cnt);
            output_chars(cnt);
        } else if (strcmp(command, "Prev") == 0) {
            move_left();
        } else {
            move_right();
        }
    }

    return 0;
}

이러한 구조는 특히 부분 점수 문제에서 유용하며, 논리적 접근이 간단하고 코드 수정이 쉬운 장점이 있다.

태그: 블록 분할 블록 리스트 데이터 구조 알고리즘 구간 연산

6월 21일 03:59에 게시됨