每日算法 — 使用java实现分块:平方分割与区间查询优化

引言

分块(Sqrt Decomposition / Blocking)是一种重要的数据结构思想,通过对数据进行平方级别的分块处理,将区间操作的时间复杂度从线性优化到接近根号级别。它在区间查询、区间修改问题中有着广泛的应用,尤其适用于线段树难以处理的复杂区间操作场景。本文使用Java实现分块数据结构,讲解平方分割的核心思想,并应用于区间最值查询与区间加法更新问题。

核心思想

将长度为 n 的数组分成约 √n 个块,每个块包含约 √n 个元素。对于区间操作:
– 完整块:整块使用预处理信息直接计算,O(1)
– 不完整块:两端零散元素逐个处理,O(√n)

单次操作时间复杂度为 O(√n),在 n = 10^5 时,√n ≈ 316,远优于 O(n) 的暴力方法。

基础分块:区间求和与单点修改

import java.util.*;

/**
 * 分块数据结构:平方分割实现区间查询与修改
 */
public class SqrtDecomposition {

    private int[] arr;      // 原数组
    private int n;          // 数组长度
    private int blockSize;  // 块大小
    private int blockCount; // 块数量
    private long[] blockSum; // 每块的和
    private int[] blockMax;  // 每块的最大值
    private int[] blockMin;  // 每块的最小值
    private long[] lazyAdd;  // 懒标记:整块加法

    public SqrtDecomposition(int[] input) {
        this.n = input.length;
        this.arr = Arrays.copyOf(input, n);
        this.blockSize = (int) Math.sqrt(n) + 1;
        this.blockCount = (n + blockSize - 1) / blockSize;
        this.blockSum = new long[blockCount];
        this.blockMax = new int[blockCount];
        this.blockMin = new int[blockCount];
        this.lazyAdd = new long[blockCount];

        Arrays.fill(blockMax, Integer.MIN_VALUE);
        Arrays.fill(blockMin, Integer.MAX_VALUE);

        // 初始化块信息
        for (int i = 0; i < n; i++) {
            int b = i / blockSize;
            blockSum[b] += arr[i];
            blockMax[b] = Math.max(blockMax[b], arr[i]);
            blockMin[b] = Math.min(blockMin[b], arr[i]);
        }
    }

    /**
     * 区间加法:将 [l, r] 内每个元素加上 val
     */
    public void rangeAdd(int l, int r, int val) {
        int bl = l / blockSize;
        int br = r / blockSize;

        if (bl == br) {
            // 同一块内,直接逐个修改
            for (int i = l; i <= r; i++) {
                arr[i] += val;
            }
            rebuildBlock(bl);
        } else {
            // 左端不完整块
            for (int i = l; i < (bl + 1) * blockSize && i < n; i++) {
                arr[i] += val;
            }
            rebuildBlock(bl);

            // 右端不完整块
            for (int i = br * blockSize; i <= r; i++) {
                arr[i] += val;
            }
            rebuildBlock(br);

            // 中间完整块,使用懒标记
            for (int b = bl + 1; b < br; b++) {
                lazyAdd[b] += val;
                blockSum[b] += (long) val * blockSize;
                blockMax[b] += val;
                blockMin[b] += val;
            }
        }
    }

    /**
     * 区间求和查询
     */
    public long rangeSum(int l, int r) {
        int bl = l / blockSize;
        int br = r / blockSize;
        long sum = 0;

        if (bl == br) {
            for (int i = l; i <= r; i++) {
                sum += arr[i] + lazyAdd[bl];
            }
        } else {
            for (int i = l; i < (bl + 1) * blockSize && i < n; i++) {
                sum += arr[i] + lazyAdd[bl];
            }
            for (int i = br * blockSize; i <= r; i++) {
                sum += arr[i] + lazyAdd[br];
            }
            for (int b = bl + 1; b < br; b++) {
                sum += blockSum[b];
            }
        }
        return sum;
    }

    /**
     * 区间最大值查询
     */
    public int rangeMax(int l, int r) {
        int bl = l / blockSize;
        int br = r / blockSize;
        int max = Integer.MIN_VALUE;

        if (bl == br) {
            for (int i = l; i <= r; i++) {
                max = Math.max(max, arr[i] + (int) lazyAdd[bl]);
            }
        } else {
            for (int i = l; i < (bl + 1) * blockSize && i < n; i++) {
                max = Math.max(max, arr[i] + (int) lazyAdd[bl]);
            }
            for (int i = br * blockSize; i <= r; i++) {
                max = Math.max(max, arr[i] + (int) lazyAdd[br]);
            }
            for (int b = bl + 1; b < br; b++) {
                max = Math.max(max, blockMax[b]);
            }
        }
        return max;
    }

    /**
     * 单点查询:获取下标 i 的实际值
     */
    public long pointQuery(int i) {
        return arr[i] + lazyAdd[i / blockSize];
    }

