阅读视图

发现新文章,点击刷新页面。

两种方法维护前缀和:Lazy 线段树 / 分块(Python/Java/C++/Go)

前置题目/知识

  1. 本题的简单版本 525. 连续数组我的题解
  2. 前缀和
  3. Lazy 线段树

转化

如果可以把问题转化成 525 题,就好解决了。

对比一下:

  • 525 题,相同元素多次统计。
  • 本题,相同元素只能统计一次。

如果我们能找到一个方法,使得相同元素只被统计一次,那么就能转化成 525 题。

从左到右遍历 $\textit{nums}$,如果固定子数组右端点为 $i$,要想让子数组包含某个元素 $x$,左端点必须 $\le x\ 最后一次出现的位置$。只要子数组包含最近遇到的 $x$,那么无论子数组有多长,都包含了 $x$。题目要求,多个 $x$ 只能算一次,那么把除了最近一次的 $x$ 全部不计入,就变成 525 题了!

以 $\textit{nums}=[1,2,1,2,3,3]$ 为例:

  • 遍历到 $i=0$,把 $\textit{nums}$ 视作 $[1,,,,,*]$。
  • 遍历到 $i=1$,把 $\textit{nums}$ 视作 $[1,2,,,,]$。
  • 遍历到 $i=2$,把 $\textit{nums}$ 视作 $[,2,1,,,]$。
  • 遍历到 $i=3$,把 $\textit{nums}$ 视作 $[,,1,2,,]$。
  • 遍历到 $i=4$,把 $\textit{nums}$ 视作 $[,,1,2,3,*]$。
  • 遍历到 $i=5$,把 $\textit{nums}$ 视作 $[,,1,2,*,3]$。

根据 525 题,把偶数视作 $-1$,奇数视作 $1$,遍历过的星号视作 $0$,设这个新数组为 $a$,问题相当于:

  • 计算 $a$ 中和为 $0$ 的最长子数组的长度。

设 $a$ 的长为 $n+1$ 的前缀和数组为 $\textit{sum}$。根据 525 题,问题相当于:

  • 枚举 $i$,在 $[0,i-1]$ 中找到一个下标最小的 $\textit{sum}[j]$,满足 $\textit{sum}[j] = \textit{sum}[i]$。
  • 用子数组长度 $i-j$ 更新答案的最大值。

根据上面动态变化的过程:

  • 设 $x=\textit{nums}[i]$ 对应的 $a[i]$ 值为 $v$。
  • 当我们首次遇到 $x$ 时,对于前缀和 $\textit{sum}$ 来说,$[i,n]$ 要全部增加 $v$。
  • 当我们再次遇到 $x$ 时,原来的 $\textit{nums}[j]$ 变成星号($a[j]=0$),$x$ 搬到了新的位置 $i$,所以之前的「$[j,n]$ 全部增加 $v$」变成了「$[i,n]$ 全部增加 $v$」,也就是撤销 $[j,i-1]$ 的加 $v$,也就是把 $[j,i-1]$ 减 $v$。

整理一下,我们需要维护一个动态变化的前缀和数组,需要一个数据结构,支持:

  1. 把 $\textit{sum}$ 的某个子数组增加 $1$ 或者 $-1$。
  2. 查询 $\textit{sum}[i]$ 在 $\textit{sum}$ 中首次出现的位置。

本题视频讲解,欢迎点赞关注~

方法一:Lazy 线段树

由于 $a$ 中元素只有 $-1,0,1$,所以 $\textit{sum}$ 数组相邻元素之差 $\le 1$。根据离散介值定理,设 $\textit{min}$ 和 $\textit{max}$ 分别为区间的最小值和最大值,只要 $\textit{sum}[i]$ 在 $[\textit{min},\textit{max}]$ 范围中,区间就一定存在等于 $\textit{sum}[i]$ 的数。

用 Lazy 线段树维护区间最小值、区间最大值、区间加的 Lazy tag。

下面只用到部分线段树模板,完整线段树模板见 数据结构题单

###py

# 手写 min max 更快
min = lambda a, b: b if b < a else a
max = lambda a, b: b if b > a else a

class Node:
    __slots__ = 'min', 'max', 'todo'

    def __init__(self):
        self.min = self.max = self.todo = 0

class LazySegmentTree:
    def __init__(self, n: int):
        self._n = n
        self._tree = [Node() for _ in range(2 << (n - 1).bit_length())]

    # 把懒标记作用到 node 子树
    def _apply(self, node: int, todo: int) -> None:
        cur = self._tree[node]
        cur.min += todo
        cur.max += todo
        cur.todo += todo

    # 把当前节点的懒标记下传给左右儿子
    def _spread(self, node: int) -> None:
        todo = self._tree[node].todo
        if todo == 0:  # 没有需要下传的信息
            return
        self._apply(node * 2, todo)
        self._apply(node * 2 + 1, todo)
        self._tree[node].todo = 0  # 下传完毕

    # 合并左右儿子的 min max 到当前节点
    def _maintain(self, node: int) -> None:
        l_node = self._tree[node * 2]
        r_node = self._tree[node * 2 + 1]
        self._tree[node].min = min(l_node.min, r_node.min)
        self._tree[node].max = max(l_node.max, r_node.max)

    def _update(self, node: int, l: int, r: int, ql: int, qr: int, f: int) -> None:
        if ql <= l and r <= qr:  # 当前子树完全在 [ql, qr] 内
            self._apply(node, f)
            return
        self._spread(node)
        m = (l + r) // 2
        if ql <= m:  # 更新左子树
            self._update(node * 2, l, m, ql, qr, f)
        if qr > m:  # 更新右子树
            self._update(node * 2 + 1, m + 1, r, ql, qr, f)
        self._maintain(node)

    def _find_first(self, node: int, l: int, r: int, ql: int, qr: int, target: int) -> int:
        if l > qr or r < ql or not self._tree[node].min <= target <= self._tree[node].max:
            return -1
        if l == r:
            return l
        self._spread(node)
        m = (l + r) // 2
        idx = self._find_first(node * 2, l, m, ql, qr, target)
        if idx < 0:
            # 去右子树找
            idx = self._find_first(node * 2 + 1, m + 1, r, ql, qr, target)
        return idx

    # 用 f 更新 [ql, qr] 中的每个 sum[i]
    # 0 <= ql <= qr <= n-1
    # 时间复杂度 O(log n)
    def update(self, ql: int, qr: int, f: int) -> None:
        self._update(1, 0, self._n - 1, ql, qr, f)

    # 查询 [ql, qr] 内第一个等于 target 的元素下标
    # 找不到返回 -1
    # 0 <= ql <= qr <= n-1
    # 时间复杂度 O(log n)
    def find_first(self, ql: int, qr: int, target: int) -> int:
        return self._find_first(1, 0, self._n - 1, ql, qr, target)

