普通视图

发现新文章,点击刷新页面。
今天 — 2026年2月11日首页

枚举 & 线段树 & 二分

作者 tsreaper
2025年10月19日 12:01

解法:枚举 & 线段树 & 二分

简化问题

先考虑一个简化版的问题:求最长的子数组,使得其中偶数的数量等于奇数的数量。

这个简化版问题和上周的 leetcode 3714. 最长的平衡子串 II 非常相似:把奇数看成 $1$,偶数看成 $-1$,求的其实就是“最长的和为 $0$ 的子数组”。详见 leetcode 560. 和为 K 的子数组

简单来说,这个问题可以用前缀和求解:设 $s_i$ 表示长度为 $i$ 的前缀的元素和,若子数组 $(l, r]$ 的元素和为 $0$,则有 $s_r - s_l = 0$,即 $s_l = s_r$。为了让子数组的长度 $(r - l)$ 最大,我们可以用哈希表维护每种 $s_l$ 对应的最小 $l$。

原问题

回到原问题。现在只统计不同的偶数和不同的奇数,怎么做?

首先,和子数组里的元素种数有关的题目,应该马上想到经典问题“luo 谷 P1972 - [SDOI2009] HH 的项链”:从左到右枚举子数组的右端点,对于每种数,只把它最近出现的位置设为 $\pm 1$,其它位置都设为 $0$。

这样,问题就变成了动态版的“求最长的和为 $0$ 的子数组”:给定一个序列,每次操作可能把一个 $0$ 变成 $\pm 1$,或把一个 $\pm 1$ 变成 $0$。每次询问给定一个前缀和目标值 $s_r$,找出 $s_l = s_r$ 的最小下标 $l$。

元素的修改可以用线段树来维护,可是最小下标该怎么找呢?其实,元素范围在 $[-1, 1]$ 里的序列有一个非常强的性质:由于每移动一位,前缀和的变化最多为 $1$,因此 在一个区间内,前缀和是连续的

所以,我们只需要在线段树的每个节点上,记录当前区间的最小前缀和 $x$ 和最大前缀和 $y$,只要 $x \le s_r \le y$,那么区间内一定存在一个下标 $l$,满足 $s_l = s_r$。所以在线段树上二分,即可找到这个下标。详见参考代码。

复杂度 $\mathcal{O}(n\log n)$。

参考代码(c++)

class Solution {
public:
    int longestBalanced(vector<int>& nums) {
        int n = nums.size();

        // 线段树节点,记录当前区间前缀和的最小值与最大值
        struct Node {
            int mn, mx, lazy;

            void apply(int x) {
                mn += x;
                mx += x;
                lazy += x;
            }
        } tree[(n + 1) * 4 + 5];

        auto merge = [&](Node nl, Node nr) {
            return Node {
                min(nl.mn, nr.mn),
                max(nl.mx, nr.mx),
                0
            };
        };

        // 线段树建树
        auto build = [&](this auto &&build, int id, int l, int r) -> void {
            if (l == r) tree[id] = Node {0, 0, 0};
            else {
                int nxt = id << 1, mid = (l + r) >> 1;
                build(nxt, l, mid); build(nxt | 1, mid + 1, r);
                tree[id] = merge(tree[nxt], tree[nxt | 1]);
            }
        };

        // 懒标记下推
        auto down = [&](int id) {
            if (tree[id].lazy == 0) return;
            int nxt = id << 1;
            tree[nxt].apply(tree[id].lazy);
            tree[nxt | 1].apply(tree[id].lazy);
            tree[id].lazy = 0;
        };

        // 给区间 [ql, qr] 的前缀和都加上 qv
        auto modify = [&](this auto &&modify, int id, int l, int r, int ql, int qr, int qv) -> void {
            if (ql <= l && r <= qr) tree[id].apply(qv);
            else {
                down(id);
                int nxt = id << 1, mid = (l + r) >> 1;
                if (ql <= mid) modify(nxt, l, mid, ql, qr, qv);
                if (qr > mid) modify(nxt | 1, mid + 1, r, ql, qr, qv);
                tree[id] = merge(tree[nxt], tree[nxt | 1]);
            }
        };

        // 线段树上二分,求前缀和等于 qv 的最小下标
        auto query = [&](this auto &&query, int id, int l, int r, int qv) -> int {
            if (l == r) return l;
            down(id);
            int nxt = id << 1, mid = (l + r) >> 1;
            // 只要一个区间满足 mn <= qv <= mx,那么一定存在一个等于 qv 的值
            // 为了让下标最小,只要左子区间满足,就去左子区间里拿答案,否则才去右子区间拿答案
            if (tree[nxt].mn <= qv && qv <= tree[nxt].mx) return query(nxt, l, mid, qv);
            else return query(nxt | 1, mid + 1, r, qv);
        };

        build(1, 0, n);
        // now:目前的前缀和
        int ans = 0, now = 0;
        // mp[x]:元素 x 最近出现在哪个下标
        unordered_map<int, int> mp;
        // 枚举子数组右端点
        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            int det = (x & 1 ? 1 : -1);
            if (mp.count(x)) {
                // 元素 x 之前出现过了,把那个位置改成 0
                modify(1, 0, n, mp[x], n, -det);
                now -= det;
            }
            // 把元素 x 当前出现的位置改成 +-1
            mp[x] = i;
            modify(1, 0, n, i, n, det);
            now += det;
            int pos = query(1, 0, n, now);
            ans = max(ans, i - pos);
        }
        return ans;
    }
};

不会做怎么办

本题的综合性比较强,需要读者掌握大量套路,我们逐个分解。

首先,如果读者不会做简化问题(即去掉“不同”的限制),说明读者没有掌握用前缀和 + 哈希表的方式,求特定子数组数量或最大长度的方法。读者可以学习 灵神题单 - 常用数据结构 的“前缀和与哈希表”一节。

接下来,如果读者看到“子数组里的不同元素”,没有马上反映出对应套路,需要复习“luo 谷 P1972 - [SDOI2009] HH 的项链”,并额外练习以下题目:

最后,如果读者没有意识到“元素范围在 $[-1, 1]$ 内的序列,在一个区间内,前缀和是连续的”,我暂时没有找到直接相关的练习题。可以练习 leetcode 2488. 统计中位数为 K 的子数组 一题,允许存在相同元素的加强版,并尝试用线性复杂度解答。我的题解 可供参考。

昨天以前首页

模拟

作者 tsreaper
2024年12月8日 12:23

解法:模拟

按题意模拟即可。复杂度 $\mathcal{O}(n)$。

参考代码(c++)

###cpp

class Solution {
public:
    vector<int> constructTransformedArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> ans;
        for (int i = 0; i < n; i++) {
            int j = (i + nums[i] % n + n) % n;
            ans.push_back(nums[j]);
        }
        return ans;
    }
};
❌
❌