    /**
     * 重建指定块的信息
     */
    private void rebuildBlock(int b) {
        int start = b * blockSize;
        int end = Math.min(start + blockSize, n);
        blockSum[b] = 0;
        blockMax[b] = Integer.MIN_VALUE;
        blockMin[b] = Integer.MAX_VALUE;

        for (int i = start; i < end; i++) {
            blockSum[b] += arr[i];
            blockMax[b] = Math.max(blockMax[b], arr[i]);
            blockMin[b] = Math.min(blockMin[b], arr[i]);
        }
        // 注意:这里arr已经是实际值,所以不需要再加lazyAdd
    }

莫队算法:离线区间查询优化

    /**
     * 莫队算法:通过调整查询顺序减少指针移动次数
     * 适用于离线区间查询问题,时间复杂度 O((n+q)*√n)
     */
    static class MoAlgorithm {
        int[] arr;
        int blockSize;

        MoAlgorithm(int[] arr) {
            this.arr = arr;
            this.blockSize = (int) Math.sqrt(arr.length) + 1;
        }

        /**
         * 对查询区间按莫队顺序排序
         */
        List<Query> sortQueries(List<Query> queries) {
            queries.sort((a, b) -> {
                int ba = a.l / blockSize;
                int bb = b.l / blockSize;
                if (ba != bb) return ba - bb;
                return (ba % 2 == 0) ? a.r - b.r : b.r - a.r; // 奇偶优化
            });
            return queries;
        }

        /**
         * 处理查询:示例为区间不同数字个数
         */
        int[] processQueries(int[] arr, List<Query> queries) {
            List<Query> sorted = sortQueries(queries);
            int[] ans = new int[sorted.size()];
            int[] freq = new int[1000005]; // 假设数值范围
            int currentL = 0, currentR = -1;
            int distinctCount = 0;

            for (Query q : sorted) {
                // 扩展/收缩左指针
                while (currentL > q.l) {
                    currentL--;
                    if (freq[arr[currentL]] == 0) distinctCount++;
                    freq[arr[currentL]]++;
                }
                while (currentL < q.l) {
                    freq[arr[currentL]]--;
                    if (freq[arr[currentL]] == 0) distinctCount--;
                    currentL++;
                }

                // 扩展/收缩右指针
                while (currentR < q.r) {
                    currentR++;
                    if (freq[arr[currentR]] == 0) distinctCount++;
                    freq[arr[currentR]]++;
                }
                while (currentR > q.r) {
                    freq[arr[currentR]]--;
                    if (freq[arr[currentR]] == 0) distinctCount--;
                    currentR--;
                }

                ans[q.id] = distinctCount;
            }
            return ans;
        }
    }

    static class Query {
        int l, r, id;
        Query(int l, int r, int id) { this.l = l; this.r = r; this.id = id; }
    }

分块 vs 线段树对比

    /**
     * 性能对比测试
     */
    public static void main(String[] args) {
        int n = 100000;
        int[] arr = new int[n];
        Random rand = new Random(42);
        for (int i = 0; i < n; i++) {
            arr[i] = rand.nextInt(1000);
        }

        SqrtDecomposition sd = new SqrtDecomposition(arr);

        System.out.println("=== 分块数据结构测试 ===\n");

        // 区间加法
        long t1 = System.currentTimeMillis();
        for (int i = 0; i < 10000; i++) {
            int l = rand.nextInt(n);
            int r = l + rand.nextInt(Math.min(1000, n - l));
            sd.rangeAdd(l, r, rand.nextInt(100));
        }
        System.out.println("10000次区间加法耗时: " + (System.currentTimeMillis() - t1) + "ms");

        // 区间求和查询
        t1 = System.currentTimeMillis();
        long totalSum = 0;
        for (int i = 0; i < 10000; i++) {
            int l = rand.nextInt(n);
            int r = l + rand.nextInt(Math.min(1000, n - l));
            totalSum += sd.rangeSum(l, r);
        }
        System.out.println("10000次区间求和耗时: " + (System.currentTimeMillis() - t1) + "ms");

        // 区间最大值查询
        t1 = System.currentTimeMillis();
        int totalMax = 0;
        for (int i = 0; i < 10000; i++) {
            int l = rand.nextInt(n);
            int r = l + rand.nextInt(Math.min(1000, n - l));
            totalMax ^= sd.rangeMax(l, r);
        }
        System.out.println("10000次区间最大值耗时: " + (System.currentTimeMillis() - t1) + "ms");

        System.out.println("\n校验值: sum=" + totalSum + ", maxXor=" + totalMax);
    }
}

复杂度分析

操作 分块 线段树 暴力
单点修改 O(√n) O(log n) O(1)
区间修改 O(√n) O(log n) O(n)
区间查询 O(√n) O(log n) O(n)
实现难度 低 中 极低
代码量 少 多 极少

分块的优势在于实现简单、代码量小、灵活性高,特别适合线段树难以处理的复杂操作。

总结

本文通过分块数据结构实现了区间加法、区间求和、区间最值等操作,并介绍了莫队算法的离线查询优化技巧。读者可以掌握平方分割的核心思想,在竞赛和工程实践中根据问题特点灵活选择分块、线段树或其他数据结构。