class Solution:
    def longestBalanced(self, nums: List[int]) -> int:
        n = len(nums)
        t = LazySegmentTree(n + 1)

        last = {}  # nums 的元素上一次出现的位置
        ans = cur_sum = 0
        for i, x in enumerate(nums, 1):
            v = 1 if x % 2 else -1
            j = last.get(x, 0)
            if j == 0:  # 首次遇到 x
                cur_sum += v
                t.update(i, n, v)  # sum[i:] 增加 v
            else:  # 再次遇到 x
                t.update(j, i - 1, -v)  # 撤销之前对 sum[j:i] 的增加
            last[x] = i

            # 把 i-1 优化成 i-1-ans,因为在下标 > i-1-ans 中搜索是没有意义的,不会把答案变大
            j = t.find_first(0, i - 1 - ans, cur_sum)
            if j >= 0:
                ans = i - j  # 如果找到了,那么答案肯定会变大
        return ans

###java

class LazySegmentTree {
    private static final class Node {
        int min;
        int max;
        int todo;
    }

    // 把懒标记作用到 node 子树
    private void apply(int node, int todo) {
        Node cur = tree[node];
        cur.min += todo;
        cur.max += todo;
        cur.todo += todo;
    }

    private final int n;
    private final Node[] tree;

    // 线段树维护一个长为 n 的数组(下标从 0 到 n-1)
    public LazySegmentTree(int n) {
        this.n = n;
        tree = new Node[2 << (32 - Integer.numberOfLeadingZeros(n - 1))];
        Arrays.setAll(tree, _ -> new Node());
    }

    // 用 f 更新 [ql, qr] 中的每个 sum[i]
    // 0 <= ql <= qr <= n-1
    // 时间复杂度 O(log n)
    public void update(int ql, int qr, int f) {
        update(1, 0, n - 1, ql, qr, f);
    }

    // 查询 [ql, qr] 内第一个等于 target 的元素下标
    // 找不到返回 -1
    // 0 <= ql <= qr <= n-1
    // 时间复杂度 O(log n)
    public int findFirst(int ql, int qr, int target) {
        return findFirst(1, 0, n - 1, ql, qr, target);
    }

    // 把当前节点的懒标记下传给左右儿子
    private void spread(int node) {
        int todo = tree[node].todo;
        if (todo == 0) { // 没有需要下传的信息
            return;
        }
        apply(node * 2, todo);
        apply(node * 2 + 1, todo);
        tree[node].todo = 0; // 下传完毕
    }

    // 合并左右儿子的 val 到当前节点的 val
    private void maintain(int node) {
        tree[node].min = Math.min(tree[node * 2].min, tree[node * 2 + 1].min);
        tree[node].max = Math.max(tree[node * 2].max, tree[node * 2 + 1].max);
    }

    private void update(int node, int l, int r, int ql, int qr, int f) {
        if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
            apply(node, f);
            return;
        }
        spread(node);
        int m = (l + r) / 2;
        if (ql <= m) { // 更新左子树
            update(node * 2, l, m, ql, qr, f);
        }
        if (qr > m) { // 更新右子树
            update(node * 2 + 1, m + 1, r, ql, qr, f);
        }
        maintain(node);
    }

    private int findFirst(int node, int l, int r, int ql, int qr, int target) {
        if (l > qr || r < ql || target < tree[node].min || target > tree[node].max) {
            return -1;
        }
        if (l == r) {
            return l;
        }
        spread(node);
        int m = (l + r) / 2;
        int idx = findFirst(node * 2, l, m, ql, qr, target);
        if (idx < 0) {
            idx = findFirst(node * 2 + 1, m + 1, r, ql, qr, target);
        }
        return idx;
    }
}

class Solution {
    public int longestBalanced(int[] nums) {
        int n = nums.length;
        LazySegmentTree t = new LazySegmentTree(n + 1);

        Map<Integer, Integer> last = new HashMap<>(); // nums 的元素上一次出现的位置
        int ans = 0;
        int curSum = 0;

        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            int v = x % 2 > 0 ? 1 : -1;
            Integer j = last.get(x);
            if (j == null) { // 首次遇到 x
                curSum += v;
                t.update(i, n, v); // sum 的 [i,n] 增加 v
            } else { // 再次遇到 x
                t.update(j, i - 1, -v); // 撤销之前对 sum 的 [j,i-1] 的增加
            }
            last.put(x, i);

            // 把 i-1 优化成 i-1-ans,因为在下标 > i-1-ans 中搜索是没有意义的,不会把答案变大
            int l = t.findFirst(0, i - 1 - ans, curSum);
            if (l >= 0) {
                ans = i - l; // 如果找到了,那么答案肯定会变大
            }
        }
        return ans;
    }
}

###cpp

class LazySegmentTree {
    using T = pair<int, int>;
    using F = int;

    // 懒标记初始值
    const F TODO_INIT = 0;

    struct Node {
        T val;
        F todo;
    };

    int n;
    vector<Node> tree;

    // 合并两个 val
    T merge_val(const T& a, const T& b) const {
        return {min(a.first, b.first), max(a.second, b.second)};
    }

    // 合并两个懒标记
    F merge_todo(const F& a, const F& b) const {
        return a + b;
    }

