阅读视图

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

简单题,简单做(Python/Java/C++/C/Go/JS/Rust)

题意:计算 $\textit{nums}$ 每个前缀的二进制数值 $x$,判断 $x$ 是否为 $5$ 的倍数。

比如 $\textit{nums}=[1,1,0,1]$,每个前缀对应的二进制数分别为 $1,11,110,1101$。

如何计算这些二进制数呢?

在十进制中,我们往 $12$ 的右边添加 $3$,得到 $123$,做法是 $12\cdot 10 + 3 = 123$。

对于二进制,做法类似,往 $110$ 的右边添加 $1$,得到 $1101$,做法是 $110\cdot 2 + 1 = 1101$,或者 $110\ \texttt{<<}\ 1\ |\ 1 = 1101$。

注意本题 $\textit{nums}$ 很长,算出的二进制数 $x$ 很大,但我们只需要判断 $x\bmod 5=0$ 是否成立。可以在中途取模,也就是每次循环计算出新的 $x$ 后,把 $x$ 替换成 $x\bmod 5$。为什么可以在中途取模?原理见 模运算的世界:当加减乘除遇上取模

###py

class Solution:
    def prefixesDivBy5(self, nums: List[int]) -> List[bool]:
        ans = [False] * len(nums)
        x = 0
        for i, bit in enumerate(nums):
            x = (x << 1 | bit) % 5
            ans[i] = x == 0
        return ans

###java

class Solution {
    public List<Boolean> prefixesDivBy5(int[] nums) {
        List<Boolean> ans = new ArrayList<>(nums.length); // 预分配空间
        int x = 0;
        for (int bit : nums) {
            x = (x << 1 | bit) % 5;
            ans.add(x == 0);
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    vector<bool> prefixesDivBy5(vector<int>& nums) {
        vector<bool> ans(nums.size());
        int x = 0;
        for (int i = 0; i < nums.size(); i++) {
            x = (x << 1 | nums[i]) % 5;
            ans[i] = x == 0;
        }
        return ans;
    }
};

###c

bool* prefixesDivBy5(int* nums, int numsSize, int* returnSize) {
    *returnSize = numsSize;
    bool* ans = malloc(numsSize * sizeof(bool));
    int x = 0;
    for (int i = 0; i < numsSize; i++) {
        x = (x << 1 | nums[i]) % 5;
        ans[i] = x == 0;
    }
    return ans;
}

###go

func prefixesDivBy5(nums []int) []bool {
ans := make([]bool, len(nums))
x := 0
for i, bit := range nums {
x = (x<<1 | bit) % 5
ans[i] = x == 0
}
return ans
}

###js

var prefixesDivBy5 = function(nums) {
    const ans = new Array(nums.length);
    let x = 0;
    for (let i = 0; i < nums.length; i++) {
        x = ((x << 1) | nums[i]) % 5;
        ans[i] = x === 0;
    }
    return ans;
};

###rust

impl Solution {
    pub fn prefixes_div_by5(nums: Vec<i32>) -> Vec<bool> {
        let mut ans = vec![false; nums.len()];
        let mut x = 0;
        for (i, bit) in nums.into_iter().enumerate() {
            x = (x << 1 | bit) % 5;
            ans[i] = x == 0;
        }
        ans
    }
}

复杂度分析

  • 时间复杂度:$\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站@灵茶山艾府

每日一题-可被 5 整除的二进制前缀🟢

给定一个二进制数组 nums索引从0开始 )。

我们将xi 定义为其二进制表示形式为子数组 nums[0..i] (从最高有效位到最低有效位)。

  • 例如,如果 nums =[1,0,1] ,那么 x0 = 1x1 = 2, 和 x2 = 5

返回布尔值列表 answer,只有当 xi 可以被 5 整除时,答案 answer[i] 为 true,否则为 false

 

示例 1:

输入:nums = [0,1,1]
输出:[true,false,false]
解释:
输入数字为 0, 01, 011;也就是十进制中的 0, 1, 3 。只有第一个数可以被 5 整除,因此 answer[0] 为 true 。

示例 2:

输入:nums = [1,1,1]
输出:[false,false,false]

 

提示:

  • 1 <= nums.length <= 105 
  • nums[i] 仅为 0 或 1

有限状态机

有限状态机

状态对应(mod 5),箭头表示状态转移,转移函数是(当前状态*2+二进制数末位)%5
image.png

代码

###rust

impl Solution {
    pub fn prefixes_div_by5(a: Vec<i32>) -> Vec<bool> {
        let mut state: i32 = 0;
        let mut result = vec![];
        let stateSet = [[0, 1], [2, 3], [4, 0], [1, 2], [3, 4]];
        for i in a {
            state = stateSet[state as usize][i as usize];
            result.push(state == 0);
        }
        result
    }
}

python3 超详细多解法

解法一:二进制+数组+暴力法(超时)

思路:

直接判断每个二进制前缀是否被5整除。

超时代码:

###python3

class Solution:
    def prefixesDivBy5(self, A: List[int]) -> List[bool]:
        return [not int("".join(map(str, A[:i])), 2)%5 for i in range(1, len(A)+1)]

解法二:二进制+数组+暴力法优化(通过)

思路:

判断每个二进制前缀是否被5整除,用变量保存当前二进制前缀。

通过代码一:(这个思路速度很慢,超过20%)

###python3

class Solution:
    def prefixesDivBy5(self, A: List[int]) -> List[bool]:
        a = ""
        res = []
        for i in A:
            a += str(i)
            res.append(not int(a, 2)%5)
        return res

解法三:二进制+数组+位运算(通过)

思路:

利用变量保存当前二进制前缀的值,每次判断是否被5整除,并改变保存当前二进制前缀的值的变量。

二进制增加:

先将前面的数乘2,再加上新增数。

通过代码二:(这个思路速度还不错,超过70%)

###python3

class Solution:
    def prefixesDivBy5(self, A: List[int]) -> List[bool]:
        res = []
        pre = 0
        for i in A:
            pre = (pre<<1)+i
            res.append(not pre%5)
        return res

解法四:二进制+数组+位运算优化(通过)

思路:

利用变量保存当前二进制前缀的值,每次判断是否被5整除,改变保存当前二进制前缀的值的变量并对5取余。

二进制增加:

先将前面的数乘2,再加上新增数。

取余原因:

因为n*2%5 == n%5*2%5,所以取余,这样就不用保存非常大的数了。

通过代码三:(这个思路速度很快,超过97.5%)

###python3

class Solution:
    def prefixesDivBy5(self, A: List[int]) -> List[bool]:
        res = []
        pre = 0
        for i in A:
            pre = ((pre<<1)+i)%5
            res.append(not pre)
        return res

可被 5 整除的二进制前缀

方法一:遍历

令 $\textit{num}_i$ 为数组 $\textit{nums}$ 从下标 $0$ 到下标 $i$ 的前缀表示的二进制数,则有 $\textit{num}_0=\textit{nums}[0]$。当 $i>0$ 时,有 $\textit{num}i=\textit{num}{i-1} \times 2+\textit{nums}[i]$。令 $n$ 为数组 $\textit{nums}$ 的长度,则对于 $0 \le i<n$,可以依次计算每个 $\textit{num}_i$ 的值。对于每个 $\textit{num}_i$ 的值,判断其是否可以被 $5$ 整除,即可得到答案。

考虑到数组 $\textit{nums}$ 可能很长,如果每次都保留 $\textit{num}_i$ 的值,则可能导致溢出。由于只需要知道每个 $\textit{num}_i$ 是否可以被 $5$ 整除,因此在计算过程中只需要保留余数即可。

令 $\textit{remain}_i$ 表示计算到下标 $i$ 时的除以 $5$ 的余数,则有 $\textit{remain}_0=\textit{nums}[0]$(显然 $\textit{nums}[0]$ 一定小于 $5$),当 $i>0$ 时,有 $\textit{remain}i=(\textit{remain}{i-1} \times 2+\textit{nums}[i])\bmod 5$,判断每个 $\textit{remain}_i$ 是否为 $0$ 即可。由于 $\textit{remain}_i$ 一定小于 $5$,因此不会溢出。

如何证明判断 $\textit{remain}_i$ 是否为 $0$ 可以得到正确的结果?可以通过数学归纳法证明。

当 $i=0$ 时,由于 $\textit{num}_0=\textit{nums}[0]<5$,因此 $\textit{remain}_0=\textit{num}_0$,$\textit{remain}_0=\textit{num}_0\bmod 5$ 成立。

当 $i>0$ 时,假设 $\textit{remain}{i-1}=\textit{num}{i-1}\bmod 5$ 成立,考虑 $\textit{num}_i\bmod 5$ 和 $\textit{remain}_i$ 的值:

$$
\begin{aligned}
\textit{num}i\bmod 5=&(\textit{num}{i-1} \times 2+\textit{nums}[i])\bmod 5 \
=&(\textit{num}{i-1} \times 2)\bmod 5+\textit{nums}[i]\bmod 5 \
\
\textit{remain}i=&(\textit{remain}{i-1} \times 2+\textit{nums}[i])\bmod 5 \
=&(\textit{num}
{i-1}\bmod 5 \times 2+\textit{nums}[i])\bmod 5 \
=&(\textit{num}{i-1}\bmod 5 \times 2)\bmod 5+\textit{nums}[i]\bmod 5 \
=&(\textit{num}
{i-1} \times 2)\bmod 5+\textit{nums}[i]\bmod 5
\end{aligned}
$$

因此有 $\textit{remain}_i=\textit{num}_i\bmod 5$ 成立。

由此可得,对任意 $0 \le i<n$,都有 $\textit{remain}_i=\textit{num}_i\bmod 5$,因此计算 $\textit{remain}_i$ 即可得到正确的结果。

###Java

class Solution {
    public List<Boolean> prefixesDivBy5(int[] nums) {
        List<Boolean> answer = new ArrayList<Boolean>();
        int prefix = 0;
        int length = nums.length;
        for (int i = 0; i < length; i++) {
            prefix = ((prefix << 1) + nums[i]) % 5;
            answer.add(prefix == 0);
        }
        return answer;
    }
}

###C++

class Solution {
public:
    vector<bool> prefixesDivBy5(vector<int>& nums) {
        vector<bool> answer;
        int prefix = 0;
        int length = nums.size();
        for (int i = 0; i < length; i++) {
            prefix = ((prefix << 1) + nums[i]) % 5;
            answer.emplace_back(prefix == 0);
        }
        return answer;
    }
};

###JavaScript

var prefixesDivBy5 = function(nums) {
    const answer = [];
    let prefix = 0;
    const length = nums.length;
    for (let i = 0; i < length; i++) {
        prefix = ((prefix << 1) + nums[i]) % 5;
        answer.push(prefix === 0);
    }
    return answer;
};

###Python

class Solution:
    def prefixesDivBy5(self, nums: List[int]) -> List[bool]:
        answer = list()
        prefix = 0
        for num in nums:
            prefix = ((prefix << 1) + num) % 5
            answer.append(prefix == 0)
        return answer

###go

func prefixesDivBy5(nums []int) []bool {
    ans := make([]bool, len(nums))
    x := 0
    for i, v := range nums {
        x = (x<<1 | v) % 5
        ans[i] = x == 0
    }
    return ans
}

###C

bool* prefixesDivBy5(int* nums, int numsSize, int* returnSize) {
    *returnSize = numsSize;
    bool* answer = malloc(sizeof(bool) * numsSize);
    int prefix = 0;
    for (int i = 0; i < numsSize; i++) {
        prefix = ((prefix << 1) + nums[i]) % 5;
        answer[i] = prefix == 0;
    }
    return answer;
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。需要遍历数组一次并计算前缀。

  • 空间复杂度:$O(1)$。除了返回值以外,额外使用的空间为常数。

每日一题-可被三整除的最大和🟡

给你一个整数数组 nums,请你找出并返回能被三整除的元素 最大和

     

    示例 1:

    输入:nums = [3,6,5,1,8]
    输出:18
    解释:选出数字 3, 6, 1 和 8,它们的和是 18(可被 3 整除的最大和)。

    示例 2:

    输入:nums = [4]
    输出:0
    解释:4 不能被 3 整除,所以无法选出数字,返回 0。
    

    示例 3:

    输入:nums = [1,2,3,4,4]
    输出:12
    解释:选出数字 1, 3, 4 以及 4,它们的和是 12(可被 3 整除的最大和)。
    

     

    提示:

    • 1 <= nums.length <= 4 * 104
    • 1 <= nums[i] <= 104

    两种算法:贪心/动态规划(Python/Java/C++/Go)

    方法一:贪心

    由于数组中没有负数,如果整个数组的元素和 $s$ 可以被 $3$ 整除,那么 $s$ 就是最大的元素和。

    否则,如果 $s$ 不能被 $3$ 整除,那就看看能否让 $s$ 减去某些 $\textit{nums}[i]$,使得 $s$ 可以被 $3$ 整除。

    找到所有 $\textit{nums}[i]\bmod 3 = 1$ 的 $\textit{nums}[i]$,放到数组 $a_1$ 中;找到所有 $\textit{nums}[i]\bmod 3 = 2$ 的 $\textit{nums}[i]$,放到数组 $a_2$ 中。

    对 $a_1$ 和 $a_2$ 从小到大排序。分类讨论:

    • 如果 $s\bmod 3 = 1$:
      • 如果 $a_1$ 不为空,那么答案可能是 $s-a_1[0]$;
      • 如果 $a_2$ 中至少有两个数,那么答案可能是 $s-a_2[0]-a_2[1]$;
      • 这两种情况取最大值。
      • 如果没有这样的数,返回 $0$。
    • 如果 $s\bmod 3 = 2$:
      • 如果 $a_2$ 不为空,那么答案可能是 $s-a_2[0]$;
      • 如果 $a_1$ 中至少有两个数,那么答案可能是 $s-a_1[0]-a_1[1]$;
      • 这两种情况取最大值。
      • 如果没有这样的数,返回 $0$。

    代码实现时,如果 $s\bmod 3 = 2$,那么可以交换数组 $a_1$ 和 $a_2$,从而复用同一套逻辑。

    但是,贪心算法是有局限的。试想一下,如果把题目中的 $3$ 换成 $4$,要如何分类讨论?换成 $5$,又要如何分类讨论?随着数字的变大,要讨论的内容越来越复杂。那么,是否有更加通用的做法呢?请继续阅读。

    class Solution:
        def maxSumDivThree(self, nums: List[int]) -> int:
            s = sum(nums)
            if s % 3 == 0:
                return s
            a1 = sorted(x for x in nums if x % 3 == 1)
            a2 = sorted(x for x in nums if x % 3 == 2)
            if s % 3 == 2:
                a1, a2 = a2, a1
            ans = s - a1[0] if a1 else 0
            if len(a2) > 1:
                ans = max(ans, s - a2[0] - a2[1])
            return ans
    
    class Solution {
        public int maxSumDivThree(int[] nums) {
            int s = 0;
            for (int x : nums)
                s += x;
            if (s % 3 == 0)
                return s;
    
            var a1 = new ArrayList<Integer>();
            var a2 = new ArrayList<Integer>();
            for (int x : nums) {
                if (x % 3 == 1) a1.add(x);
                else if (x % 3 == 2) a2.add(x);
            }
            Collections.sort(a1);
            Collections.sort(a2);
    
            if (s % 3 == 2) { // swap(a1,a2)
                var tmp = a1;
                a1 = a2;
                a2 = tmp;
            }
            int ans = a1.isEmpty() ? 0 : s - a1.get(0);
            if (a2.size() > 1)
                ans = Math.max(ans, s - a2.get(0) - a2.get(1));
            return ans;
        }
    }
    
    class Solution {
    public:
        int maxSumDivThree(vector<int> &nums) {
            int s = accumulate(nums.begin(), nums.end(), 0);
            if (s % 3 == 0)
                return s;
    
            vector<int> a[3];
            for (int x: nums)
                a[x % 3].push_back(x);
            sort(a[1].begin(), a[1].end());
            sort(a[2].begin(), a[2].end());
    
            if (s % 3 == 2)
                swap(a[1], a[2]);
            int ans = a[1].size() ? s - a[1][0] : 0;
            if (a[2].size() > 1)
                ans = max(ans, s - a[2][0] - a[2][1]);
            return ans;
        }
    };
    
    func maxSumDivThree(nums []int) (ans int) {
        s := 0
        for _, x := range nums {
            s += x
        }
        if s%3 == 0 {
            return s
        }
    
        a := [3][]int{}
        for _, x := range nums {
            a[x%3] = append(a[x%3], x)
        }
        sort.Ints(a[1])
        sort.Ints(a[2])
    
        if s%3 == 2 {
            a[1], a[2] = a[2], a[1]
        }
        if len(a[1]) > 0 {
            ans = s - a[1][0]
        }
        if len(a[2]) > 1 {
            ans = max(ans, s-a[2][0]-a[2][1])
        }
        return
    }
    
    func max(a, b int) int { if a < b { return b }; return a }
    

    复杂度分析

    注:由于只要最小的两个数,可以做到 $\mathcal{O}(n)$ 时间和 $\mathcal{O}(1)$ 额外空间,但写起来较为复杂。考虑到动态规划的做法同样可以做到 $\mathcal{O}(n)$ 时间和 $\mathcal{O}(1)$ 额外空间,所以这里就不进一步优化了。

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

    方法二:动态规划

    前置知识:动态规划入门

    详见 动态规划入门:从记忆化搜索到递推【基础算法精讲 17】

    一、寻找子问题

    用「选或不选」的思路,考虑最后一个数 $x = \textit{nums}[n-1]$:

    • 如果 $x\bmod 3= 0$,选 $x$ 不影响结果,一定要选(不选白不选),问题变成从 $\textit{nums}[0]$ 到 $\textit{nums}[n-2]$ 中寻找能被 $3$ 整除的元素最大和。你也可以从结果的角度考虑,结果是 $3$ 的倍数,如果 $\textit{nums}$ 中有一个 $3$ 的倍数没有选,岂不是白白浪费?
    • 如果 $x\bmod 3= 1$:
      • 如果不选 $x$,和上面一样,问题变成从 $\textit{nums}[0]$ 到 $\textit{nums}[n-2]$ 中寻找能被 $3$ 整除的元素最大和 $s_0$。
      • 如果选 $x$,问题变成从 $\textit{nums}[0]$ 到 $\textit{nums}[n-2]$ 中寻找最大元素和 $s_2$,满足 $s_2\bmod 3 = 2$。
      • 答案为 $\max(s_0, s_2 + x)$。
    • 如果 $x\bmod 3= 2$:
      • 如果不选 $x$,和上面一样,问题变成从 $\textit{nums}[0]$ 到 $\textit{nums}[n-2]$ 中寻找能被 $3$ 整除的元素最大和 $s_0$。
      • 如果选 $x$,问题变成从 $\textit{nums}[0]$ 到 $\textit{nums}[n-2]$ 中寻找最大元素和 $s_1$,满足 $s_1\bmod 3 = 1$。
      • 答案为 $\max(s_0, s_1 + x)$。

    上述讨论,刻画了这道题的两个重要参数:

    • $i$:表示从 $\textit{nums}[0]$ 到 $\textit{nums}[i]$ 中选数。
    • $j$:表示所选数字之和 $s$ 需要满足 $s\bmod 3 = j$。

    那么原问题就是 $(i=n-1, j=0)$,上述讨论得到的子问题有 $(i=n-2,j=0),\ (i=n-2,j=1),\ (i=n-2,j=2)$。

    注:为什么要从最后一个数开始讨论?主要是为了方便后面把记忆化搜索改成递推。当然,从第一个数开始讨论也是可以的。

    二、状态定义与状态转移方程

    根据上面的讨论,定义 $\textit{dfs}(i,j)$ 表示从 $\textit{nums}[0]$ 到 $\textit{nums}[i]$ 中选数,后续还需要选的数字之和 $s$ 满足 $s\bmod 3 = j$ 的前提下,$s$ 的最大值。

    设 $x=\textit{nums}[i]$,分类讨论:

    • 如果不选 $x$,问题变成从 $\textit{nums}[0]$ 到 $\textit{nums}[i-1]$ 中选数,所选数字之和 $s$ 满足 $s\bmod 3 = j$ 的前提下,$s$ 的最大值。即 $\textit{dfs}(i,j) = \textit{dfs}(i-1,j)$。
    • 如果选 $x$,问题变成从 $\textit{nums}[0]$ 到 $\textit{nums}[i-1]$ 中选数,所选数字之和 $s$ 满足 $(s+x)\bmod 3 = j$,即 $s\bmod 3 = (j-x)\bmod 3$ 的前提下,$s$ 的最大值。即 $\textit{dfs}(i,j) = \textit{dfs}(i-1,(j-x)\bmod 3) + x$。

    这两种情况取最大值,有

    $$
    \textit{dfs}(i,j) = \max(\textit{dfs}(i-1,j),\textit{dfs}(i-1,(j-x)\bmod 3) + x)
    $$

    注意,如果 $(j-x)\bmod 3 < 0$,需要再 $+3$ 调整到 $[0,2]$ 内。考虑到这样写有些麻烦,不妨把 $j$ 的定义改为已选数字之和 $\bmod\ 3 = j$。(注意这里的定义是已经选的,上面定义的是还需要选的。)

    这样修改后,不选 $x$ 仍然是 $\textit{dfs}(i,j) = \textit{dfs}(i-1,j)$;选 $x$ 就是 $\textit{dfs}(i,j) = \textit{dfs}(i-1,(j+x)\bmod 3)$ 了。

    这两种情况取最大值,有

    $$
    \textit{dfs}(i,j) = \max(\textit{dfs}(i-1,j),\textit{dfs}(i-1,(j+x)\bmod 3) + x)
    $$

    递归边界:$\textit{dfs}(-1,0)=0,\textit{dfs}(-1,1)=-\infty,\textit{dfs}(-1,2)=-\infty$。我们需要保证所选数字之和是 $3$ 的倍数,否则不符合题目要求。注意,如果没有选任何数字,那么会递归到 $\textit{dfs}(-1,0)$,得到 $0$,这是符合题目要求的。

    注:$-\infty$ 表示非法方案,这样后面取 $\max$ 的时候会自动排除非法方案。

    递归入口:$\textit{dfs}(n-1,0)$。

    class Solution:
        def maxSumDivThree(self, nums: List[int]) -> int:
            @cache  # 记忆化搜索
            def dfs(i: int, j: int) -> int:
                if i < 0: return -inf if j else 0
                return max(dfs(i - 1, j), dfs(i - 1, (j + nums[i]) % 3) + nums[i])
            return dfs(len(nums) - 1, 0)
    
    class Solution {
        public int maxSumDivThree(int[] nums) {
            int n = nums.length;
            int[][] memo = new int[n][3];
            for (int i = 0; i < n; i++)
                Arrays.fill(memo[i], -1); // -1 表示没有计算过
            return dfs(memo, nums, n - 1, 0);
        }
    
        private int dfs(int[][] memo, int[] nums, int i, int j) {
            if (i < 0) return j == 0 ? 0 : Integer.MIN_VALUE;
            if (memo[i][j] != -1) return memo[i][j]; // 之前计算过
            return memo[i][j] = Math.max(dfs(memo, nums, i - 1, j),
                    dfs(memo, nums, i - 1, (j + nums[i]) % 3) + nums[i]);
        }
    }
    
    class Solution {
    public:
        int maxSumDivThree(vector<int> &nums) {
            int n = nums.size(), memo[n][3];
            memset(memo, -1, sizeof(memo)); // -1 表示没有计算过
            auto dfs = [&](auto&& dfs, int i, int j) -> int {
                if (i < 0) return j ? INT_MIN : 0;
                int &res = memo[i][j]; // 注意这里是引用,下面会直接修改 memo[i][j]
                if (res != -1) return res; // 之前计算过
                return res = max(dfs(dfs, i - 1, j), dfs(dfs, i - 1, (j + nums[i]) % 3) + nums[i]);
            };
            return dfs(dfs, n - 1, 0);
        }
    };
    
    func maxSumDivThree(nums []int) int {
        n := len(nums)
        memo := make([][3]int, n)
        for i := range memo {
            for j := range memo[i] {
                memo[i][j] = -1 // -1 表示没有计算过
            }
        }
        var dfs func(int, int) int
        dfs = func(i, j int) int {
            if i < 0 {
                if j == 0 {
                    return 0
                }
                return math.MinInt
            }
            p := &memo[i][j]
            if *p != -1 { // 之前计算过
                return *p
            }
            *p = max(dfs(i-1, j), dfs(i-1, (j+nums[i])%3)+nums[i])
            return *p
        }
        return dfs(n-1, 0)
    }
    
    func max(a, b int) int { if a < b { return b }; return a }
    

    复杂度分析

    • 时间复杂度:$\mathcal{O}(nk)$,其中 $n$ 为 $\textit{nums}$ 的长度,$k=3$。动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题中状态个数等于 $\mathcal{O}(nk)$,单个状态的计算时间为 $\mathcal{O}(1)$,因此时间复杂度为 $\mathcal{O}(nk)$。
    • 空间复杂度:$\mathcal{O}(nk)$。

    三、1:1 翻译成递推

    我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。

    做法:

    • $\textit{dfs}$ 改成 $f$ 数组;
    • 递归改成循环(每个参数都对应一层循环);
    • 递归边界改成 $f$ 数组的初始值。

    具体来说,$f[i][j]$ 的含义与状态转移方程和 $\textit{dfs}(i,j)$ 的是一致的,即

    $$
    f[i][j] = \max(f[i-1][j],f[i-1][(j+x)\bmod 3] + x)
    $$

    但当 $i=0$ 时,等号右边会出现负数下标。或者说,这种定义方式没有状态能表示递归边界

    解决办法:在 $f$ 数组的上边加一排,把原来的 $f[i]$ 改成 $f[i+1]$,$f[i-1]$ 改成 $f[i]$。此时 $f[0][j]$ 就对应着 $\textit{dfs}(-1,j)$。

    修改后的递推式为

    $$
    f[i+1][j] = \max(f[i][j],f[i][(j+x)\bmod 3] + x)
    $$

    初始值 $f[0]=[0,-\infty,-\infty]$,翻译自 $\textit{dfs}(-1,0)=0,\textit{dfs}(-1,1)=-\infty,\textit{dfs}(-1,2)=-\infty$。

    答案为 $f[n][0]$,翻译自 $\textit{dfs}(n-1,0)$。

    class Solution:
        def maxSumDivThree(self, nums: List[int]) -> int:
            f = [[-inf] * 3 for _ in range(len(nums) + 1)]
            f[0][0] = 0
            for i, x in enumerate(nums):
                for j in range(3):
                    f[i + 1][j] = max(f[i][j], f[i][(j + x) % 3] + x)
            return f[-1][0]
    
    class Solution {
        public int maxSumDivThree(int[] nums) {
            int n = nums.length;
            var f = new int[n + 1][3];
            f[0][1] = f[0][2] = Integer.MIN_VALUE;
            for (int i = 0; i < n; i++)
                for (int j = 0; j < 3; j++)
                    f[i + 1][j] = Math.max(f[i][j], f[i][(j + nums[i]) % 3] + nums[i]);
            return f[n][0];
        }
    }
    
    class Solution {
    public:
        int maxSumDivThree(vector<int> &nums) {
            int n = nums.size(), f[n + 1][3];
            f[0][0] = 0, f[0][1] = INT_MIN, f[0][2] = INT_MIN;
            for (int i = 0; i < n; i++)
                for (int j = 0; j < 3; j++)
                    f[i + 1][j] = max(f[i][j], f[i][(j + nums[i]) % 3] + nums[i]);
            return f[n][0];
        }
    };
    
    func maxSumDivThree(nums []int) int {
        n := len(nums)
        f := make([][3]int, n+1)
        f[0][1] = math.MinInt
        f[0][2] = math.MinInt
        for i, x := range nums {
            for j := 0; j < 3; j++ {
                f[i+1][j] = max(f[i][j], f[i][(j+x)%3]+x)
            }
        }
        return f[n][0]
    }
    
    func max(a, b int) int { if a < b { return b }; return a }
    

    复杂度分析

    • 时间复杂度:$\mathcal{O}(nk)$,其中 $n$ 为 $\textit{nums}$ 的长度,$k=3$。动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题中状态个数等于 $\mathcal{O}(nk)$,单个状态的计算时间为 $\mathcal{O}(1)$,因此时间复杂度为 $\mathcal{O}(nk)$。
    • 空间复杂度:$\mathcal{O}(nk)$。

    四、用滚动数组优化空间

    由于 $f[i+1]$ 只依赖 $f[i]$,那么 $f[i-1]$ 及其之前的数据就没用了。

    例如计算 $f[2]$ 的时候,数组 $f[0]$ 不再使用了。

    那么干脆把 $f[2]$ 填到 $f[0]$ 中。同理,把 $f[3]$ 填到 $f[1]$ 中,$f[4]$ 填到 $f[0]$ 中,……

    因此可以只用两个长为 $n+1$ 的数组滚动计算。

    具体可以看【基础算法精讲 18】中的画图讲解。

    class Solution:
        def maxSumDivThree(self, nums: List[int]) -> int:
            f = [[-inf] * 3 for _ in range(2)]
            f[0][0] = 0
            for i, x in enumerate(nums):
                for j in range(3):
                    f[(i + 1) % 2][j] = max(f[i % 2][j], f[i % 2][(j + x) % 3] + x)
            return f[len(nums) % 2][0]
    
    class Solution {
        public int maxSumDivThree(int[] nums) {
            int n = nums.length;
            var f = new int[2][3];
            f[0][1] = f[0][2] = Integer.MIN_VALUE;
            for (int i = 0; i < n; i++)
                for (int j = 0; j < 3; j++)
                    f[(i + 1) % 2][j] = Math.max(f[i % 2][j], f[i % 2][(j + nums[i]) % 3] + nums[i]);
            return f[n % 2][0];
        }
    }
    
    class Solution {
    public:
        int maxSumDivThree(vector<int> &nums) {
            int n = nums.size(), f[2][3];
            f[0][0] = 0, f[0][1] = INT_MIN, f[0][2] = INT_MIN;
            for (int i = 0; i < n; i++)
                for (int j = 0; j < 3; j++)
                    f[(i + 1) % 2][j] = max(f[i % 2][j], f[i % 2][(j + nums[i]) % 3] + nums[i]);
            return f[n % 2][0];
        }
    };
    
    func maxSumDivThree(nums []int) int {
        f := [2][3]int{{0, math.MinInt, math.MinInt}}
        for i, x := range nums {
            for j := 0; j < 3; j++ {
                f[(i+1)%2][j] = max(f[i%2][j], f[i%2][(j+x)%3]+x)
            }
        }
        return f[len(nums)%2][0]
    }
    
    func max(a, b int) int { if a < b { return b }; return a }
    

    复杂度分析

    • 时间复杂度:$\mathcal{O}(nk)$,其中 $n$ 为 $\textit{nums}$ 的长度,$k=3$。
    • 空间复杂度:$\mathcal{O}(k)$。

    总结

    相比贪心算法,动态规划的适用性更广。请你思考,如果数组中有负数,动态规划是否也能得到正确的结果?如果把 $3$ 换成其它数字呢?欢迎在评论区发表你的看法。

    分类题单

    如何科学刷题?

    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站@灵茶山艾府

    20行代码轻松双百,一看就懂

    remainder分别为余数0,1,2时的最大值

    class Solution {
    public:
        int maxSumDivThree(vector<int>& nums) {
             int N = nums.size();
            int remainder[3] = {0};
            for(int i = 0; i < N; i++){
                int a,b,c;
                a = remainder[0] + nums[i];
                b = remainder[1] + nums[i];
                c = remainder[2] + nums[i];
                remainder[a%3] = max(remainder[a%3], a);
                remainder[b%3] = max(remainder[b%3], b);
                remainder[c%3] = max(remainder[c%3], c);
            }
            return remainder[0];
        }
    };
    

    动态规划与状态转移

    解题思路

    根据题意很容易想到用状态转移与动态规划的思路来解决

    定义

    • dp[i][0]表示nums[0...i]模三余零的最大和
    • dp[i][1]表示nums[0...i]模三余一的最大和
    • dp[i][2]表示nums[0...i]模三余二的最大和
    • 零状态:当前数字最大和模三余零
    • 一状态:当前数字最大和模三余一
    • 二状态:当前数字最大和模三余二

    动态规划的思路

    对于任意一种状态,下一步我们都有两种选择,一是选择当前元素二是不选择当前元素

    dp[i][*] = max{dp[i-1][*],dp[i-1][*] + nums[i]}  (* 取值为 0,1,2)
    

    以上是常见的动态规划的递推结构

    状态转移

    本题的状态转移显而易见,以当前状态是零状态为例。我们可以想到,前一个状态无非是零状态``一状态``二状态,三种情况,针对这三种情况我们分类讨论即可

    批注 2020-02-02 133759.png

    动态规划与状态转移结合

    显然可以直接两种方法直接结合起来
    image.png

    所以零状态如何转移我们理解了之后,可以一次写出一状态的转移,二状态的转移
    image.png
    image.png

    我的题解

    LeetCode1262 可被三整除的最大和
    LeetCode688 “马”在棋盘上的概率
    LeetCode967 连续差相同的数字
    LeetCode873 最长的斐波那契子序列的长度
    LeetCode1218 最长定差子序列
    LeetCode523 连续子数组和
    LeetCode576 出界的路径数
    LeetCode1220 统计元音字母序列的数目

    代码

    ###cpp

    class Solution {
    public:
        int maxSumDivThree(vector<int>& nums) {
           int n = nums.size();
    vector<vector<int>> dp(n + 1, vector<int>(3, 0));
    dp[0][0] = 0; dp[0][1] = INT_MIN; dp[0][2] = INT_MIN;
    
    
    for (int i = 1; i <= n; i++) {
    if (nums[i - 1] % 3 == 0) {
    dp[i][0] = max(dp[i - 1][0], dp[i - 1][0] + nums[i - 1]);
    dp[i][1] = max(dp[i - 1][1], dp[i - 1][1] + nums[i - 1]);
    dp[i][2] = max(dp[i - 1][2], dp[i - 1][2] + nums[i - 1]);
    }
    else if (nums[i - 1] % 3 == 1) {
    dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] + nums[i - 1]);
    dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + nums[i - 1]);
    dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + nums[i - 1]);
    }
    else if (nums[i - 1] % 3 == 2) {
    dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + nums[i - 1]);
    dp[i][1] = max(dp[i - 1][1], dp[i - 1][2] + nums[i - 1]);
    dp[i][2] = max(dp[i - 1][2], dp[i - 1][0] + nums[i - 1]);
    }
    }
    return dp[n][0];
        }
    };
    

    每日一题-使所有元素都可以被 3 整除的最少操作数🟢

    给你一个整数数组 nums 。一次操作中,你可以将 nums 中的 任意 一个元素增加或者减少 1 。

    请你返回将 nums 中所有元素都可以被 3 整除的 最少 操作次数。

     

    示例 1:

    输入:nums = [1,2,3,4]

    输出:3

    解释:

    通过以下 3 个操作,数组中的所有元素都可以被 3 整除:

    • 将 1 减少 1 。
    • 将 2 增加 1 。
    • 将 4 减少 1 。

    示例 2:

    输入:nums = [3,6,9]

    输出:0

     

    提示:

    • 1 <= nums.length <= 50
    • 1 <= nums[i] <= 50

    3190. 使所有元素都可以被 3 整除的最少操作数

    解法一

    思路和算法

    对于数组 $\textit{nums}$ 中的每个元素,可以根据元素除以 $3$ 的余数计算使元素可以被 $3$ 整除的最少操作次数。

    • 如果一个元素除以 $3$ 的余数是 $0$,则不需要执行操作。

    • 如果一个元素除以 $3$ 的余数是 $1$,则需要将元素减少 $1$ 才能使元素可以被 $3$ 整除,最少操作次数是 $1$。

    • 如果一个元素除以 $3$ 的余数是 $2$,则需要将元素增加 $1$ 才能使元素可以被 $3$ 整除,最少操作次数是 $1$。

    上述情况可以概括成两种情况。

    • 能被 $3$ 整除的元素的最少操作次数是 $0$。

    • 不能被 $3$ 整除的元素的最少操作次数是 $1$。

    因此,使数组 $\textit{nums}$ 中所有元素都可以被 $3$ 整除的最少操作次数等于数组 $\textit{nums}$ 中的不能被 $3$ 整除的元素个数。

    代码

    ###Java

    class Solution {
        public int minimumOperations(int[] nums) {
            int operations = 0;
            for (int num : nums) {
                if (num % 3 != 0) {
                    operations++;
                }
            }
            return operations;
        }
    }
    

    ###C#

    public class Solution {
        public int MinimumOperations(int[] nums) {
            int operations = 0;
            foreach (int num in nums) {
                if (num % 3 != 0) {
                    operations++;
                }
            }
            return operations;
        }
    }
    

    复杂度分析

    • 时间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。需要遍历数组一次。

    • 空间复杂度:$O(1)$。

    解法二

    思路和算法

    考虑更一般的情形,对于正整数 $d$,计算使数组 $\textit{nums}$ 中所有元素都可以被 $d$ 整除的最少操作次数。

    对于正整数元素 $\textit{num}$,当 $\textit{num} \bmod d = 0$ 时最少操作次数是 $0$,当 $\textit{num} \bmod d \ne 0$ 时可以将 $\textit{num}$ 减少或增加使更新后的 $\textit{num}$ 可以被 $d$ 整除。

    • 将 $\textit{num}$ 减少到可以被 $d$ 整除,最少操作次数是 $\textit{num} \bmod d$。

    • 将 $\textit{num}$ 增加到可以被 $d$ 整除,最少操作次数是 $d - \textit{num} \bmod d$。

    为了使操作次数最少,应取两种情况中的最小值,因此使 $\textit{num}$ 可以被 $d$ 整除的最少操作次数是 $\min(\textit{num} \bmod d, d - \textit{num} \bmod d)$。

    当 $\textit{num} \bmod d = 0$ 时,$\min(\textit{num} \bmod d, d - \textit{num} \bmod d) = 0$,最少操作次数也是 $\min(\textit{num} \bmod d, d - \textit{num} \bmod d)$。因此对于任意元素 $\textit{num}$,使 $\textit{num}$ 可以被 $d$ 整除的最少操作次数是 $\min(\textit{num} \bmod d, d - \textit{num} \bmod d)$。

    遍历数组 $\textit{nums}$ 分别计算使每个元素可以被 $d$ 整除的最少操作次数之后,即可得到使数组 $\textit{nums}$ 中所有元素都可以被 $d$ 整除的最少操作次数。

    代码

    ###Java

    class Solution {
        static final int DIVISOR = 3;
    
        public int minimumOperations(int[] nums) {
            int operations = 0;
            for (int num : nums) {
                operations += Math.min(num % DIVISOR, DIVISOR - num % DIVISOR);
            }
            return operations;
        }
    }
    

    ###C#

    public class Solution {
        const int DIVISOR = 3;
    
        public int MinimumOperations(int[] nums) {
            int operations = 0;
            foreach (int num in nums) {
                operations += Math.Min(num % DIVISOR, DIVISOR - num % DIVISOR);
            }
            return operations;
        }
    }
    

    复杂度分析

    • 时间复杂度:$O(n)$,其中 $n$ 是数组 $\textit{nums}$ 的长度。需要遍历数组一次。

    • 空间复杂度:$O(1)$。

    不是 3 的倍数的元素个数(Python/Java/C++/C/Go/JS/Rust)

    遍历 $\textit{nums}$,按照元素模 $3$ 的余数分类:

    • 如果 $\textit{nums}[i] = 3k$,无需操作。
    • 如果 $\textit{nums}[i] = 3k+1$,减一得到 $3$ 的倍数。
    • 如果 $\textit{nums}[i] = 3k+2$,加一得到 $3$ 的倍数。

    由此可见,对于不是 $3$ 的倍数的元素,只需操作一次就可以变成 $3$ 的倍数。

    所以答案为不是 $3$ 的倍数的元素个数。

    ###py

    class Solution:
        def minimumOperations(self, nums: List[int]) -> int:
            return sum(x % 3 != 0 for x in nums)
    

    ###java

    class Solution {
        public int minimumOperations(int[] nums) {
            int ans = 0;
            for (int x : nums) {
                ans += x % 3 != 0 ? 1 : 0;
            }
            return ans;
        }
    }
    

    ###cpp

    class Solution {
    public:
        int minimumOperations(vector<int>& nums) {
            int ans = 0;
            for (int x : nums) {
                ans += x % 3 != 0;
            }
            return ans;
        }
    };
    

    ###c

    int minimumOperations(int* nums, int numsSize) {
        int ans = 0;
        for (int i = 0; i < numsSize; i++) {
            ans += nums[i] % 3 != 0;
        }
        return ans;
    }
    

    ###go

    func minimumOperations(nums []int) (ans int) {
    for _, x := range nums {
    if x%3 != 0 {
    ans++
    }
    }
    return
    }
    

    ###js

    var minimumOperations = function(nums) {
        return _.sumBy(nums, x => (x % 3 !== 0 ? 1 : 0));
    };
    

    ###rust

    impl Solution {
        pub fn minimum_operations(nums: Vec<i32>) -> i32 {
            nums.into_iter().filter(|&x| x % 3 != 0).count() as _
        }
    }
    

    复杂度分析

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

    思考题

    把题目中的 $3$ 改成 $4$ 呢?改成 $m$ 呢?

    请看 视频讲解,欢迎点赞关注!

    分类题单

    如何科学刷题?

    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站@灵茶山艾府

    每日一题-长度为 3 的不同回文子序列🟡

    给你一个字符串 s ,返回 s长度为 3 不同回文子序列 的个数。

    即便存在多种方法来构建相同的子序列,但相同的子序列只计数一次。

    回文 是正着读和反着读一样的字符串。

    子序列 是由原字符串删除其中部分字符(也可以不删除)且不改变剩余字符之间相对顺序形成的一个新字符串。

    • 例如,"ace""abcde" 的一个子序列。

     

    示例 1:

    输入:s = "aabca"
    输出:3
    解释:长度为 3 的 3 个回文子序列分别是:
    - "aba" ("aabca" 的子序列)
    - "aaa" ("aabca" 的子序列)
    - "aca" ("aabca" 的子序列)
    

    示例 2:

    输入:s = "adc"
    输出:0
    解释:"adc" 不存在长度为 3 的回文子序列。
    

    示例 3:

    输入:s = "bbcbaba"
    输出:4
    解释:长度为 3 的 4 个回文子序列分别是:
    - "bbb" ("bbcbaba" 的子序列)
    - "bcb" ("bbcbaba" 的子序列)
    - "bab" ("bbcbaba" 的子序列)
    - "aba" ("bbcbaba" 的子序列)
    

     

    提示:

    • 3 <= s.length <= 105
    • s 仅由小写英文字母组成

    长度为 3 的不同回文子序列

    方法一:枚举两侧的字符

    思路与算法

    我们可以枚举回文序列两侧的字符种类。对于每种字符,如果它在字符串 $s$ 中出现,我们记录它第一次出现的下标 $l$ 与最后一次出现的下标 $r$。那么,以该字符为两侧的回文子序列,它中间的字符只可能在 $s[l+1..r-1]$ 中选取;且以该字符为两侧的回文子序列的种数即为 $s[l+1..r-1]$ 中的字符种数。

    我们遍历 $s[l+1..r-1]$ 子串计算该子串中的字符种数。在遍历时,我们可以使用哈希集合来维护该子串中的字符种类;当遍历完成后,哈希集合内元素的数目即为该子串中的字符总数。

    在枚举两侧字符种类时,我们维护这些回文子序列种数之和,并最终作为答案返回。

    代码

    ###C++

    class Solution {
    public:
        int countPalindromicSubsequence(string s) {
            int n = s.size();
            int res = 0;
            // 枚举两侧字符
            for (char ch = 'a'; ch <= 'z'; ++ch){
                int l = 0, r = n - 1;
                // 寻找该字符第一次出现的下标
                while (l < n && s[l] != ch){
                    ++l;
                }
                // 寻找该字符最后一次出现的下标
                while (r >= 0 && s[r] != ch){
                    --r;
                }
                if (r - l < 2){
                    // 该字符未出现,或两下标中间的子串不存在
                    continue;
                }
                // 利用哈希集合统计 s[l+1..r-1] 子串的字符总数,并更新答案
                unordered_set<char> charset;
                for (int k = l + 1; k < r; ++k){
                    charset.insert(s[k]);
                }
                res += charset.size();
            }
            return res;
        }
    };
    

    ###Python

    class Solution:
        def countPalindromicSubsequence(self, s: str) -> int:
            n = len(s)
            res = 0
            # 枚举两侧字符
            for i in range(26):
                l, r = 0, n - 1
                # 寻找该字符第一次出现的下标
                while l < n and ord(s[l]) - ord('a') != i:
                    l += 1
                # 寻找该字符最后一次出现的下标
                while r >= 0 and ord(s[r]) - ord('a') != i:
                    r -= 1
                if r - l < 2:
                    # 该字符未出现,或两下标中间的子串不存在
                    continue
                # 利用哈希集合统计 s[l+1..r-1] 子串的字符总数,并更新答案
                charset = set()
                for k in range(l + 1, r):
                    charset.add(s[k])
                res += len(charset)
            return res
    

    ###Java

    class Solution {
        public int countPalindromicSubsequence(String s) {
            int n = s.length();
            int res = 0;
            // 枚举两侧字符
            for (char ch = 'a'; ch <= 'z'; ++ch) {
                int l = 0, r = n - 1;
                // 寻找该字符第一次出现的下标
                while (l < n && s.charAt(l) != ch) {
                    ++l;
                }
                // 寻找该字符最后一次出现的下标
                while (r >= 0 && s.charAt(r) != ch) {
                    --r;
                }
                if (r - l < 2) {
                    // 该字符未出现,或两下标中间的子串不存在
                    continue;
                }
                // 利用哈希集合统计 s[l+1..r-1] 子串的字符总数,并更新答案
                Set<Character> charset = new HashSet<>();
                for (int k = l + 1; k < r; ++k) {
                    charset.add(s.charAt(k));
                }
                res += charset.size();
            }
            return res;
        }
    }
    

    ###C#

    public class Solution {
        public int CountPalindromicSubsequence(string s) {
            int n = s.Length;
            int res = 0;
            // 枚举两侧字符
            for (char ch = 'a'; ch <= 'z'; ++ch) {
                int l = 0, r = n - 1;
                // 寻找该字符第一次出现的下标
                while (l < n && s[l] != ch) {
                    ++l;
                }
                // 寻找该字符最后一次出现的下标
                while (r >= 0 && s[r] != ch) {
                    --r;
                }
                if (r - l < 2) {
                    // 该字符未出现,或两下标中间的子串不存在
                    continue;
                }
                // 利用哈希集合统计 s[l+1..r-1] 子串的字符总数,并更新答案
                HashSet<char> charset = new HashSet<char>();
                for (int k = l + 1; k < r; ++k) {
                    charset.Add(s[k]);
                }
                res += charset.Count;
            }
            return res;
        }
    }
    

    ###Go

    func countPalindromicSubsequence(s string) int {
        n := len(s)
        res := 0
        // 枚举两侧字符
        for ch := 'a'; ch <= 'z'; ch++ {
            l, r := 0, n-1
            // 寻找该字符第一次出现的下标
            for l < n && rune(s[l]) != ch {
                l++
            }
            // 寻找该字符最后一次出现的下标
            for r >= 0 && rune(s[r]) != ch {
                r--
            }
            if r-l < 2 {
                // 该字符未出现,或两下标中间的子串不存在
                continue
            }
            // 利用哈希集合统计 s[l+1..r-1] 子串的字符总数,并更新答案
            charset := make(map[rune]bool)
            for _, c := range s[l+1:r] {
                charset[c] = true
            }
            res += len(charset)
        }
        return res
    }
    

    ###C

    int countPalindromicSubsequence(char* s) {
        int n = strlen(s);
        int res = 0;
        // 枚举两侧字符
        for (char ch = 'a'; ch <= 'z'; ++ch) {
            int l = 0, r = n - 1;
            // 寻找该字符第一次出现的下标
            while (l < n && s[l] != ch) {
                ++l;
            }
            // 寻找该字符最后一次出现的下标
            while (r >= 0 && s[r] != ch) {
                --r;
            }
            if (r - l < 2) {
                // 该字符未出现,或两下标中间的子串不存在
                continue;
            }
            // 利用哈希集合统计 s[l+1..r-1] 子串的字符总数,并更新答案
            bool charset[26] = {false};
            for (int k = l + 1; k < r; ++k) {
                charset[s[k] - 'a'] = true;
            }
            int count = 0;
            for (int i = 0; i < 26; ++i) {
                if (charset[i]) {
                    count++;
                }
            }
            res += count;
        }
        return res;
    }
    

    ###JavaScript

    var countPalindromicSubsequence = function(s) {
        const n = s.length;
        let res = 0;
        // 枚举两侧字符
        for (let ch = 'a'.charCodeAt(0); ch <= 'z'.charCodeAt(0); ch++) {
            const c = String.fromCharCode(ch);
            let l = 0, r = n - 1;
            // 寻找该字符第一次出现的下标
            while (l < n && s[l] !== c) {
                ++l;
            }
            // 寻找该字符最后一次出现的下标
            while (r >= 0 && s[r] !== c) {
                --r;
            }
            if (r - l < 2) {
                // 该字符未出现,或两下标中间的子串不存在
                continue;
            }
            // 利用哈希集合统计 s[l+1..r-1] 子串的字符总数,并更新答案
            const charset = new Set();
            for (let k = l + 1; k < r; k++) {
                charset.add(s[k]);
            }
            res += charset.size;
        }
        return res;
    };
    

    ###TypeScript

    function countPalindromicSubsequence(s: string): number {
        const n = s.length;
        let res = 0;
        // 枚举两侧字符
        for (let ch = 'a'.charCodeAt(0); ch <= 'z'.charCodeAt(0); ch++) {
            const c = String.fromCharCode(ch);
            let l = 0, r = n - 1;
            // 寻找该字符第一次出现的下标
            while (l < n && s[l] !== c) {
                ++l;
            }
            // 寻找该字符最后一次出现的下标
            while (r >= 0 && s[r] !== c) {
                --r;
            }
            if (r - l < 2) {
                // 该字符未出现,或两下标中间的子串不存在
                continue;
            }
            // 利用哈希集合统计 s[l+1..r-1] 子串的字符总数,并更新答案
            const charset = new Set<string>();
            for (let k = l + 1; k < r; k++) {
                charset.add(s[k]);
            }
            res += charset.size;
        }
        return res;
    }
    

    ###Rust

    use std::collections::HashSet;
    
    impl Solution {
        pub fn count_palindromic_subsequence(s: String) -> i32 {
            let mut res = 0;
            // 枚举两侧字符
            for ch in 'a'..='z' {
                // 使用迭代器找到第一个和最后一个出现位置
                let mut chars = s.chars();
                let l = chars.position(|c| c == ch);
                let r = chars.rev().position(|c| c == ch).map(|pos| s.len() - 1 - pos);
                if let (Some(l), Some(r)) = (l, r) {
                    if r > l + 1 {
                        // 收集中间字符
                        let unique_chars: HashSet<_> = s[l+1..r].chars().collect();
                        res += unique_chars.len() as i32;
                    }
                }
            }
            res
        }
    }
    

    复杂度分析

    • 时间复杂度:$O(n|\Sigma| + |\Sigma|^2)$,其中 $n$ 为 $s$ 的长度,$|\Sigma|$ 为字符集的大小。我们总共需要枚举 $|\Sigma|$ 种字符,每次枚举至多需要遍历一次字符串 $s$ 与哈希集合,时间复杂度分别为 $O(n)$ 与 $O(|\Sigma|)$。

    • 空间复杂度:$O(|\Sigma|)$,即为哈希集合的空间开销。

    方法二:枚举中间的字符

    思路与算法

    我们也可以遍历字符串 $s$ 枚举回文子序列中间的字符。假设 $s$ 的长度为 $n$,当我们遍历到 $s[i]$ 时,以 $s[i]$ 为中间字符的回文子序列种数即为前缀 $s[0..i-1]$ 与后缀 $s[i+1..n-1]$ 的公共字符种数。

    对于一个任意的子串,由于其仅由小写英文字母组成,我们可以用一个 $32$ 位整数来表示该子串含有哪些字符。如果该整数从低到高第 $i$ 个二进制位为 $1$,那么代表该子串含有字典序为 $i$ 的小写英文字母。在遍历该子串时,我们需要用按位或来维护该整数。

    为了简化计算,我们可以参照前文所述的对应关系,用两个 $32$ 位整数的数组 $\textit{pre}, \textit{suf}$ 分别维护 $s$ 中前缀与后缀包含的字符。其中,$\textit{pre}[i]$ 代表前缀 $s[0..i-1]$ 包含的字符种类,$\textit{suf}[i]$ 代表后缀 $s[i+1..n-1]$ 包含的字符种类。那么,以 $s[i]$ 为中间字符的回文子序列中,两侧字符的种类对应的状态即为 $\textit{pre}[i] & \textit{suf}[i]$,其中 $&$ 为按位与运算符。

    为了避免重复计算,我们需要在遍历的同时使用按位或来维护每种字符为中间字符的回文子序列种数。最终,我们将不同种类字符对应的回文子序列总数求和作为答案返回。

    代码

    ###C++

    class Solution {
    public:
        int countPalindromicSubsequence(string s) {
            int n = s.size();
            int res = 0;
            // 前缀/后缀字符状态数组
            vector<int> pre(n), suf(n);
            for (int i = 0; i < n; ++i) {
                // 前缀 s[0..i-1] 包含的字符种类
                pre[i] = (i ? pre[i - 1] : 0) | (1 << (s[i] - 'a'));
            }
            for (int i = n - 1; i >= 0; --i) {
                // 后缀 s[i+1..n-1] 包含的字符种类
                suf[i] = (i != n - 1 ? suf[i + 1] : 0) | (1 << (s[i] - 'a'));
            }
            // 每种中间字符的回文子序列状态数组
            vector<int> ans(26);
            for (int i = 1; i < n - 1; ++i) {
                ans[s[i]-'a'] |= (pre[i - 1] & suf[i + 1]);
            }
            // 更新答案
            for (int i = 0; i < 26; ++i) {
                res += __builtin_popcount(ans[i]);
            }
            return res;
        }
    };
    

    ###Python

    class Solution:
        def countPalindromicSubsequence(self, s: str) -> int:
            n = len(s)
            res = 0
            # 前缀/后缀字符状态数组
            pre = [0] * n
            suf = [0] * n
            for i in range(n):
                # 前缀 s[0..i-1] 包含的字符种类
                pre[i] = (pre[i - 1] if i else 0) | (1 << (ord(s[i]) - ord('a')))
            for i in range(n - 1, -1, -1):
                # 后缀 s[i+1..n-1] 包含的字符种类
                suf[i] = (suf[i + 1] if i != n - 1 else 0) | (1 << (ord(s[i]) - ord('a')))
            # 每种中间字符的回文子序列状态数组
            ans = [0] * 26
            for i in range(1, n - 1):
                ans[ord(s[i]) - ord('a')] |= pre[i - 1] & suf[i + 1]
            # 更新答案
            for i in range(26):
                res += bin(ans[i]).count("1")
            return res
    

    ###Java

    class Solution {
        public int countPalindromicSubsequence(String s) {
            int n = s.length();
            int res = 0;
            // 前缀/后缀字符状态数组
            int[] pre = new int[n];
            int[] suf = new int[n];
            for (int i = 0; i < n; ++i) {
                // 前缀 s[0..i-1] 包含的字符种类
                pre[i] = (i > 0 ? pre[i - 1] : 0) | (1 << (s.charAt(i) - 'a'));
            }
            for (int i = n - 1; i >= 0; --i) {
                // 后缀 s[i+1..n-1] 包含的字符种类
                suf[i] = (i != n - 1 ? suf[i + 1] : 0) | (1 << (s.charAt(i) - 'a'));
            }
            // 每种中间字符的回文子序列状态数组
            int[] ans = new int[26];
            for (int i = 1; i < n - 1; ++i) {
                ans[s.charAt(i) - 'a'] |= (pre[i - 1] & suf[i + 1]);
            }
            // 更新答案
            for (int i = 0; i < 26; ++i) {
                res += Integer.bitCount(ans[i]);
            }
            return res;
        }
    }
    

    ###C#

    public class Solution {
        public int CountPalindromicSubsequence(string s) {
            int n = s.Length;
            int res = 0;
            // 前缀/后缀字符状态数组
            int[] pre = new int[n];
            int[] suf = new int[n];
            for (int i = 0; i < n; ++i) {
                // 前缀 s[0..i-1] 包含的字符种类
                pre[i] = (i > 0 ? pre[i - 1] : 0) | (1 << (s[i] - 'a'));
            }
            for (int i = n - 1; i >= 0; --i) {
                // 后缀 s[i+1..n-1] 包含的字符种类
                suf[i] = (i != n - 1 ? suf[i + 1] : 0) | (1 << (s[i] - 'a'));
            }
            // 每种中间字符的回文子序列状态数组
            int[] ans = new int[26];
            for (int i = 1; i < n - 1; ++i) {
                ans[s[i] - 'a'] |= (pre[i - 1] & suf[i + 1]);
            }
            // 更新答案
            for (int i = 0; i < 26; ++i) {
                res += BitOperations.PopCount((uint)ans[i]);
            }
            return res;
        }
    }
    

    ###Go

    func countPalindromicSubsequence(s string) int {
        n := len(s)
        res := 0
        // 前缀/后缀字符状态数组
        pre := make([]int, n)
        suf := make([]int, n)
        for i := 0; i < n; i++ {
            // 前缀 s[0..i-1] 包含的字符种类
            if i > 0 {
                pre[i] = pre[i-1]
            }
            pre[i] |= 1 << (s[i] - 'a')
        }
        for i := n - 1; i >= 0; i-- {
            // 后缀 s[i+1..n-1] 包含的字符种类
            if i != n - 1 {
                suf[i] = suf[i + 1]
            }
            suf[i] |= 1 << (s[i] - 'a')
        }
        // 每种中间字符的回文子序列状态数组
        ans := make([]int, 26)
        for i := 1; i < n - 1; i++ {
            ans[s[i] - 'a'] |= (pre[i - 1] & suf[i + 1])
        }
        // 更新答案
        for i := 0; i < 26; i++ {
            res += bits.OnesCount(uint(ans[i]))
        }
        return res
    }
    

    ###C

    int countPalindromicSubsequence(char* s) {
        int n = strlen(s);
        int res = 0;
        // 前缀/后缀字符状态数组
        int pre[n], suf[n];
        for (int i = 0; i < n; ++i) {
            // 前缀 s[0..i-1] 包含的字符种类
            pre[i] = (i ? pre[i - 1] : 0) | (1 << (s[i] - 'a'));
        }
        for (int i = n - 1; i >= 0; --i) {
            // 后缀 s[i+1..n-1] 包含的字符种类
            suf[i] = (i != n - 1 ? suf[i + 1] : 0) | (1 << (s[i] - 'a'));
        }
        // 每种中间字符的回文子序列状态数组
        int ans[26] = {0};
        for (int i = 1; i < n - 1; ++i) {
            ans[s[i] - 'a'] |= (pre[i - 1] & suf[i + 1]);
        }
        // 更新答案
        for (int i = 0; i < 26; ++i) {
            res += __builtin_popcount(ans[i]);
        }
        return res;
    }
    

    ###JavaScript

    var countPalindromicSubsequence = function(s) {
        const n = s.length;
        let res = 0;
        // 前缀/后缀字符状态数组
        const pre = new Array(n).fill(0);
        const suf = new Array(n).fill(0);
        for (let i = 0; i < n; ++i) {
            // 前缀 s[0..i-1] 包含的字符种类
            pre[i] = (i > 0 ? pre[i - 1] : 0) | (1 << (s.charCodeAt(i) - 97));
        }
        for (let i = n - 1; i >= 0; --i) {
            // 后缀 s[i+1..n-1] 包含的字符种类
            suf[i] = (i !== n - 1 ? suf[i + 1] : 0) | (1 << (s.charCodeAt(i) - 97));
        }
        // 每种中间字符的回文子序列状态数组
        const ans = new Array(26).fill(0);
        for (let i = 1; i < n - 1; ++i) {
            ans[s.charCodeAt(i) - 97] |= (pre[i-1] & suf[i + 1]);
        }
        // 更新答案
        for (let i = 0; i < 26; ++i) {
            res += ans[i].toString(2).split('1').length - 1;
        }
        return res;
    };
    

    ###TypeScript

    function countPalindromicSubsequence(s: string): number {
        const n = s.length;
        let res = 0;
        // 前缀/后缀字符状态数组
        const pre: number[] = new Array(n).fill(0);
        const suf: number[] = new Array(n).fill(0);
        for (let i = 0; i < n; ++i) {
            // 前缀 s[0..i-1] 包含的字符种类
            pre[i] = (i > 0 ? pre[i - 1] : 0) | (1 << (s.charCodeAt(i) - 97));
        }
        for (let i = n - 1; i >= 0; --i) {
            // 后缀 s[i+1..n-1] 包含的字符种类
            suf[i] = (i !== n - 1 ? suf[i + 1] : 0) | (1 << (s.charCodeAt(i) - 97));
        }
        // 每种中间字符的回文子序列状态数组
        const ans: number[] = new Array(26).fill(0);
        for (let i = 1; i < n - 1; ++i) {
            ans[s.charCodeAt(i) - 97] |= (pre[i - 1] & suf[i + 1]);
        }
        // 更新答案
        for (let i = 0; i < 26; ++i) {
            res += ans[i].toString(2).split('1').length - 1;
        }
        return res;
    }
    

    ###Rust

    impl Solution {
        pub fn count_palindromic_subsequence(s: String) -> i32 {
            let n = s.len();
            let mut res = 0;
            // 前缀/后缀字符状态数组
            let mut pre = vec![0u32; n];
            let mut suf = vec![0u32; n];
            
            for (i, c) in s.chars().enumerate() {
                // 前缀 s[0..i-1] 包含的字符种类
                pre[i] = if i > 0 { pre[i-1] } else { 0 } | (1 << (c as u8 - b'a'));
            }
            for (i, c) in s.chars().rev().enumerate() {
                let i = n - 1 - i;
                // 后缀 s[i+1..n-1] 包含的字符种类
                suf[i] = if i != n - 1 { suf[i+1] } else { 0 } | (1 << (c as u8 - b'a'));
            }
            
            // 每种中间字符的回文子序列状态数组
            let mut ans = vec![0u32; 26];
            for (i, c) in s.chars().enumerate() {
                if i > 0 && i < n - 1 {
                    ans[(c as u8 - b'a') as usize] |= pre[i-1] & suf[i+1];
                }
            }
            
            // 更新答案
            for &count in &ans {
                res += count.count_ones() as i32;
            } 
            res
        }
    }
    

    复杂度分析

    • 时间复杂度:$O(n + |\Sigma|)$,其中 $n$ 为 $s$ 的长度,$|\Sigma|$ 为字符集的大小。预处理前后缀状态数组与遍历 $s$ 更新每种字符状态数组的时间复杂度均为 $O(n)$,初始化每种字符状态数组与更新答案的时间复杂度均为 $O(|\Sigma|)$。

    • 空间复杂度:$O(|\Sigma|)$,即为每种字符状态数组的空间开销。

    c++ 寻找回文,关键还是一前一后

    解题思路

    1. 一开始还想着dp,可以添加一个新的字符,受到影响的变化规律非常奇怪,然后转变思路
    2. 添加一个新的字符能添加多少呢?首先了解回文字符串,就两种,aba和aaa
    3. 那么添加新的字符需要回去找原来同样的字符,然后看看中间卡了多少种不同字符
    4. 到了这一步我就悟了,真正的核心是前后两个相同字符以及它们之间夹了多少个不同字符
    5. 一开始想用前缀和,计数字母的个数,想想空间开销,就离谱,算了
    6. 然后回到思路,只需要一前一后,总共也就26个字母,只要遍历每个字母的一前一后,最多时间也是O(n)
    7. 于是就有了以下代码,思路理解了,最多遍历26次即可

    代码

    ###cpp

    class Solution {
    public:
        int countPalindromicSubsequence(string s) {
            //找到一前一后
            map<char, int> first;
            map<char, int> last;
            int size = s.size();
            
            for (int i = 0; i < size; ++i){
                if (first.count(s[i])){
                    last[s[i]] = i;
                }else{
                    first[s[i]] = i;
                }
            }
            
            int res = 0;
            for (char i = 'a'; i < 'a' + 26; i++){
                if (!first.count(i) || !last.count(i)) continue;
                
                int tL = first[i], tR = last[i];
                vector<int> count(26);
                for (int j = tL + 1; j < tR; ++j){
                    count[s[j] - 'a'] = 1;
                }
                
                for (int j = 0; j < 26; ++j){
                    if(count[j] == 1) res++;
                }
            }
            
            return res;
        }
    };
    

    两种方法:枚举两侧 / 枚举中间,附进阶问题(Python/Java/C++/C/Go/JS/Rust)

    前言

    本题要找长为 $3$ 的回文子序列,这要求子序列的第一个字母等于第三个字母。

    换句话说,确定了子序列的前两个字母,就确定了子序列。

    这引出了两类做法:

    • 先枚举两侧的字母,再枚举中间的字母。
    • 先枚举中间的字母,再枚举两侧的字母。

    方法一:枚举两侧

    枚举子序列的第一、三个字母是 $\texttt{a},\texttt{b},\ldots,\texttt{z}$。

    如果第一、三个字母都是 $\texttt{a}$,如何找到尽量多的不同的子序列?

    例如 $s = \texttt{abbacad}$。如果选前两个 $\texttt{a}$ 作为子序列的第一、三个字母,我们只能找到子序列 $\texttt{aba}$。而如果选第一个 $\texttt{a}$ 和最后一个 $\texttt{a}$,夹在两个 $\texttt{a}$ 之间的字母都可以是子序列的第二个字母,从而找到第一、三个字母都是 $\texttt{a}$ 的所有子序列,即 $\texttt{aaa}$、$\texttt{aba}$ 和 $\texttt{aca}$。

    算法

    1. 枚举 $\alpha = \texttt{a},\texttt{b},\ldots,\texttt{z}$。
    2. 找 $\alpha$ 在 $s$ 中首次出现的下标 $i$ 和最后一次出现的下标 $j$。如果没有这样的下标,回到第一步继续枚举。
    3. 下标在 $[i+1,j-1]$ 中的字母,可以作为回文子序列的中间字母。
    4. 题目要求相同的子序列只计数一次。这可以用哈希集合去重,也可以用长为 $26$ 的布尔数组记录遇到过的中间字母,避免重复统计。
    class Solution:
        def countPalindromicSubsequence(self, s: str) -> int:
            ans = 0
            for alpha in ascii_lowercase:  # 枚举两侧字母 alpha
                i = s.find(alpha)  # 最左边的 alpha 的下标
                if i < 0:  # s 中没有 alpha
                    continue
                j = s.rfind(alpha)  # 最右边的 alpha 的下标
                ans += len(set(s[i + 1: j]))  # 统计有多少个不同的中间字母
            return ans
    
    class Solution {
        public int countPalindromicSubsequence(String s) {
            int ans = 0;
            for (char alpha = 'a'; alpha <= 'z'; alpha++) { // 枚举两侧字母 alpha
                int i = s.indexOf(alpha); // 最左边的 alpha 的下标
                if (i < 0) { // s 中没有 alpha
                    continue;
                }
                int j = s.lastIndexOf(alpha); // 最右边的 alpha 的下标
    
                boolean[] has = new boolean[26];
                for (int k = i + 1; k < j; k++) { // 枚举中间字母 mid
                    int mid = s.charAt(k) - 'a';
                    if (!has[mid]) {
                        has[mid] = true; // 避免重复统计
                        ans++;
                    }
                }
            }
            return ans;
        }
    }
    
    class Solution {
    public:
        int countPalindromicSubsequence(string s) {
            int ans = 0;
            for (char alpha = 'a'; alpha <= 'z'; alpha++) { // 枚举两侧字母 alpha
                int i = s.find(alpha); // 最左边的 alpha 的下标
                if (i == string::npos) { // s 中没有 alpha
                    continue;
                }
                int j = s.rfind(alpha); // 最右边的 alpha 的下标
    
                bool has[26]{};
                for (int k = i + 1; k < j; k++) { // 枚举中间字母 s[k]
                    if (!has[s[k] - 'a']) {
                        has[s[k] - 'a'] = true; // 避免重复统计
                        ans++;
                    }
                }
            }
            return ans;
        }
    };
    
    int countPalindromicSubsequence(char* s) {
        int ans = 0;
        for (char alpha = 'a'; alpha <= 'z'; alpha++) { // 枚举两侧字母 alpha
            char* p = strchr(s, alpha); // 找最左边的 alpha
            if (p == NULL) { // s 中没有 alpha
                continue;
            }
            int i = p - s; // 最左边的 alpha 的下标
            int j = strrchr(s, alpha) - s; // 最右边的 alpha 的下标
    
            bool has[26] = {};
            for (int k = i + 1; k < j; k++) { // 枚举中间字母 s[k]
                if (!has[s[k] - 'a']) {
                    has[s[k] - 'a'] = true; // 避免重复统计
                    ans++;
                }
            }
        }
        return ans;
    }
    
    func countPalindromicSubsequence(s string) (ans int) {
    for alpha := byte('a'); alpha <= 'z'; alpha++ { // 枚举两侧字母 alpha
    i := strings.IndexByte(s, alpha) // 最左边的 alpha 的下标
    if i < 0 { // s 中没有 alpha
    continue
    }
    j := strings.LastIndexByte(s, alpha) // 最右边的 alpha 的下标
    if i+1 >= j { // 长度不足 3
    continue
    }
    
    has := [26]bool{}
    for _, mid := range s[i+1 : j] { // 枚举中间字母 mid
    if !has[mid-'a'] {
    has[mid-'a'] = true // 避免重复统计
    ans++
    }
    }
    }
    return
    }
    
    var countPalindromicSubsequence = function(s) {
        const ordA = 'a'.charCodeAt(0);
        let ans = 0;
        for (let alpha = ordA; alpha <= 'z'.charCodeAt(0); alpha++) { // 枚举两侧字母 alpha
            const ch = String.fromCharCode(alpha);
            const i = s.indexOf(ch); // 最左边的 alpha 的下标
            if (i < 0) { // s 中没有 alpha
                continue;
            }
            const j = s.lastIndexOf(ch); // 最右边的 alpha 的下标
    
            const has = Array(26).fill(false);
            for (let k = i + 1; k < j; k++) { // 枚举中间字母 mid
                const mid = s.charCodeAt(k) - ordA;
                if (!has[mid]) {
                    has[mid] = true; // 避免重复统计
                    ans++;
                }
            }
        }
        return ans;
    };
    
    impl Solution {
        pub fn count_palindromic_subsequence(s: String) -> i32 {
            let s = s.as_bytes();
            let mut ans = 0;
            for alpha in b'a'..=b'z' { // 枚举两侧字母 alpha
                let i = s.iter().position(|&c| c == alpha); // 找最左边的 alpha
                if i.is_none() { // s 中没有 alpha
                    continue;
                }
                let i = i.unwrap(); // 最左边的 alpha 的下标
                let j = s.iter().rposition(|&c| c == alpha).unwrap(); // 最右边的 alpha 的下标
    
                let mut has = [false; 26];
                for k in i + 1..j { // 枚举中间字母 mid
                    let mid = (s[k] - b'a') as usize;
                    if !has[mid] {
                        has[mid] = true; // 避免重复统计
                        ans += 1;
                    }
                }
            }
            ans
        }
    }
    

    复杂度分析

    • 时间复杂度:$\mathcal{O}(n|\Sigma|)$,其中 $n$ 是 $s$ 的长度,$|\Sigma|=26$ 是字符集合的大小。
    • 空间复杂度:$\mathcal{O}(|\Sigma|)$。

    方法二:枚举中间 + 前后缀分解

    枚举 $i=1,2,\ldots,n-2$,把 $s[i]$ 当作子序列的第二个字母,那么对于第一、三个字母 $\alpha = \texttt{a},\texttt{b},\ldots,\texttt{z}$,我们需要判断:

    • $s$ 的前缀 $[0,i-1]$ 中有没有 $\alpha$?
    • $s$ 的后缀 $[i+1,n-1]$ 中有没有 $\alpha$?

    暴力找是 $\mathcal{O}(n)$ 的,如何加速?

    我们可以先遍历 $s$,统计 $s$ 中每个字母的个数,然后再从左到右遍历 $s$,把 $s[i]$ 的个数减一,就得到了后缀 $[i+1,n-1]$ 每个字母的个数。

    对于前缀 $[0,i-1]$,在从左到右遍历 $s$ 的同时,记录遇到了哪些字母。

    优化前

    class Solution:
        def countPalindromicSubsequence(self, s: str) -> int:
            suf_cnt = Counter(s[1:])  # 统计 [1,n-1] 每个字母的个数
            pre_set = set()
            st = set()
            for i in range(1, len(s) - 1):  # 枚举中间字母 mid
                mid = s[i]
                suf_cnt[mid] -= 1  # 撤销 mid 的计数,suf_cnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
                if suf_cnt[mid] == 0:  # 后缀 [i+1,n-1] 不包含 mid
                    del suf_cnt[mid]  # 从 suf_cnt 中去掉 mid
                pre_set.add(s[i - 1])  # 记录前缀 [0,i-1] 有哪些字母
                for alpha in pre_set & suf_cnt.keys():  # mid 的左右两侧都有字母 alpha
                    st.add(alpha + mid)
            return len(st)
    
    class Solution {
        public int countPalindromicSubsequence(String S) {
            char[] s = S.toCharArray();
            int n = s.length;
    
            // 统计 [1,n-1] 每个字母的个数
            int[] sufCnt = new int[26];
            for (int i = 1; i < n; i++) {
                sufCnt[s[i] - 'a']++;
            }
    
            boolean[] preHas = new boolean[26];
            boolean[][] has = new boolean[26][26];
            int ans = 0;
            for (int i = 1; i < n - 1; i++) { // 枚举中间字母 mid
                int mid = s[i] - 'a';
                sufCnt[mid]--; // 撤销 mid 的计数,sufCnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
                preHas[s[i - 1] - 'a'] = true; // 记录前缀 [0,i-1] 有哪些字母
                for (int alpha = 0; alpha < 26; alpha++) { // 枚举两侧字母 alpha
                    // 判断 mid 的左右两侧是否都有字母 alpha
                    if (preHas[alpha] && sufCnt[alpha] > 0 && !has[mid][alpha]) {
                        has[mid][alpha] = true;
                        ans++;
                    }
                }
            }
            return ans;
        }
    }
    
    class Solution {
    public:
        int countPalindromicSubsequence(string s) {
            int n = s.size();
            // 统计 [1,n-1] 每个字母的个数
            int suf_cnt[26]{};
            for (int i = 1; i < n; i++) {
                suf_cnt[s[i] - 'a']++;
            }
    
            bool pre_has[26]{};
            bool has[26][26]{};
            int ans = 0;
            for (int i = 1; i < n - 1; i++) { // 枚举中间字母 mid
                int mid = s[i] - 'a';
                suf_cnt[mid]--; // 撤销 mid 的计数,suf_cnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
                pre_has[s[i - 1] - 'a'] = true; // 记录前缀 [0,i-1] 有哪些字母
                for (int alpha = 0; alpha < 26; alpha++) { // 枚举两侧字母 alpha
                    // 判断 mid 的左右两侧是否都有字母 alpha
                    if (pre_has[alpha] && suf_cnt[alpha] && !has[mid][alpha]) {
                        has[mid][alpha] = true;
                        ans++;
                    }
                }
            }
            return ans;
        }
    };
    
    int countPalindromicSubsequence(char* s) {
        // 统计 [1,n-1] 每个字母的个数
        int suf_cnt[26] = {}; 
        for (int i = 1; s[i]; i++) {
            suf_cnt[s[i] - 'a']++;
        }
    
        bool pre_has[26] = {};
        bool has[26][26] = {};
        int ans = 0;
        for (int i = 1; s[i + 1]; i++) { // 枚举中间字母 mid
            int mid = s[i] - 'a';
            suf_cnt[mid]--; // 撤销 mid 的计数,suf_cnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
            pre_has[s[i - 1] - 'a'] = true; // 记录前缀 [0,i-1] 有哪些字母
            for (int alpha = 0; alpha < 26; alpha++) { // 枚举两侧字母 alpha
                // 判断 mid 的左右两侧是否都有字母 alpha
                if (pre_has[alpha] && suf_cnt[alpha] && !has[mid][alpha]) {
                    has[mid][alpha] = true;
                    ans++;
                }
            }
        }
        return ans;
    }
    
    func countPalindromicSubsequence(s string) (ans int) {
    // 统计 s[1:] 每个字母的个数
    sufCnt := [26]int{} 
    for _, ch := range s[1:] {
    sufCnt[ch-'a']++
    }
    
    preHas := [26]bool{}
    has := [26][26]bool{}
    for i := 1; i < len(s)-1; i++ { // 枚举中间字母 mid
    mid := s[i] - 'a'
    sufCnt[mid]-- // 撤销 mid 的计数,suf_cnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
    preHas[s[i-1]-'a'] = true // 记录前缀 [0,i-1] 有哪些字母
    for alpha := range 26 { // 枚举两侧字母 alpha
    // 判断 mid 的左右两侧是否都有字母 alpha
    if preHas[alpha] && sufCnt[alpha] > 0 && !has[mid][alpha] {
    has[mid][alpha] = true
    ans++
    }
    }
    }
    return
    }
    
    var countPalindromicSubsequence = function(s) {
        const n = s.length;
        const ordA = 'a'.charCodeAt(0);
    
        // 统计 [1,n-1] 每个字母的个数
        const sufCnt = Array(26).fill(0);
        for (let i = 1; i < n; i++) {
            sufCnt[s.charCodeAt(i) - ordA]++;
        }
    
        const preHas = Array(26).fill(false);
        const has = Array.from({ length: 26 }, () => Array(26).fill(false));
        let ans = 0;
        for (let i = 1; i < n - 1; i++) { // 枚举中间字母 mid
            const mid = s.charCodeAt(i) - ordA;
            sufCnt[mid]--; // 撤销 mid 的计数,sufCnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
            preHas[s.charCodeAt(i - 1) - ordA] = true; // 记录前缀 [0,i-1] 有哪些字母
            for (let alpha = 0; alpha < 26; alpha++) { // 枚举两侧字母 alpha
                // 判断 mid 的左右两侧是否都有字母 alpha
                if (preHas[alpha] && sufCnt[alpha] && !has[mid][alpha]) {
                    has[mid][alpha] = true;
                    ans++;
                }
            }
        }
        return ans;
    };
    
    impl Solution {
        pub fn count_palindromic_subsequence(s: String) -> i32 {
            let s = s.as_bytes();
            let n = s.len();
    
            // 统计 [1,n-1] 每个字母的个数
            let mut suf_cnt = [0; 26]; 
            for &ch in &s[1..] {
                suf_cnt[(ch - b'a') as usize] += 1;
            }
    
            let mut pre_has = [false; 26];
            let mut has = [[false; 26]; 26];
            let mut ans = 0;
            for i in 1..n - 1 { // 枚举中间字母 mid
                let mid = (s[i] - b'a') as usize;
                suf_cnt[mid] -= 1; // 撤销 s[i] 的计数,suf_cnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
                pre_has[(s[i - 1] - b'a') as usize] = true; // 记录前缀 [0,i-1] 有哪些字母
                for alpha in 0..26 { // 枚举两侧字母 alpha
                    // 判断 mid 的左右两侧是否都有字母 alpha
                    if pre_has[alpha] && suf_cnt[alpha] > 0 && !has[mid][alpha] {
                        has[mid][alpha] = true;
                        ans += 1;
                    }
                }
            }
            ans
        }
    }
    

    复杂度分析

    • 时间复杂度:$\mathcal{O}(n|\Sigma|)$,其中 $n$ 是 $s$ 的长度,$|\Sigma|=26$ 是字符集合的大小。
    • 空间复杂度:$\mathcal{O}(|\Sigma|^2)$。

    位运算优化

    我们可以把集合或者布尔数组压缩成一个二进制数,二进制数中的 $0$ 表示 $\texttt{false}$,$1$ 表示 $\texttt{true}$。原理请看 从集合论到位运算,常见位运算技巧分类总结!

    用与运算可以 $\mathcal{O}(1)$ 求出 $\textit{pre}$ 和 $\textit{suf}$ 的交集。

    class Solution:
        def countPalindromicSubsequence(self, s: str) -> int:
            n = len(s)
            ord_a = ord('a')
    
            # 统计 [1,n-1] 每个字母的个数
            suf_cnt = [0] * 26
            suf = 0
            for ch in s[1:]:
                ch = ord(ch) - ord_a
                suf_cnt[ch] += 1
                suf |= 1 << ch  # 把 ch 记录到二进制数 suf 中,表示后缀有 ch
    
            pre = 0
            ans = [0] * 26  # ans[mid] = 由 alpha 组成的二进制数
            for i in range(1, n - 1):  # 枚举中间字母 mid
                mid = ord(s[i]) - ord_a
                suf_cnt[mid] -= 1  # 撤销 mid 的计数,suf_cnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
                if suf_cnt[mid] == 0:  # 后缀 [i+1,n-1] 不包含 mid
                    suf ^= 1 << mid  # 从 suf 中去掉 mid
                pre |= 1 << (ord(s[i - 1]) - ord_a)  # 把 s[i-1] 记录到二进制数 pre 中,表示前缀有 s[i-1]
                ans[mid] |= pre & suf  # 计算 pre 和 suf 的交集,|= 表示把交集中的字母加到 ans[mid] 中
    
            return sum(mask.bit_count() for mask in ans)  # mask 中的每个 1 对应着一个 alpha
    
    class Solution {
        public int countPalindromicSubsequence(String S) {
            char[] s = S.toCharArray();
            int n = s.length;
    
            // 统计 [1,n-1] 每个字母的个数
            int[] sufCnt = new int[26];
            int suf = 0;
            for (int i = 1; i < n; i++) {
                int ch = s[i] - 'a';
                sufCnt[ch]++;
                suf |= 1 << ch; // 把 ch 记录到二进制数 suf 中,表示后缀有 ch
            }
    
            int pre = 0;
            int[] has = new int[26]; // has[mid] = 由 alpha 组成的二进制数
            for (int i = 1; i < n - 1; i++) { // 枚举中间字母 mid
                int mid = s[i] - 'a';
                sufCnt[mid]--; // 撤销 mid 的计数,sufCnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
                if (sufCnt[mid] == 0) { // 后缀 [i+1,n-1] 不包含 mid
                    suf ^= 1 << mid; // 从 suf 中去掉 mid
                }
                pre |= 1 << (s[i - 1] - 'a'); // 把 s[i-1] 记录到二进制数 pre 中,表示前缀有 s[i-1]
                has[mid] |= pre & suf; // 计算 pre 和 suf 的交集,|= 表示把交集中的字母加到 has[mid] 中
            }
    
            int ans = 0;
            for (int mask : has) {
                ans += Integer.bitCount(mask); // mask 中的每个 1 对应着一个 alpha
            }
            return ans;
        }
    }
    
    class Solution {
    public:
        int countPalindromicSubsequence(string s) {
            int n = s.size();
            // 统计 [1,n-1] 每个字母的个数
            int suf_cnt[26]{};
            int suf = 0;
            for (int i = 1; i < n; i++) {
                int ch = s[i] - 'a';
                suf_cnt[ch]++;
                suf |= 1 << ch; // 把 ch 记录到二进制数 suf 中,表示后缀有 ch
            }
    
            int pre = 0;
            int has[26]{}; // has[mid] = 由 alpha 组成的二进制数
            for (int i = 1; i < n - 1; i++) { // 枚举中间字母 mid
                int mid = s[i] - 'a';
                suf_cnt[mid]--; // 撤销 mid 的计数,suf_cnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
                if (suf_cnt[mid] == 0) { // 后缀 [i+1,n-1] 不包含 mid
                    suf ^= 1 << mid; // 从 suf 中去掉 mid
                }
                pre |= 1 << (s[i - 1] - 'a'); // 把 s[i-1] 记录到二进制数 pre 中,表示前缀有 s[i-1]
                has[mid] |= pre & suf; // 计算 pre 和 suf 的交集,|= 表示把交集中的字母加到 has[mid] 中
            }
    
            int ans = 0;
            for (int mask : has) {
                ans += popcount((uint32_t) mask); // mask 中的每个 1 对应着一个 alpha
            }
            return ans;
        }
    };
    
    int countPalindromicSubsequence(char* s) {
        int n = strlen(s);
        // 统计 [1,n-1] 每个字母的个数
        int suf_cnt[26] = {};
        int suf = 0;
        for (int i = 1; s[i]; i++) {
            int ch = s[i] - 'a';
            suf_cnt[ch]++;
            suf |= 1 << ch; // 把 ch 记录到二进制数 suf 中,表示后缀有 ch
        }
    
        int pre = 0;
        int has[26] = {}; // has[mid] = 由 alpha 组成的二进制数
        for (int i = 1; s[i + 1]; i++) { // 枚举中间字母 mid
            int mid = s[i] - 'a';
            suf_cnt[mid]--; // 撤销 mid 的计数,suf_cnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
            if (suf_cnt[mid] == 0) { // 后缀 [i+1,n-1] 不包含 mid
                suf ^= 1 << mid; // 从 suf 中去掉 mid
            }
            pre |= 1 << (s[i - 1] - 'a'); // 把 s[i-1] 记录到二进制数 pre 中,表示前缀有 s[i-1]
            has[mid] |= pre & suf; // 计算 pre 和 suf 的交集,|= 表示把交集中的字母加到 has[mid] 中
        }
    
        int ans = 0;
        for (int i = 0; i < 26; i++) {
            ans += __builtin_popcount(has[i]); // has[i] 中的每个 1 对应着一个 alpha
        }
        return ans;
    }
    
    func countPalindromicSubsequence(s string) (ans int) {
    // 统计 [1,n-1] 每个字母的个数
    sufCnt := [26]int{}
    suf := 0
    for _, ch := range s[1:] {
    ch -= 'a'
    sufCnt[ch]++
    suf |= 1 << ch // 把 ch 记录到二进制数 suf 中,表示后缀有 ch
    }
    
    pre := 0
    has := [26]int{} // has[mid] = 由 alpha 组成的二进制数
    for i := 1; i < len(s)-1; i++ { // 枚举中间字母 mid
    mid := s[i] - 'a'
    sufCnt[mid]-- // 撤销 mid 的计数,sufCnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
    if sufCnt[mid] == 0 { // 后缀 [i+1,n-1] 不包含 mid
    suf ^= 1 << mid // 从 suf 中去掉 mid
    }
    pre |= 1 << (s[i-1] - 'a') // 把 s[i-1] 记录到二进制数 pre 中,表示前缀有 s[i-1]
    has[mid] |= pre & suf // 计算 pre 和 suf 的交集,|= 表示把交集中的字母加到 has[mid] 中
    }
    
    for _, mask := range has {
    ans += bits.OnesCount(uint(mask)) // mask 中的每个 1 对应着一个 alpha
    }
    return
    }
    
    var countPalindromicSubsequence = function(s) {
        const n = s.length;
        const ordA = 'a'.charCodeAt(0);
    
        // 统计 [1,n-1] 每个字母的个数
        const sufCnt = Array(26).fill(0);
        let suf = 0;
        for (let i = 1; i < n; i++) {
            const ch = s.charCodeAt(i) - ordA;
            sufCnt[ch]++;
            suf |= 1 << ch; // 把 ch 记录到二进制数 suf 中,表示后缀有 ch
        }
    
        let pre = 0;
        const has = Array(26).fill(0); // has[mid] = 由 alpha 组成的二进制数
        for (let i = 1; i < n - 1; i++) { // 枚举中间字母 mid
            const mid = s.charCodeAt(i) - ordA;
            sufCnt[mid]--; // 撤销 mid 的计数,sufCnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
            if (sufCnt[mid] === 0) { // 后缀 [i+1,n-1] 不包含 mid
                suf ^= 1 << mid; // 从 suf 中去掉 mid
            }
            pre |= 1 << (s.charCodeAt(i - 1) - ordA); // 把 s[i-1] 记录到二进制数 pre 中,表示前缀有 s[i-1]
            has[mid] |= pre & suf; // 计算 pre 和 suf 的交集,|= 表示把交集中的字母加到 has[mid] 中
        }
    
        let ans = 0;
        for (const mask of has) {
            ans += bitCount32(mask); // mask 中的每个 1 对应着一个 alpha
        }
        return ans;
    };
    
    // 参考 Java 的 Integer.bitCount
    function bitCount32(i) {
        i = i - ((i >>> 1) & 0x55555555);
        i = (i & 0x33333333) + ((i >>> 2) & 0x33333333);
        i = (i + (i >>> 4)) & 0x0f0f0f0f;
        i = i + (i >>> 8);
        i = i + (i >>> 16);
        return i & 0x3f;
    }
    
    impl Solution {
        pub fn count_palindromic_subsequence(s: String) -> i32 {
            let s = s.as_bytes();
            let n = s.len();
    
            // 统计 [1,n-1] 每个字母的个数
            let mut suf_cnt = [0; 26];
            let mut suf = 0;
            for &ch in &s[1..] {
                let ch = (ch - b'a') as usize;
                suf_cnt[ch] += 1;
                suf |= 1 << ch; // 把 ch 记录到二进制数 suf 中,表示后缀有 ch
            }
    
            let mut pre = 0;
            let mut has = [0u32; 26]; // has[mid] = 由 alpha 组成的二进制数
            for i in 1..n - 1 { // 枚举中间字母 mid
                let mid = (s[i] - b'a') as usize;
                suf_cnt[mid] -= 1; // 撤销 mid 的计数,suf_cnt 剩下的就是后缀 [i+1,n-1] 每个字母的个数
                if suf_cnt[mid] == 0 { // 后缀 [i+1,n-1] 不包含 mid
                    suf ^= 1 << mid; // 从 suf 中去掉 mid
                }
                pre |= 1 << (s[i - 1] - b'a'); // 把 s[i-1] 记录到二进制数 pre 中,表示前缀有 s[i-1]
                has[mid] |= pre & suf; // 计算 pre 和 suf 的交集,|= 表示把交集中的字母加到 has[mid] 中
            }
    
            // mask 中的每个 1 对应着一个 alpha
            has.into_iter().map(|mask| mask.count_ones() as i32).sum()
        }
    }
    

    复杂度分析

    • 时间复杂度:$\mathcal{O}(n + |\Sigma|)$,其中 $n$ 是 $s$ 的长度,$|\Sigma|=26$ 是字符集合的大小。
    • 空间复杂度:$\mathcal{O}(|\Sigma|)$。

    进阶问题

    1. 计算不同子序列的个数:940. 不同的子序列 II
    2. 计算不同回文子序列的个数:730. 统计不同回文子序列

    专题训练

    1. 数据结构题单的「§0.2 枚举中间」。
    2. 动态规划题单的「专题:前后缀分解」。

    分类题单

    如何科学刷题?

    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)

    把 $2$ 改成 $k$ 怎么做?

    更一般地,每个区间要包含的数字个数各有不同,怎么做?

    这题是 2589. 完成所有任务的最少时间我的题解

    class Solution:
        def intersectionSizeTwo(self, intervals: List[List[int]]) -> int:
            intervals.sort(key=lambda interval: interval[1])
            # 栈中保存闭区间左右端点,栈底到栈顶的区间长度的和
            st = [(-2, -2, 0)]  # 哨兵,保证不和任何区间相交
            for start, end in intervals:
                _, r, s = st[bisect_left(st, (start,)) - 1]
                d = 2 - (st[-1][2] - s)  # 去掉运行中的时间点
                if start <= r:  # start 在区间 st[i] 内
                    d -= r - start + 1  # 去掉运行中的时间点
                if d <= 0:
                    continue
                while end - st[-1][1] <= d:  # 剩余的 d 填充区间后缀
                    l, r, _ = st.pop()
                    d += r - l + 1  # 合并区间
                st.append((end - d + 1, end, st[-1][2] + d))
            return st[-1][2]
    
    class Solution {
        public int intersectionSizeTwo(int[][] intervals) {
            Arrays.sort(intervals, (a, b) -> a[1] - b[1]);
            // 栈中保存闭区间左右端点,栈底到栈顶的区间长度的和
            List<int[]> st = new ArrayList<>();
            st.add(new int[]{-2, -2, 0}); // 哨兵,保证不和任何区间相交
            for (int[] t : intervals) {
                int start = t[0], end = t[1];
                int[] e = st.get(lowerBound(st, start) - 1);
                int d = 2 - (st.get(st.size() - 1)[2] - e[2]); // 去掉运行中的时间点
                if (start <= e[1]) { // start 在区间 st[i] 内
                    d -= e[1] - start + 1; // 去掉运行中的时间点
                }
                if (d <= 0) {
                    continue;
                }
                while (end - st.get(st.size() - 1)[1] <= d) { // 剩余的 d 填充区间后缀
                    e = st.remove(st.size() - 1);
                    d += e[1] - e[0] + 1; // 合并区间
                }
                st.add(new int[]{end - d + 1, end, st.get(st.size() - 1)[2] + d});
            }
            return st.get(st.size() - 1)[2];
        }
    
        // 开区间二分
        // 见 https://www.bilibili.com/video/BV1AP41137w7/
        private int lowerBound(List<int[]> st, int target) {
            int left = -1, right = st.size(); // 开区间 (left, right)
            while (left + 1 < right) { // 区间不为空
                // 循环不变量:
                // st[left] < target
                // st[right] >= target
                int mid = (left + right) >>> 1;
                if (st.get(mid)[0] < target) {
                    left = mid; // 范围缩小到 (mid, right)
                } else {
                    right = mid; // 范围缩小到 (left, mid)
                }
            }
            return right;
        }
    }
    
    class Solution {
    public:
        int intersectionSizeTwo(vector<vector<int>>& intervals) {
            ranges::sort(intervals, {}, [](auto& a) { return a[1]; });
            // 栈中保存闭区间左右端点,栈底到栈顶的区间长度的和
            vector<array<int, 3>> st = {{-2, -2, 0}}; // 哨兵,保证不和任何区间相交
            for (auto& t : intervals) {
                int start = t[0], end = t[1];
                auto [_, r, s] = *--ranges::lower_bound(st, start, {}, [](auto& x) { return x[0]; });
                int d = 2 - (st.back()[2] - s); // 去掉运行中的时间点
                if (start <= r) { // start 在区间 st[i] 内
                    d -= r - start + 1; // 去掉运行中的时间点
                }
                if (d <= 0) {
                    continue;
                }
                while (end - st.back()[1] <= d) { // 剩余的 d 填充区间后缀
                    auto [l, r, _] = st.back();
                    st.pop_back();
                    d += r - l + 1; // 合并区间
                }
                st.push_back({end - d + 1, end, st.back()[2] + d});
            }
            return st.back()[2];
        }
    };
    
    func intersectionSizeTwo(intervals [][]int) int {
        slices.SortFunc(intervals, func(a, b []int) int { return a[1] - b[1] })
        // 栈中保存闭区间左右端点,栈底到栈顶的区间长度的和
        type tuple struct{ l, r, s int }
        st := []tuple{{-2, -2, 0}} // 哨兵,保证不和任何区间相交
        for _, p := range intervals {
            start, end := p[0], p[1]
            i := sort.Search(len(st), func(i int) bool { return st[i].l >= start }) - 1
            d := 2 - (st[len(st)-1].s - st[i].s) // 去掉运行中的时间点
            if start <= st[i].r { // start 在区间 st[i] 内
                d -= st[i].r - start + 1 // 去掉运行中的时间点
            }
            if d <= 0 {
                continue
            }
            for end-st[len(st)-1].r <= d { // 剩余的 d 填充区间后缀
                top := st[len(st)-1]
                st = st[:len(st)-1]
                d += top.r - top.l + 1 // 合并区间
            }
            st = append(st, tuple{end - d + 1, end, st[len(st)-1].s + d})
        }
        return st[len(st)-1].s
    }
    

    复杂度分析

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

    相似题目

    见下面贪心题单的「§2.3 区间选点」。

    分类题单

    如何科学刷题?

    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站@灵茶山艾府

    每日一题-设置交集大小至少为2🔴

    给你一个二维整数数组 intervals ,其中 intervals[i] = [starti, endi] 表示从 startiendi 的所有整数,包括 startiendi

    包含集合 是一个名为 nums 的数组,并满足 intervals 中的每个区间都 至少两个 整数在 nums 中。

    • 例如,如果 intervals = [[1,3], [3,7], [8,9]] ,那么 [1,2,4,7,8,9][2,3,4,8,9] 都符合 包含集合 的定义。

    返回包含集合可能的最小大小。

     

    示例 1:

    输入:intervals = [[1,3],[3,7],[8,9]]
    输出:5
    解释:nums = [2, 3, 4, 8, 9].
    可以证明不存在元素数量为 4 的包含集合。
    

    示例 2:

    输入:intervals = [[1,3],[1,4],[2,5],[3,5]]
    输出:3
    解释:nums = [2, 3, 4].
    可以证明不存在元素数量为 2 的包含集合。 
    

    示例 3:

    输入:intervals = [[1,2],[2,3],[2,4],[4,5]]
    输出:5
    解释:nums = [1, 2, 3, 4, 5].
    可以证明不存在元素数量为 4 的包含集合。 
    

     

    提示:

    • 1 <= intervals.length <= 3000
    • intervals[i].length == 2
    • 0 <= starti < endi <= 108
    ❌