    // 把懒标记作用到 node 子树(本例为区间加)
    void apply(int node, int l, int r, F todo) {
        Node& cur = tree[node];
        // 计算 tree[node] 区间的整体变化
        cur.val.first += todo;
        cur.val.second += todo;
        cur.todo = merge_todo(todo, cur.todo);
    }

    // 把当前节点的懒标记下传给左右儿子
    void spread(int node, int l, int r) {
        Node& cur = tree[node];
        F todo = cur.todo;
        if (todo == TODO_INIT) { // 没有需要下传的信息
            return;
        }
        int m = (l + r) / 2;
        apply(node * 2, l, m, todo);
        apply(node * 2 + 1, m + 1, r, todo);
        cur.todo = TODO_INIT; // 下传完毕
    }

    // 合并左右儿子的 val 到当前节点的 val
    void maintain(int node) {
        tree[node].val = merge_val(tree[node * 2].val, tree[node * 2 + 1].val);
    }

    // 用 a 初始化线段树
    // 时间复杂度 O(n)
    void build(const vector<T>& a, int node, int l, int r) {
        Node& cur = tree[node];
        cur.todo = TODO_INIT;
        if (l == r) { // 叶子
            cur.val = a[l]; // 初始化叶节点的值
            return;
        }
        int m = (l + r) / 2;
        build(a, node * 2, l, m); // 初始化左子树
        build(a, node * 2 + 1, m + 1, r); // 初始化右子树
        maintain(node);
    }

    void update(int node, int l, int r, int ql, int qr, F f) {
        if (ql <= l && r <= qr) { // 当前子树完全在 [ql, qr] 内
            apply(node, l, r, f);
            return;
        }
        spread(node, l, r);
        int m = (l + r) / 2;
        if (ql <= m) { // 更新左子树
            update(node * 2, l, m, ql, qr, f);
        }
        if (qr > m) { // 更新右子树
            update(node * 2 + 1, m + 1, r, ql, qr, f);
        }
        maintain(node);
    }

    // 查询 [ql,qr] 内第一个等于 target 的元素下标
    // 找不到返回 -1
    int find_first(int node, int l, int r, int ql, int qr, int target) {
        // 不相交 或 target 不在当前区间的 [min,max] 范围内
        if (l > qr || r < ql || target < tree[node].val.first || target > tree[node].val.second) {
            return -1;
        }
        if (l == r) {
            // 此处必然等于 target
            return l;
        }
        spread(node, l, r);
        int m = (l + r) / 2;
        int idx = find_first(node * 2, l, m, ql, qr, target);
        if (idx < 0) {
            // 去右子树找
            idx = find_first(node * 2 + 1, m + 1, r, ql, qr, target);
        }
        return idx;
    }

public:
    // 线段树维护一个长为 n 的数组(下标从 0 到 n-1),元素初始值为 init_val
    LazySegmentTree(int n, T init_val = {0, 0}) : LazySegmentTree(vector<T>(n, init_val)) {}

    // 线段树维护数组 a
    LazySegmentTree(const vector<T>& a) : n(a.size()), tree(2 << bit_width(a.size() - 1)) {
        build(a, 1, 0, n - 1);
    }

    // 用 f 更新 [ql, qr] 中的每个 a[i]
    // 0 <= ql <= qr <= n-1
    // 时间复杂度 O(log n)
    void update(int ql, int qr, F f) {
        update(1, 0, n - 1, ql, qr, f);
    }

    // 查询 [ql, qr] 内第一个等于 target 的元素下标
    // 找不到返回 -1
    // 0 <= ql <= qr <= n-1
    // 时间复杂度 O(log n)
    int find_first(int ql, int qr, int target) {
        return find_first(1, 0, n - 1, ql, qr, target);
    }
};

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

        unordered_map<int, int> last; // nums 的元素上一次出现的位置
        int ans = 0, cur_sum = 0;
        for (int i = 1; i <= n; i++) {
            int x = nums[i - 1];
            int v = x % 2 ? 1 : -1;
            auto it = last.find(x);
            if (it == last.end()) { // 首次遇到 x
                cur_sum += v;
                t.update(i, n, v); // sum 的 [i,n] 增加 v
            } else { // 再次遇到 x
                int j = it->second;
                t.update(j, i - 1, -v); // 撤销之前对 sum 的 [j,i-1] 的增加
            }
            last[x] = i;

            // 把 i-1 优化成 i-1-ans,因为在下标 > i-1-ans 中搜索是没有意义的,不会把答案变大
            int j = t.find_first(0, i - 1 - ans, cur_sum);
            if (j >= 0) {
                ans = i - j; // 如果找到了,那么答案肯定会变大
            }
        }
        return ans;
    }
};

###go

// 完整模板及注释见数据结构题单 https://leetcode.cn/circle/discuss/mOr1u6/
type pair struct{ min, max int }
type lazySeg []struct {
l, r int
pair
todo int
}

func merge(l, r pair) pair {
return pair{min(l.min, r.min), max(l.max, r.max)}
}

func (t lazySeg) apply(o int, f int) {
cur := &t[o]
cur.min += f
cur.max += f
cur.todo += f
}

func (t lazySeg) maintain(o int) {
t[o].pair = merge(t[o<<1].pair, t[o<<1|1].pair)
}

func (t lazySeg) spread(o int) {
f := t[o].todo
if f == 0 {
return
}
t.apply(o<<1, f)
t.apply(o<<1|1, f)
t[o].todo = 0
}

func (t lazySeg) build(o, l, r int) {
t[o].l, t[o].r = l, r
if l == r {
return
}
m := (l + r) >> 1
t.build(o<<1, l, m)
t.build(o<<1|1, m+1, r)
}

func (t lazySeg) update(o, l, r int, f int) {
if l <= t[o].l && t[o].r <= r {
t.apply(o, f)
return
}
t.spread(o)
m := (t[o].l + t[o].r) >> 1
if l <= m {
t.update(o<<1, l, r, f)
}
if m < r {
t.update(o<<1|1, l, r, f)
}
t.maintain(o)
}

// 查询 [l,r] 内第一个等于 target 的元素下标
func (t lazySeg) findFirst(o, l, r, target int) int {
if t[o].l > r || t[o].r < l || target < t[o].min || target > t[o].max {
return -1
}
if t[o].l == t[o].r {
return t[o].l
}
t.spread(o)
idx := t.findFirst(o<<1, l, r, target)
if idx < 0 {
// 去右子树找
idx = t.findFirst(o<<1|1, l, r, target)
}
return idx
}

func longestBalanced(nums []int) (ans int) {
n := len(nums)
t := make(lazySeg, 2<<bits.Len(uint(n)))
t.build(1, 0, n)

last := map[int]int{} // nums 的元素上一次出现的位置
curSum := 0
for i := 1; i <= n; i++ {
x := nums[i-1]
v := x%2*2 - 1
if j := last[x]; j == 0 { // 首次遇到 x
curSum += v
t.update(1, i, n, v) // sum[i:] 增加 v
} else { // 再次遇到 x
t.update(1, j, i-1, -v) // 撤销之前对 sum[j:i] 的增加
}
last[x] = i

// 把 i-1 优化成 i-1-ans,因为在下标 > i-1-ans 中搜索是没有意义的,不会把答案变大
j := t.findFirst(1, 0, i-1-ans, curSum)
if j >= 0 {
ans = i - j // 如果找到了,那么答案肯定会变大
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\log n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

方法二:分块

分块思想

这个做法没有用到 $\textit{sum}$ 数组的特殊性质,支持区间更新、查询任意值首次出现的位置。

每块维护块内 $\textit{sum}[i]$ 首次出现的位置,以及区间加的 Lazy tag。

###go

func longestBalanced(nums []int) (ans int) {
n := len(nums)
B := int(math.Sqrt(float64(n+1)))/2 + 1
sum := make([]int, n+1)

// === 分块模板开始 ===
// 用分块维护 sum
type block struct {
l, r int // [l,r) 左闭右开
todo int
pos  map[int]int
}
blocks := make([]block, n/B+1)
calcPos := func(l, r int) map[int]int {
pos := map[int]int{}
for j := r - 1; j >= l; j-- {
pos[sum[j]] = j
}
return pos
}
for i := 0; i <= n; i += B {
r := min(i+B, n+1)
pos := calcPos(i, r)
blocks[i/B] = block{i, r, 0, pos}
}

// sum[l:r] 增加 v
rangeAdd := func(l, r, v int) {
for i := range blocks {
b := &blocks[i]
if b.r <= l {
continue
}
if b.l >= r {
break
}
if l <= b.l && b.r <= r { // 完整块
b.todo += v
} else { // 部分块,直接重算
for j := b.l; j < b.r; j++ {
sum[j] += b.todo
if l <= j && j < r {
sum[j] += v
}
}
b.pos = calcPos(b.l, b.r)
b.todo = 0
}
}
}

// 返回 sum[:r] 中第一个 v 的下标
// 如果没有 v,返回 n
findFirst := func(r, v int) int {
for i := range blocks {
b := &blocks[i]
if b.r <= r { // 完整块,直接查哈希表
if j, ok := b.pos[v-b.todo]; ok {
return j
}
} else { // 部分块,暴力查找
for j := b.l; j < r; j++ {
if sum[j] == v-b.todo {
return j
}
}
break
}
}
return n
}
// === 分块模板结束 ===

last := map[int]int{} // nums 的元素上一次出现的位置
for i := 1; i <= n; i++ {
x := nums[i-1]
v := x%2*2 - 1
if j := last[x]; j == 0 { // 首次遇到 x
rangeAdd(i, n+1, v) // sum[i:] 增加 v
} else { // 再次遇到 x
rangeAdd(j, i, -v) // 撤销之前对 sum[j:i] 的增加
}
last[x] = i

s := sum[i] + blocks[i/B].todo // sum[i] 的实际值
ans = max(ans, i-findFirst(i-ans, s)) // 优化右边界
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n\sqrt n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(n)$。

相似题目

HH 的项链

专题训练

见下面数据结构题单的「§8.4 Lazy 线段树」和「十、根号算法」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

二叉搜索树 -> 数组 -> 二叉搜索树(Python/Java/C++/C/Go/JS/Rust)

由于输入的是一棵二叉搜索树,节点值满足 $左子树 < 根 < 右子树$,所以通过一次 94. 二叉树的中序遍历,把遍历到的节点值添加到一个数组中,可以直接得到一个递增数组,无需排序。

然后 108. 将有序数组转换为二叉搜索树,做法见 我的题解

###py

class Solution:
    # 94. 二叉树的中序遍历
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        def dfs(node: Optional[TreeNode]) -> None:
            if node is None:
                return
            dfs(node.left)        # 左
            ans.append(node.val)  # 根(这行代码移到前面就是前序,移到后面就是后序)
            dfs(node.right)       # 右

        ans = []
        dfs(root)
        return ans

    # 108. 将有序数组转换为二叉搜索树
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        if not nums:
            return None
        m = len(nums) // 2
        left = self.sortedArrayToBST(nums[:m])
        right = self.sortedArrayToBST(nums[m + 1:])
        return TreeNode(nums[m], left, right)

    def balanceBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        nums = self.inorderTraversal(root)
        return self.sortedArrayToBST(nums)

###py

class Solution:
    # 94. 二叉树的中序遍历
    def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
        def dfs(node: Optional[TreeNode]) -> None:
            if node is None:
                return
            dfs(node.left)        # 左
            ans.append(node.val)  # 根(这行代码移到前面就是前序,移到后面就是后序)
            dfs(node.right)       # 右

        ans = []
        dfs(root)
        return ans

    # 108. 将有序数组转换为二叉搜索树
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        # 把 nums[left:right] 转成平衡二叉搜索树
        def dfs(left: int, right: int) -> Optional[TreeNode]:
            if left == right:
                return None
            m = (left + right) // 2
            return TreeNode(nums[m], dfs(left, m), dfs(m + 1, right))

        return dfs(0, len(nums))

    def balanceBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]:
        nums = self.inorderTraversal(root)
        return self.sortedArrayToBST(nums)

###java

class Solution {
    public TreeNode balanceBST(TreeNode root) {
        List<Integer> nums = inorderTraversal(root);
        return sortedArrayToBST(nums);
    }

    // 94. 二叉树的中序遍历
    private List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        dfs(ans, root);
        return ans;
    }

    private void dfs(List<Integer> ans, TreeNode node) {
        if (node == null) {
            return;
        }
        dfs(ans, node.left);  // 左
        ans.add(node.val);    // 根(这行代码移到前面就是前序,移到后面就是后序)
        dfs(ans, node.right); // 右
    }

    // 108. 将有序数组转换为二叉搜索树
    private TreeNode sortedArrayToBST(List<Integer> nums) {
        return buildBST(nums, 0, nums.size());
    }

    // 把 nums[left] 到 nums[right-1] 转成平衡二叉搜索树
    private TreeNode buildBST(List<Integer> nums, int left, int right) {
        if (left == right) {
            return null;
        }
        int m = (left + right) >>> 1;
        return new TreeNode(nums.get(m), buildBST(nums, left, m), buildBST(nums, m + 1, right));
    }
}

###cpp

class Solution {
    // 94. 二叉树的中序遍历
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ans;

        // lambda 递归
        auto dfs = [&](this auto&& dfs, TreeNode* node) -> void {
            if (node == nullptr) {
                return;
            }
            dfs(node->left);          // 左
            ans.push_back(node->val); // 根(这行代码移到前面就是前序,移到后面就是后序)
            dfs(node->right);         // 右
        };

        dfs(root);
        return ans;
    }

    // 108. 将有序数组转换为二叉搜索树
    TreeNode* sortedArrayToBST(vector<int>& nums) {
        // 把 nums[left] 到 nums[right-1] 转成平衡二叉搜索树
        auto dfs = [&](this auto&& dfs, int left, int right) -> TreeNode* {
            if (left == right) {
                return nullptr;
            }
            int m = left + (right - left) / 2;
            return new TreeNode(nums[m], dfs(left, m), dfs(m + 1, right));
        };

        return dfs(0, nums.size());
    }

public:
    TreeNode* balanceBST(TreeNode* root) {
        auto nums = inorderTraversal(root);
        return sortedArrayToBST(nums);
    }
};

###c

// 获取树的大小(节点个数)
int getSize(struct TreeNode* root) {
    if (root == NULL) {
        return 0;
    }
    return 1 + getSize(root->left) + getSize(root->right);
}

// 94. 二叉树的中序遍历
int* inorderTraversal(struct TreeNode* root, int* returnSize) {
    int* ans = malloc(getSize(root) * sizeof(int));
    *returnSize = 0;

    void dfs(struct TreeNode* node) {
        if (node == NULL) {
            return;
        }
        dfs(node->left);                  // 左
        ans[(*returnSize)++] = node->val; // 根(这行代码移到前面就是前序,移到后面就是后序)
        dfs(node->right);                 // 右
    }

    dfs(root);
    return ans;
}

// 108. 将有序数组转换为二叉搜索树
struct TreeNode* sortedArrayToBST(int* nums, int numsSize) {
    // 把 nums[left] 到 nums[right-1] 转成平衡二叉搜索树
    struct TreeNode* dfs(int left, int right) {
        if (left == right) {
            return NULL;
        }
        int m = left + (right - left) / 2;
        struct TreeNode* node = malloc(sizeof(struct TreeNode));
        node->val = nums[m];
        node->left = dfs(left, m);
        node->right = dfs(m + 1, right);
        return node;
    }

    return dfs(0, numsSize);
}

struct TreeNode* balanceBST(struct TreeNode* root) {
    int numsSize;
    int* nums = inorderTraversal(root, &numsSize);
    root = sortedArrayToBST(nums, numsSize);

    free(nums);
    return root;
}

###go

// 94. 二叉树的中序遍历
func inorderTraversal(root *TreeNode) (ans []int) {
var dfs func(*TreeNode)
dfs = func(node *TreeNode) {
if node == nil {
return
}
dfs(node.Left)              // 左
ans = append(ans, node.Val) // 根(这行代码移到前面就是前序,移到后面就是后序)
dfs(node.Right)             // 右
}
dfs(root)
return
}

// 108. 将有序数组转换为二叉搜索树
func sortedArrayToBST(nums []int) *TreeNode {
if len(nums) == 0 {
return nil
}
m := len(nums) / 2
return &TreeNode{
Val:   nums[m],
Left:  sortedArrayToBST(nums[:m]),
Right: sortedArrayToBST(nums[m+1:]),
}
}

func balanceBST(root *TreeNode) *TreeNode {
nums := inorderTraversal(root)
return sortedArrayToBST(nums)
}

###js

// 94. 二叉树的中序遍历
var inorderTraversal = function(root) {
    function dfs(node) {
        if (node === null) {
            return;
        }
        dfs(node.left);     // 左
        ans.push(node.val); // 根(这行代码移到前面就是前序,移到后面就是后序)
        dfs(node.right);    // 右
    }

    const ans = [];
    dfs(root);
    return ans;
};

// 108. 将有序数组转换为二叉搜索树
var sortedArrayToBST = function(nums) {
    // 把 nums[left] 到 nums[right-1] 转成平衡二叉搜索树
    function dfs(left, right) {
        if (left === right) {
            return null;
        }
        const m = Math.floor((left + right) / 2);
        return new TreeNode(nums[m], dfs(left, m), dfs(m + 1, right));
    }
    return dfs(0, nums.length);
};

var balanceBST = function(root) {
    const nums = inorderTraversal(root)
    return sortedArrayToBST(nums)
};

###rust

use std::rc::Rc;
use std::cell::RefCell;

impl Solution {
    // 94. 二叉树的中序遍历
    fn inorder_traversal(root: Option<Rc<RefCell<TreeNode>>>) -> Vec<i32> {
        fn dfs(node: &Option<Rc<RefCell<TreeNode>>>, ans: &mut Vec<i32>) {
            if let Some(node) = node {
                let n = node.borrow();
                dfs(&n.left, ans);  // 左
                ans.push(n.val);    // 根(这行代码移到前面就是前序,移到后面就是后序)
                dfs(&n.right, ans); // 右
            }
        }

        let mut ans = vec![];
        dfs(&root, &mut ans);
        ans
    }

    // 108. 将有序数组转换为二叉搜索树
    fn sorted_array_to_bst(nums: Vec<i32>) -> Option<Rc<RefCell<TreeNode>>> {
        fn dfs(nums: &[i32]) -> Option<Rc<RefCell<TreeNode>>> {
            if nums.is_empty() {
                return None;
            }
            let m = nums.len() / 2;
            Some(Rc::new(RefCell::new(TreeNode {
                val: nums[m],
                left: dfs(&nums[..m]),
                right: dfs(&nums[m + 1..]),
            })))
        }
        dfs(&nums)
    }
    
    pub fn balance_bst(root: Option<Rc<RefCell<TreeNode>>>) -> Option<Rc<RefCell<TreeNode>>> {
        let nums = Self::inorder_traversal(root);
        Self::sorted_array_to_bst(nums)
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是二叉树的节点个数。:Python 的第一种写法有切片的复制开销,二叉树的每一层都需要花费 $\mathcal{O}(n)$ 的时间,一共有 $\mathcal{O}(\log n)$ 层,所以时间复杂度是 $\mathcal{O}(n\log n)$;第二种写法避免了切片的复制开销,时间复杂度是 $\mathcal{O}(n)$。
  • 空间复杂度:$\mathcal{O}(n)$。

专题训练

见下面树题单的「§2.9 二叉搜索树」和「§2.10 创建二叉树」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

【视频】如何灵活运用递归?(Python/Java/C++/C/Go/JS/Rust)

看完这两期视频,让你对递归的理解更上一层楼!

【基础算法精讲 09】

【基础算法精讲 10】

答疑

:代码中的 $-1$ 是怎么产生的?怎么返回的?

:在某次递归中,发现左右子树高度绝对差大于 $1$,我们会返回 $-1$。这个 $-1$ 会一路向上不断返回,直到根节点。

写法一

class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        def get_height(node: Optional[TreeNode]) -> int:
            if node is None:
                return 0
            left_h = get_height(node.left)
            right_h = get_height(node.right)
            if left_h == -1 or right_h == -1 or abs(left_h - right_h) > 1:
                return -1
            return max(left_h, right_h) + 1
        return get_height(root) != -1
class Solution {
    public boolean isBalanced(TreeNode root) {
        return getHeight(root) != -1;
    }

    private int getHeight(TreeNode node) {
        if (node == null) {
            return 0;
        }
        int leftH = getHeight(node.left);
        int rightH = getHeight(node.right);
        if (leftH == -1 || rightH == -1 || Math.abs(leftH - rightH) > 1) {
            return -1;
        }
        return Math.max(leftH, rightH) + 1;
    }
}
class Solution {
    int get_height(TreeNode* node) {
        if (node == nullptr) {
            return 0;
        }
        int left_h = get_height(node->left);
        int right_h = get_height(node->right);
        if (left_h == -1 || right_h == -1 || abs(left_h - right_h) > 1) {
            return -1;
        }
        return max(left_h, right_h) + 1;
    }

public:
    bool isBalanced(TreeNode* root) {
        return get_height(root) != -1;
    }
};
#define MAX(a, b) ((b) > (a) ? (b) : (a))

int getHeight(struct TreeNode* node) {
    if (node == NULL) {
        return 0;
    }
    int left_h = getHeight(node->left);
    int right_h = getHeight(node->right);
    if (left_h == -1 || right_h == -1 || abs(left_h - right_h) > 1) {
        return -1;
    }
    return MAX(left_h, right_h) + 1;
}

bool isBalanced(struct TreeNode* root) {
    return getHeight(root) != -1;
}
func getHeight(node *TreeNode) int {
    if node == nil {
        return 0
    }
    leftH := getHeight(node.Left)
    rightH := getHeight(node.Right)
    if leftH == -1 || rightH == -1 || abs(leftH-rightH) > 1 {
        return -1
    }
    return max(leftH, rightH) + 1
}

func isBalanced(root *TreeNode) bool {
    return getHeight(root) != -1
}

func abs(x int) int { if x < 0 { return -x }; return x }
function getHeight(node) {
    if (node === null) {
        return 0;
    }
    const leftH = getHeight(node.left);
    const rightH = getHeight(node.right);
    if (leftH === -1 || rightH === -1 || Math.abs(leftH - rightH) > 1) {
        return -1;
    }
    return Math.max(leftH, rightH) + 1;
}

var isBalanced = function(root) {
    return getHeight(root) !== -1;
};
use std::rc::Rc;
use std::cell::RefCell;

impl Solution {
    pub fn is_balanced(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
        fn get_height(node: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
            if let Some(node) = node {
                let node = node.borrow();
                let left_h = get_height(&node.left);
                let right_h = get_height(&node.right);
                if left_h == -1 || right_h == -1 || (left_h - right_h).abs() > 1 {
                    return -1;
                }
                return left_h.max(right_h) + 1;
            }
            0
        }
        get_height(&root) != -1
    }
}

写法二

class Solution:
    def isBalanced(self, root: Optional[TreeNode]) -> bool:
        def get_height(node: Optional[TreeNode]) -> int:
            if node is None:
                return 0
            left_h = get_height(node.left)
            if left_h == -1:
                return -1  # 提前退出,不再递归
            right_h = get_height(node.right)
            if right_h == -1 or abs(left_h - right_h) > 1:
                return -1
            return max(left_h, right_h) + 1
        return get_height(root) != -1
class Solution {
    public boolean isBalanced(TreeNode root) {
        return getHeight(root) != -1;
    }

    private int getHeight(TreeNode node) {
        if (node == null) {
            return 0;
        }
        int leftH = getHeight(node.left);
        if (leftH == -1) {
            return -1; // 提前退出,不再递归
        }
        int rightH = getHeight(node.right);
        if (rightH == -1 || Math.abs(leftH - rightH) > 1) {
            return -1;
        }
        return Math.max(leftH, rightH) + 1;
    }
}
class Solution {
    int get_height(TreeNode* node) {
        if (node == nullptr) {
            return 0;
        }
        int left_h = get_height(node->left);
        if (left_h == -1) {
            return -1; // 提前退出,不再递归
        }
        int right_h = get_height(node->right);
        if (right_h == -1 || abs(left_h - right_h) > 1) {
            return -1;
        }
        return max(left_h, right_h) + 1;
    }

public:
    bool isBalanced(TreeNode* root) {
        return get_height(root) != -1;
    }
};
#define MAX(a, b) ((b) > (a) ? (b) : (a))

int getHeight(struct TreeNode* node) {
    if (node == NULL) {
        return 0;
    }

    int left_h = getHeight(node->left);
    if (left_h == -1) {
        return -1; // 提前退出,不再递归
    }

    int right_h = getHeight(node->right);
    if (right_h == -1 || abs(left_h - right_h) > 1) {
        return -1;
    }

    return MAX(left_h, right_h) + 1;
}

bool isBalanced(struct TreeNode* root) {
    return getHeight(root) != -1;
}
func getHeight(node *TreeNode) int {
    if node == nil {
        return 0
    }
    leftH := getHeight(node.Left)
    if leftH == -1 {
        return -1 // 提前退出,不再递归
    }
    rightH := getHeight(node.Right)
    if rightH == -1 || abs(leftH-rightH) > 1 {
        return -1
    }
    return max(leftH, rightH) + 1
}

func isBalanced(root *TreeNode) bool {
    return getHeight(root) != -1
}

func abs(x int) int { if x < 0 { return -x }; return x }
function getHeight(node) {
    if (node === null) {
        return 0;
    }
    const leftH = getHeight(node.left);
    if (leftH === -1) {
        return -1; // 提前退出,不再递归
    }
    const rightH = getHeight(node.right);
    if (rightH === -1 || Math.abs(leftH - rightH) > 1) {
        return -1;
    }
    return Math.max(leftH, rightH) + 1;
}

var isBalanced = function(root) {
    return getHeight(root) !== -1;
};
use std::rc::Rc;
use std::cell::RefCell;

impl Solution {
    pub fn is_balanced(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
        fn get_height(node: &Option<Rc<RefCell<TreeNode>>>) -> i32 {
            if let Some(node) = node {
                let node = node.borrow();
                let left_h = get_height(&node.left);
                if left_h == -1 {
                    return -1; // 提前退出,不再递归
                }
                let right_h = get_height(&node.right);
                if right_h == -1 || (left_h - right_h).abs() > 1 {
                    return -1;
                }
                return left_h.max(right_h) + 1;
            }
            0
        }
        get_height(&root) != -1
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 为二叉树的节点个数。
  • 空间复杂度:$\mathcal{O}(n)$。最坏情况下,二叉树退化成一条链,递归需要 $\mathcal{O}(n)$ 的栈空间。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

前后缀分解,一张图秒懂!(附动态规划)Python/Java/C++/Go

方法一:前后缀分解(两次遍历)

1653-2-cut3.png

答疑

:为什么把 if-else 写成 (c - 'a') * 2 - 1 会快很多?

:CPU 在遇到分支(条件跳转指令)时会预测代码要执行哪个分支,如果预测正确,CPU 就会继续按照预测的路径执行程序。但如果预测失败,CPU 就需要回滚之前的指令并加载正确的指令,以确保程序执行的正确性。

对于本题的数据,字符 $\text{a'}$ 和 $\text{b'}$ 可以认为是随机出现的,在这种情况下分支预测就会有 $50%$ 的概率失败。失败导致的回滚和加载操作需要消耗额外的 CPU 周期,如果能用较小的代价去掉分支,对于本题的情况必然可以带来效率上的提升。

注:这种优化方法会降低可读性,不建议在业务代码中使用。

class Solution:
    def minimumDeletions(self, s: str) -> int:
        ans = delete = s.count('a')
        for c in s:
            delete -= 1 if c == 'a' else -1
            if delete < ans:  # 手动计算 min 会快很多
                ans = delete
        return ans
class Solution {
    public int minimumDeletions(String S) {
        char[] s = S.toCharArray();
        int del = 0;
        for (char c : s) {
            del += 'b' - c; // 统计 'a' 的个数
        }

        int ans = del;
        for (char c : s) {
            // 'a' -> -1    'b' -> 1
            del += (c - 'a') * 2 - 1;
            ans = Math.min(ans, del);
        }
        return ans;
    }
}
class Solution {
public:
    int minimumDeletions(string s) {
        int del = 0;
        for (char c : s) {
            del += 'b' - c; // 统计 'a' 的个数
        }

        int ans = del;
        for (char c : s) {
            // 'a' -> -1    'b' -> 1
            del += (c - 'a') * 2 - 1;
            ans = min(ans, del);
        }
        return ans;
    }
};
func minimumDeletions(s string) int {
del := strings.Count(s, "a")
ans := del
for _, c := range s {
// 'a' -> -1    'b' -> 1
del += int((c-'a')*2 - 1)
ans = min(ans, del)
}
return ans
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $s$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

方法二:动态规划(一次遍历)

如果你还不熟悉动态规划(包括空间优化),可以先看看 动态规划入门

考虑 $s$ 的最后一个字母:

  • 如果它是 $\text{`b'}$,则无需删除,问题规模缩小,变成「使 $s$ 的前 $n-1$ 个字母平衡的最少删除次数」。
  • 如果它是 $\text{`a'}$:
    • 删除它,则答案为「使 $s$ 的前 $n-1$ 个字母平衡的最少删除次数」加上 $1$。
    • 保留它,那么前面的所有 $\text{`b'}$ 都要删除;

设 $\textit{cntB}_i$ 为前 $i$ 个字母中 $\text{`b'}$ 的个数。定义 $f_i$ 表示使 $s$ 的前 $i$ 个字母平衡的最少删除次数:

  • 如果第 $i$ 个字母是 $\text{`b'}$,则 $f_i = f_{i-1}$;
  • 如果第 $i$ 个字母是 $\text{`a'}$,则 $f_i = \min(f_{i-1}+1,\textit{cntB}_i)$。
class Solution:
    def minimumDeletions(self, s: str) -> int:
        f = cnt_b = 0
        for c in s:
            if c == 'b':
                cnt_b += 1  # f 值不变
            else:
                f = min(f + 1, cnt_b)
        return f
class Solution {
    public int minimumDeletions(String s) {
        int f = 0;
        int cntB = 0;
        for (char c : s.toCharArray()) {
            if (c == 'b') {
                cntB++; // f 值不变
            } else {
                f = Math.min(f + 1, cntB);
            }
        }
        return f;
    }
}
class Solution {
public:
    int minimumDeletions(string s) {
        int f = 0, cnt_b = 0;
        for (char c : s) {
            if (c == 'b') {
                cnt_b++; // f 值不变
            } else {
                f = min(f + 1, cnt_b);
            }
        }
        return f;
    }
};
func minimumDeletions(s string) int {
    f, cntB := 0, 0
    for _, c := range s {
        if c == 'b' { // f 值不变
            cntB++
        } else {
            f = min(f+1, cntB)
        }
    }
    return f
}

这份代码也可以像方法一那样去掉分支:

  • 如果第 $i$ 个字母是 $\text{b'}$,则 $\textit{cntB}$ 增加 $1$,$f[i] = \min(f[i-1],\textit{cntB})$,这里也考虑全部删除 $\text{b'}$;
  • 如果第 $i$ 个字母是 $\text{`a'}$,则 $\textit{cntB}$ 增加 $0$,$f[i] = \min(f[i-1]+1,\textit{cntB})$。

这两种情况可以合并成:

设当前字母为 $c$,$x=c-\text{`a'}$,则 $\textit{cntB}$ 增加 $x$,$f[i] = \min(f[i-1]+(x\oplus 1),\textit{cntB})$。其中 $\oplus$ 表示异或。

class Solution:
    def minimumDeletions(self, s: str) -> int:
        f = cnt_b = 0
        for c in s:
            x = ord(c) - ord('a')  # ord 很慢
            cnt_b += x
            f = min(f + (x ^ 1), cnt_b)
        return f
class Solution {
    public int minimumDeletions(String s) {
        int f = 0;
        int cntB = 0;
        for (char c : s.toCharArray()) {
            int x = c - 'a';
            cntB += x;
            f = Math.min(f + (x ^ 1), cntB);
        }
        return f;
    }
}
class Solution {
public:
    int minimumDeletions(string s) {
        int f = 0, cnt_b = 0;
        for (char c : s) {
            int x = c - 'a';
            cnt_b += x;
            f = min(f + (x ^ 1), cnt_b);
        }
        return f;
    }
};
func minimumDeletions(s string) int {
    f, cntB := 0, 0
    for _, c := range s {
        x := int(c - 'a')
        cntB += x
        f = min(f+(x^1), cntB)
    }
    return f
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $s$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。

专题训练

见下面动态规划题单的「专题:前后缀分解」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

简洁写法(Python/Java/C++/C/Go/JS/Rust)

操作相当于从下标 $i$ 移动到下标 $i+\textit{nums}[i]$。

如果 $i+\textit{nums}[i]$ 下标越界呢?

需要把 $i+\textit{nums}[i]$ 调整到 $[0,n-1]$ 范围中。具体来说,把下标 $i+\textit{nums}[i]$ 模 $n$。比如 $n=4$,在循环数组中,正数下标 $5,9,13,\ldots$ 都是下标 $1$,负数下标 $-3,-7,-11,\ldots$ 也都是下标 $1$。

不了解取模的同学,请看 模运算的世界:当加减乘除遇上取模

本题视频讲解,欢迎点赞关注~

###py

class Solution:
    def constructTransformedArray(self, nums: List[int]) -> List[int]:
        n = len(nums)
        return [nums[(i + x) % n] for i, x in enumerate(nums)]

###java

class Solution {
    public int[] constructTransformedArray(int[] nums) {
        int n = nums.length;
        int[] result = new int[n];
        for (int i = 0; i < n; i++) {
            result[i] = nums[((i + nums[i]) % n + n) % n]; // 保证结果在 [0,n-1] 中
        }
        return result;
    }
}

###cpp

class Solution {
public:
    vector<int> constructTransformedArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> result(n);
        for (int i = 0; i < n; i++) {
            result[i] = nums[((i + nums[i]) % n + n) % n]; // 保证结果在 [0,n-1] 中
        }
        return result;
    }
};

###c

int* constructTransformedArray(int* nums, int numsSize, int* returnSize) {
    int n = numsSize;
    int* result = malloc(n * sizeof(int));
    for (int i = 0; i < n; i++) {
        result[i] = nums[((i + nums[i]) % n + n) % n]; // 保证结果在 [0,n-1] 中
    }
    *returnSize = n;
    return result;
}

###go

func constructTransformedArray(nums []int) []int {
n := len(nums)
result := make([]int, n)
for i, x := range nums {
result[i] = nums[((i+x)%n+n)%n] // 保证结果在 [0,n-1] 中
}
return result
}

###js

var constructTransformedArray = function(nums) {
    const n = nums.length;
    const result = new Array(n);
    for (let i = 0; i < n; i++) {
        result[i] = nums[((i + nums[i]) % n + n) % n]; // 保证结果在 [0,n-1] 中
    }
    return result;
};

###rust

impl Solution {
    pub fn construct_transformed_array(nums: Vec<i32>) -> Vec<i32> {
        let n = nums.len();
        let m = n as i32;
        let mut result = vec![0; n];
        for i in 0..n {
            let j = ((i as i32 + nums[i]) % m + m) % m; // 保证结果在 [0,n-1] 中
            result[i] = nums[j as usize];
        }
        result
    }
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(n)$,其中 $n$ 是 $\textit{nums}$ 的长度。
  • 空间复杂度:$\mathcal{O}(1)$。返回值不计入。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/基环树/最短路/最小生成树/网络流)
  7. 动态规划(入门/背包/划分/状态机/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

❌