阅读视图

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

每日一题-找出有效子序列的最大长度 II🟡

给你一个整数数组 nums 和一个  整数 k 。

nums 的一个 子序列 sub 的长度为 x ,如果其满足以下条件,则称其为 有效子序列 :

  • (sub[0] + sub[1]) % k == (sub[1] + sub[2]) % k == ... == (sub[x - 2] + sub[x - 1]) % k
返回 nums 的 最长有效子序列 的长度。

 

示例 1:

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

输出:5

解释:

最长有效子序列是 [1, 2, 3, 4, 5] 。

示例 2:

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

输出:4

解释:

最长有效子序列是 [1, 4, 1, 4] 。

 

提示:

  • 2 <= nums.length <= 103
  • 1 <= nums[i] <= 107
  • 1 <= k <= 103

3202. 找出有效子序列的最大长度 II

解法

思路和算法

长度为 $x$ 的有效子序列 $\textit{sub}$ 满足对于任意 $1 \le i \le x - 2$ 都有 $(\textit{sub}[i - 1] + \textit{sub}[i]) \bmod k = (\textit{sub}[i] + \textit{sub}[i + 1]) \bmod k$,等价于 $(\textit{sub}[i - 1] + \textit{sub}[i] - \textit{sub}[i] - \textit{sub}[i + 1]) \bmod k = 0$,整理得 $(\textit{sub}[i - 1] - \textit{sub}[i + 1]) \bmod k = 0$,即 $\textit{sub}[i - 1] \bmod k = \textit{sub}[i + 1] \bmod k$。因此子序列 $\textit{sub}$ 满足子序列中的下标相差 $2$ 的两项除以 $k$ 的余数相等,进一步可以得到子序列 $\textit{sub}$ 满足所有偶数下标项除以 $k$ 的余数相等且所有奇数下标项除以 $k$ 的余数相等。

为了计算最长有效子序列的长度,对于所有满足 $0 \le i, j < k$ 的整数 $i$ 和 $j$ 需要计算最后两项除以 $k$ 的余数分别是 $i$ 和 $j$ 的子序列的最大长度。

创建 $k \times k$ 的二维数组 $\textit{dp}$,其中 $\textit{dp}[i][j]$ 表示最后两项除以 $k$ 的余数分别是 $i$ 和 $j$ 的子序列的最大长度。

动态规划的边界情况是:如果当前没有最后两项除以 $k$ 的余数分别是 $i$ 和 $j$ 的子序列,则 $\textit{dp}[i][j] = 0$。初始时,$\textit{dp}$ 中的所有状态值都是 $0$。

遍历数组 $\textit{nums}$,对于遍历到的每个元素 $\textit{num}$,执行如下操作。

  1. 计算当前余数 $\textit{curr} = \textit{num} \bmod k$。

  2. 遍历 $0 \le \textit{prev} < k$ 的每个余数 $\textit{prev}$,为了得到最后两项除以 $k$ 的余数分别是 $\textit{prev}$ 和 $\textit{curr}$ 的最长有效子序列,需要首先得到最后两项除以 $k$ 的余数分别是 $\textit{curr}$ 和 $\textit{prev}$ 的最长有效子序列,然后在末尾添加 $\textit{num}$ 得到更长的有效子序列,因此将 $\textit{dp}[\textit{prev}][\textit{curr}]$ 的值更新为 $\textit{dp}[\textit{curr}][\textit{prev}] + 1$,然后使用 $\textit{dp}[\textit{prev}][\textit{curr}]$ 更新最长有效子序列的长度。

上述计算过程即为动态规划的状态转移方程。

遍历结束之后,即可得到最长有效子序列的长度。

代码

###Java

class Solution {
    public int maximumLength(int[] nums, int k) {
        int maxLength = 0;
        int[][] dp = new int[k][k];
        for (int num : nums) {
            int curr = num % k;
            for (int prev = 0; prev < k; prev++) {
                dp[prev][curr] = dp[curr][prev] + 1;
                maxLength = Math.max(maxLength, dp[prev][curr]);
            }
        }
        return maxLength;
    }
}

###C#

public class Solution {
    public int MaximumLength(int[] nums, int k) {
        int maxLength = 0;
        int[][] dp = new int[k][];
        for (int i = 0; i < k; i++) {
            dp[i] = new int[k];
        }
        foreach (int num in nums) {
            int curr = num % k;
            for (int prev = 0; prev < k; prev++) {
                dp[prev][curr] = dp[curr][prev] + 1;
                maxLength = Math.Max(maxLength, dp[prev][curr]);
            }
        }
        return maxLength;
    }
}

复杂度分析

  • 时间复杂度:$O(k^2 + nk)$,其中 $k$ 是给定的整数,$n$ 是数组 $\textit{nums}$ 的长度。创建大小为 $k \times k$ 的二维数组 $\textit{dp}$ 的时间是 $O(k^2)$,遍历数组 $\textit{nums}$ 时对于每个元素的计算时间是 $O(k)$,因此时间复杂度是 $O(k^2 + nk)$。

  • 空间复杂度:$O(k^2)$,其中 $k$ 是给定的整数。需要创建大小为 $k \times k$ 的二维数组。

时间O(n^2) 空间O(n*k) 的动态规划

思路

  1. 本题是选与不选的子序列问题,可以尝试给出这样的状态定义f[i]:以nums[i]结尾的符合条件的最长子序列的长度
  2. 那么什么是符合条件的子序列呢?数组中任意两个数相加模k都符合条件,为了表示模k后的值,可以再增加一个维度的数组f[i][j]:以nums[i]结尾模k后值为j的最长子序列的长度
  3. 那么状态转移方程是怎样的呢?对于每一个i,遍历j(0<=j<i)f[i][(nums[i] + nums[j]) % k] = f[j][(nums[i] + nums[j]) % k] + 1,保证模k后的值相同。

代码

###Python3

class Solution:
    def maximumLength(self, nums: List[int], k: int) -> int:
        n = len(nums)
        f = [[1] * k for _ in range(n)]
        ans = 1
        for i in range(1, n):
            for j in range(i):
                f[i][(nums[i] + nums[j]) % k] = f[j][(nums[i] + nums[j]) % k] + 1
                ans = max(ans, f[i][(nums[i] + nums[j]) % k])
        return ans

复杂度分析

  • 时间复杂度:O(n^2)
  • 空间复杂度:O(n*k)

分析式子+动态规划,两种方法(Python/Java/C++/Go)

分析

对于等式

$$
(a+b)\bmod k = (b+c)\bmod k
$$

根据 模运算的世界:当加减乘除遇上取模,可以移项,得

$$
(a+b-(b+c)) \bmod k = 0
$$

化简得

$$
(a-c)\bmod k = 0
$$

这意味着 $a$ 与 $c$ 关于模 $k$ 同余。即题目式子中的 $\textit{sub}[i]$ 与 $\textit{sub}[i+2]$ 关于模 $k$ 同余。换句话说,有效子序列的偶数项 $\textit{sub}[0],\textit{sub}[2],\textit{sub}[4],\ldots$ 都关于模 $k$ 同余,奇数项 $\textit{sub}[1],\textit{sub}[3],\textit{sub}[5],\ldots$ 都关于模 $k$ 同余。

如果把每个 $\textit{nums}[i]$ 都改成 $\textit{nums}[i]\bmod k$,问题等价于:

  • 求最长子序列的长度,该子序列的奇数项都相同,偶数项都相同。

方法一:考察子序列的最后两项

比如 $\textit{nums}=[1,2,1,2,1,2]$:

  • 遍历到 $1$ 的时候,在「末尾为 $1,2$ 的子序列」的末尾添加一个 $1$,得到「末尾为 $2,1$ 的子序列」。
  • 遍历到 $2$ 的时候,在「末尾为 $2,1$ 的子序列」的末尾添加一个 $2$,得到「末尾为 $1,2$ 的子序列」。

从左到右遍历 $\textit{nums}$,遍历的同时,维护一个二维数组 $f[y][x]$,表示最后两项模 $k$ 分别为 $y$ 和 $x$ 的子序列的长度。

对于 $x=\textit{nums}[i]\bmod k$,我们可以在「最后两项模 $k$ 分别为 $x$ 和 $y$ 的子序列」的末尾添加 $\textit{nums}[i]$,那么「最后两项模 $k$ 分别为 $y$ 和 $x$ 的子序列」的长度会增加 $1$,即

$$
f[y][x] = f[x][y] + 1
$$

最后答案为 $f[i][j]$ 的最大值。

答疑

:如何理解这个递推?它和记忆化搜索的区别是什么?

:对比二者的计算顺序。如果用记忆化搜索来做,需要单独计算「最左(或者最右)两项模 $k$ 分别为 $x$ 和 $y$ 的子序列」的长度,这是「单线程」,必须查找下一个元素的位置。而递推的计算顺序是,(假设我们先遍历到了元素 $2$,然后遍历到了元素 $4$,两个元素属于不同的子序列)一会计算一下「最后两项模 $k$ 分别为 $y$ 和 $2$ 的子序列」,一会又计算一下「最后两项模 $k$ 分别为 $y$ 和 $4$ 的子序列」,这是「多线程」,没有查找元素位置的过程,遇到谁就处理谁

具体请看 视频讲解 第三题,欢迎点赞关注~

###py

class Solution:
    def maximumLength(self, nums: List[int], k: int) -> int:
        f = [[0] * k for _ in range(k)]
        for x in nums:
            x %= k
            for y, fxy in enumerate(f[x]):
                f[y][x] = fxy + 1
        return max(map(max, f))

###py

class Solution:
    def maximumLength(self, nums: List[int], k: int) -> int:
        f = [0] * (k * k)
        for x in nums:
            x %= k
            # f[x * k: (x + 1) * k] 是二维写法的第 x 行
            # f[x::k] 是二维写法的第 x 列
            f[x::k] = [v + 1 for v in f[x * k: (x + 1) * k]]
        return max(f)

###java

class Solution {
    public int maximumLength(int[] nums, int k) {
        int ans = 0;
        int[][] f = new int[k][k];
        for (int x : nums) {
            x %= k;
            for (int y = 0; y < k; y++) {
                f[y][x] = f[x][y] + 1;
                ans = Math.max(ans, f[y][x]);
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maximumLength(vector<int>& nums, int k) {
        int ans = 0;
        vector f(k, vector<int>(k));
        for (int x : nums) {
            x %= k;
            for (int y = 0; y < k; y++) {
                f[y][x] = f[x][y] + 1;
                ans = max(ans, f[y][x]);
            }
        }
        return ans;
    }
};

###go

func maximumLength(nums []int, k int) (ans int) {
f := make([][]int, k)
for i := range f {
f[i] = make([]int, k)
}
for _, x := range nums {
x %= k
for y, fxy := range f[x] {
f[y][x] = fxy + 1
ans = max(ans, f[y][x])
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(k^2 + nk)$,其中 $n$ 是 $\textit{nums}$ 的长度。注意创建大小为 $k^2$ 的二维数组需要 $\mathcal{O}(k^2)$ 的时间。
  • 空间复杂度:$\mathcal{O}(k^2)$。

方法二:枚举余数,考察子序列的最后一项

枚举子序列相邻两项之和模 $k$ 的结果为 $m=0,1,2,\ldots, k-1$。

如果知道了子序列的最后一项(假设是 $x$),那么子序列的倒数第二项也就确定了,根据 模运算的世界:当加减乘除遇上取模,倒数第二项为

$$
(m - x\bmod k + k) \bmod k
$$

加 $k$ 再模 $k$ 是为了在 $m < x\bmod k$ 时,保证计算结果非负。

类似方法一,从左到右遍历 $\textit{nums}$ 的同时,维护一个数组 $f[x]$,表示最后一项模 $k$ 为 $x$ 的子序列的长度。

对于 $x=\textit{nums}[i]\bmod k$,我们可以在「最后一项模 $k$ 为 $(m - x\bmod k + k) \bmod k$ 的子序列」的末尾添加 $\textit{nums}[i]$,那么「最后一项模 $k$ 为 $x$ 的子序列」的长度会增加 $1$,即

$$
f[x] = f[(m - x\bmod k + k) \bmod k] + 1
$$

Python 取模更简单,由于允许负数下标,可以直接用 $f[m-x\bmod k]$ 作为转移来源。

遍历结束后(或者遍历中),用 $f[i]$ 更新答案的最大值。

###py

class Solution:
    def maximumLength(self, nums: List[int], k: int) -> int:
        ans = 0
        for m in range(k):
            f = [0] * k
            for x in nums:
                x %= k
                f[x] = f[m - x] + 1
            ans = max(ans, max(f))
        return ans

###java

class Solution {
    public int maximumLength(int[] nums, int k) {
        int ans = 0;
        for (int m = 0; m < k; m++) {
            int[] f = new int[k];
            for (int x : nums) {
                x %= k;
                f[x] = f[(m - x + k) % k] + 1;
                ans = Math.max(ans, f[x]);
            }
        }
        return ans;
    }
}

###cpp

class Solution {
public:
    int maximumLength(vector<int>& nums, int k) {
        int ans = 0;
        for (int m = 0; m < k; m++) {
            vector<int> f(k);
            for (int x : nums) {
                x %= k;
                f[x] = f[(m - x + k) % k] + 1;
                ans = max(ans, f[x]);
            }
        }
        return ans;
    }
};

###go

func maximumLength(nums []int, k int) (ans int) {
f := make([]int, k)
for m := 0; m < k; m++ {
clear(f)
for _, x := range nums {
x %= k
f[x] = f[(m-x+k)%k] + 1
ans = max(ans, f[x])
}
}
return
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(k(k+n))$,其中 $n$ 是 $\textit{nums}$ 的长度。注意创建大小为 $k$ 的数组需要 $\mathcal{O}(k)$ 的时间。
  • 空间复杂度:$\mathcal{O}(k)$。

专题训练

见动态规划题单的「§7.4 合法子序列 DP」。

分类题单

如何科学刷题?

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

每日一题-找出有效子序列的最大长度 I🟡

给你一个整数数组 nums

nums 的子序列 sub 的长度为 x ,如果其满足以下条件,则称其为 有效子序列

  • (sub[0] + sub[1]) % 2 == (sub[1] + sub[2]) % 2 == ... == (sub[x - 2] + sub[x - 1]) % 2

返回 nums最长的有效子序列 的长度。

一个 子序列 指的是从原数组中删除一些元素(也可以不删除任何元素),剩余元素保持原来顺序组成的新数组。

 

示例 1:

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

输出: 4

解释:

最长的有效子序列是 [1, 2, 3, 4]

示例 2:

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

输出: 6

解释:

最长的有效子序列是 [1, 2, 1, 2, 1, 2]

示例 3:

输入: nums = [1,3]

输出: 2

解释:

最长的有效子序列是 [1, 3]

 

提示:

  • 2 <= nums.length <= 2 * 105
  • 1 <= nums[i] <= 107

3201. 找出有效子序列的最大长度 I

解法一

思路和算法

有效子序列满足子序列的任意相邻两项之和除以 $2$ 的余数相等,因此有效子序列中的元素奇偶性有如下四种情况。

  • 有效子序列中的所有元素都是偶数。

  • 有效子序列中的所有元素都是奇数。

  • 有效子序列中的所有元素的奇偶性交替出现,且末尾元素是偶数。

  • 有效子序列中的所有元素的奇偶性交替出现,且末尾元素是奇数。

对于每个有效子序列,在移除最后一个元素之后,剩余元素仍是有效子序列且有效子序列的元素奇偶性不变,因此可以使用动态规划计算最长有效子序列的长度。

用 $n$ 表示数组 $\textit{nums}$ 的长度。创建 $n \times 2 \times 2$ 的三维数组 $\textit{dp}$,其中 $\textit{dp}[i][0][0]$ 表示数组 $\textit{nums}$ 的下标范围 $[0, i]$ 中的所有元素的奇偶性相同且都是偶数的最长有效子序列的长度,$\textit{dp}[i][0][1]$ 表示数组 $\textit{nums}$ 的下标范围 $[0, i]$ 中的所有元素的奇偶性相同且都是奇数的最长有效子序列的长度, $\textit{dp}[i][1][0]$ 表示数组 $\textit{nums}$ 的下标范围 $[0, i]$ 中的所有元素的奇偶性交替出现且末尾元素是偶数的最长有效子序列的长度,$\textit{dp}[i][1][1]$ 表示数组 $\textit{nums}$ 的下标范围 $[0, i]$ 中的所有元素的奇偶性交替出现且末尾元素是奇数的最长有效子序列的长度。

当 $i = 0$ 时,数组 $\textit{nums}$ 的下标范围 $[0, i]$ 中只有一个元素 $\textit{nums}[0]$,因此动态规划的边界情况如下。

  • 当 $\textit{nums}[0]$ 是偶数时,$\textit{dp}[0][0][0] = \textit{dp}[0][1][0] = 1$,$\textit{dp}[0][0][1] = \textit{dp}[0][1][1] = 0$。

  • 当 $\textit{nums}[0]$ 是奇数时,$\textit{dp}[0][0][0] = \textit{dp}[0][1][0] = 0$,$\textit{dp}[0][0][1] = \textit{dp}[0][1][1] = 1$。

当 $i > 0$ 时,需要根据 $\textit{nums}[i]$ 的奇偶性计算 $\textit{dp}[i][][]$ 的状态值。

  • 当 $\textit{nums}[i]$ 是偶数时,如果 $\textit{nums}[i]$ 是有效子序列的末尾元素,则有效子序列的末尾元素是偶数,此时可以在所有元素都是偶数或者所有元素的奇偶性交替出现且末尾元素是奇数的有效子序列的后面添加 $\textit{nums}[i]$ 得到更长的有效子序列。

  • 当 $\textit{nums}[i]$ 是奇数时,如果 $\textit{nums}[i]$ 是有效子序列的末尾元素,则有效子序列的末尾元素是奇数,此时可以在所有元素都是奇数或者所有元素的奇偶性交替出现且末尾元素是偶数的有效子序列的后面添加 $\textit{nums}[i]$ 得到更长的有效子序列。

因此动态规划的状态转移方程如下。

  • 当 $\textit{nums}[i]$ 是偶数时,$\textit{dp}[i][0][0] = \textit{dp}[i - 1][0][0] + 1$,$\textit{dp}[i][0][1] = \textit{dp}[i - 1][0][1]$,$\textit{dp}[i][1][0] = \textit{dp}[i - 1][1][1] + 1$,$\textit{dp}[i][1][1] = \textit{dp}[i - 1][1][1]$。

  • 当 $\textit{nums}[i]$ 是奇数时,$\textit{dp}[i][0][0] = \textit{dp}[i - 1][0][0]$,$\textit{dp}[i][0][1] = \textit{dp}[i - 1][0][1] + 1$,$\textit{dp}[i][1][0] = \textit{dp}[i - 1][1][0]$,$\textit{dp}[i][1][1] = \textit{dp}[i - 1][1][0] + 1$。

根据动态规划的状态转移方程,应从小到大遍历每个 $i$ 并计算 $\textit{dp}[i][][]$。计算所有的状态之后,$\textit{dp}[n - 1][][]$ 中的最大值即为最长有效子序列的长度。

上述做法的时间复杂度和空间复杂度都是 $O(n)$。

实现方面,由于 $\textit{dp}[i][][]$ 只取决于 $\textit{dp}[i - 1][][]$,和更早的状态无关,因此可以使用滚动数组的思想,只保留前一个 $i$ 处的状态,将空间复杂度降到 $O(1)$。

代码

下面的代码为不优化空间的实现。

###Java

class Solution {
    public int maximumLength(int[] nums) {
        int n = nums.length;
        int[][][] dp = new int[n][2][2];
        dp[0][0][nums[0] % 2] = 1;
        dp[0][1][nums[0] % 2] = 1;
        for (int i = 1; i < n; i++) {
            if (nums[i] % 2 == 0) {
                dp[i][0][0] = dp[i - 1][0][0] + 1;
                dp[i][0][1] = dp[i - 1][0][1];
                dp[i][1][0] = dp[i - 1][1][1] + 1;
                dp[i][1][1] = dp[i - 1][1][1];
            } else {
                dp[i][0][0] = dp[i - 1][0][0];
                dp[i][0][1] = dp[i - 1][0][1] + 1;
                dp[i][1][0] = dp[i - 1][1][0];
                dp[i][1][1] = dp[i - 1][1][0] + 1;
            }
        }
        return Math.max(Math.max(dp[n - 1][0][0], dp[n - 1][0][1]), Math.max(dp[n - 1][1][0], dp[n - 1][1][1]));
    }
}

###C#

public class Solution {
    public int MaximumLength(int[] nums) {
        int n = nums.Length;
        int[][][] dp = new int[n][][];
        for (int i = 0; i < n; i++) {
            dp[i] = new int[2][];
            for (int j = 0; j < 2; j++) {
                dp[i][j] = new int[2];
            }
        }
        dp[0][0][nums[0] % 2] = 1;
        dp[0][1][nums[0] % 2] = 1;
        for (int i = 1; i < n; i++) {
            if (nums[i] % 2 == 0) {
                dp[i][0][0] = dp[i - 1][0][0] + 1;
                dp[i][0][1] = dp[i - 1][0][1];
                dp[i][1][0] = dp[i - 1][1][1] + 1;
                dp[i][1][1] = dp[i - 1][1][1];
            } else {
                dp[i][0][0] = dp[i - 1][0][0];
                dp[i][0][1] = dp[i - 1][0][1] + 1;
                dp[i][1][0] = dp[i - 1][1][0];
                dp[i][1][1] = dp[i - 1][1][0] + 1;
            }
        }
        return Math.Max(Math.Max(dp[n - 1][0][0], dp[n - 1][0][1]), Math.Max(dp[n - 1][1][0], dp[n - 1][1][1]));
    }
}

下面的代码为优化空间的实现。

###Java

class Solution {
    public int maximumLength(int[] nums) {
        int n = nums.length;
        int[][] dp = new int[2][2];
        dp[0][nums[0] % 2] = 1;
        dp[1][nums[0] % 2] = 1;
        for (int i = 1; i < n; i++) {
            if (nums[i] % 2 == 0) {
                dp[0][0]++;
                dp[1][0] = dp[1][1] + 1;
            } else {
                dp[0][1]++;
                dp[1][1] = dp[1][0] + 1;
            }
        }
        return Math.max(Math.max(dp[0][0], dp[0][1]), Math.max(dp[1][0], dp[1][1]));
    }
}

###C#

public class Solution {
    public int MaximumLength(int[] nums) {
        int n = nums.Length;
        int[][] dp = new int[2][];
        for (int j = 0; j < 2; j++) {
            dp[j] = new int[2];
        }
        dp[0][nums[0] % 2] = 1;
        dp[1][nums[0] % 2] = 1;
        for (int i = 1; i < n; i++) {
            if (nums[i] % 2 == 0) {
                dp[0][0]++;
                dp[1][0] = dp[1][1] + 1;
            } else {
                dp[0][1]++;
                dp[1][1] = dp[1][0] + 1;
            }
        }
        return Math.Max(Math.Max(dp[0][0], dp[0][1]), Math.Max(dp[1][0], dp[1][1]));
    }
}

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是字符串 $s$ 的长度。状态数是 $O(n)$,每个状态的计算时间是 $O(1)$,因此时间复杂度是 $O(n)$。

  • 空间复杂度:$O(n)$ 或 $O(1)$,其中 $n$ 是字符串 $s$ 的长度。空间复杂度取决于实现方式,不优化空间的实现的空间复杂度是 $O(n)$,优化空间的实现的空间复杂度是 $O(1)$。

解法二

思路和算法

考虑更一般的情况,子序列 $\textit{sub}$ 的任意相邻两项之和除以 $k$ 的余数相等。这道题为 $k = 2$ 的特例。

长度为 $x$ 的有效子序列 $\textit{sub}$ 满足对于任意 $1 \le i \le x - 2$ 都有 $(\textit{sub}[i - 1] + \textit{sub}[i]) \bmod k = (\textit{sub}[i] + \textit{sub}[i + 1]) \bmod k$,等价于 $(\textit{sub}[i - 1] + \textit{sub}[i] - \textit{sub}[i] - \textit{sub}[i + 1]) \bmod k = 0$,整理得 $(\textit{sub}[i - 1] - \textit{sub}[i + 1]) \bmod k = 0$,即 $\textit{sub}[i - 1] \bmod k = \textit{sub}[i + 1] \bmod k$。因此子序列 $\textit{sub}$ 满足子序列中的下标相差 $2$ 的两项除以 $k$ 的余数相等,进一步可以得到子序列 $\textit{sub}$ 满足所有偶数下标项除以 $k$ 的余数相等且所有奇数下标项除以 $k$ 的余数相等。

为了计算最长有效子序列的长度,对于所有满足 $0 \le i, j < k$ 的整数 $i$ 和 $j$ 需要计算最后两项除以 $k$ 的余数分别是 $i$ 和 $j$ 的子序列的最大长度。

创建 $k \times k$ 的二维数组 $\textit{dp}$,其中 $\textit{dp}[i][j]$ 表示最后两项除以 $k$ 的余数分别是 $i$ 和 $j$ 的子序列的最大长度。

动态规划的边界情况是:如果当前没有最后两项除以 $k$ 的余数分别是 $i$ 和 $j$ 的子序列,则 $\textit{dp}[i][j] = 0$。初始时,$\textit{dp}$ 中的所有状态值都是 $0$。

遍历数组 $\textit{nums}$,对于遍历到的每个元素 $\textit{num}$,执行如下操作。

  1. 计算当前余数 $\textit{curr} = \textit{num} \bmod k$。

  2. 遍历 $0 \le \textit{prev} < k$ 的每个余数 $\textit{prev}$,为了得到最后两项除以 $k$ 的余数分别是 $\textit{prev}$ 和 $\textit{curr}$ 的最长有效子序列,需要首先得到最后两项除以 $k$ 的余数分别是 $\textit{curr}$ 和 $\textit{prev}$ 的最长有效子序列,然后在末尾添加 $\textit{num}$ 得到更长的有效子序列,因此将 $\textit{dp}[\textit{prev}][\textit{curr}]$ 的值更新为 $\textit{dp}[\textit{curr}][\textit{prev}] + 1$,然后使用 $\textit{dp}[\textit{prev}][\textit{curr}]$ 更新最长有效子序列的长度。

上述计算过程即为动态规划的状态转移方程。

遍历结束之后,即可得到最长有效子序列的长度。

代码

###Java

class Solution {
    public int maximumLength(int[] nums) {
        return maximumLength(nums, 2);
    }

    public int maximumLength(int[] nums, int k) {
        int maxLength = 0;
        int[][] dp = new int[k][k];
        for (int num : nums) {
            int curr = num % k;
            for (int prev = 0; prev < k; prev++) {
                dp[prev][curr] = dp[curr][prev] + 1;
                maxLength = Math.max(maxLength, dp[prev][curr]);
            }
        }
        return maxLength;
    }
}

###C#

public class Solution {
    public int MaximumLength(int[] nums) {
        return MaximumLength(nums, 2);
    }

    public int MaximumLength(int[] nums, int k) {
        int maxLength = 0;
        int[][] dp = new int[k][];
        for (int i = 0; i < k; i++) {
            dp[i] = new int[k];
        }
        foreach (int num in nums) {
            int curr = num % k;
            for (int prev = 0; prev < k; prev++) {
                dp[prev][curr] = dp[curr][prev] + 1;
                maxLength = Math.Max(maxLength, dp[prev][curr]);
            }
        }
        return maxLength;
    }
}

复杂度分析

  • 时间复杂度:$O(k^2 + nk)$,其中 $k$ 是给定的整数,这道题中 $k = 2$,$n$ 是数组 $\textit{nums}$ 的长度。创建大小为 $k \times k$ 的二维数组 $\textit{dp}$ 的时间是 $O(k^2)$,遍历数组 $\textit{nums}$ 时对于每个元素的计算时间是 $O(k)$,因此时间复杂度是 $O(k^2 + nk)$。

  • 空间复杂度:$O(k^2)$,其中 $k$ 是给定的整数,这道题中 $k = 2$。需要创建大小为 $k \times k$ 的二维数组。

[Python3/Java/C++/Go/TypeScript] 一题一解:模拟(清晰题解)

方法一:模拟

我们首先判断字符串的长度是否小于 3,如果是则返回 false

然后我们遍历字符串,判断每个字符是否是字母或数字,如果不是则返回 false。否则我们判断该字符是否是元音字母,如果是则将 has_vowel 置为 true,否则将 has_consonant 置为 true

最后,如果 has_vowelhas_consonant 都为 true,则返回 true,否则返回 false

###python

class Solution:
    def isValid(self, word: str) -> bool:
        if len(word) < 3:
            return False
        has_vowel = has_consonant = False
        vs = set("aeiouAEIOU")
        for c in word:
            if not c.isalnum():
                return False
            if c.isalpha():
                if c in vs:
                    has_vowel = True
                else:
                    has_consonant = True
        return has_vowel and has_consonant

###java

class Solution {
    public boolean isValid(String word) {
        if (word.length() < 3) {
            return false;
        }
        boolean hasVowel = false, hasConsonant = false;
        boolean[] vs = new boolean[26];
        for (char c : "aeiou".toCharArray()) {
            vs[c - 'a'] = true;
        }
        for (char c : word.toCharArray()) {
            if (Character.isAlphabetic(c)) {
                if (vs[Character.toLowerCase(c) - 'a']) {
                    hasVowel = true;
                } else {
                    hasConsonant = true;
                }
            } else if (!Character.isDigit(c)) {
                return false;
            }
        }
        return hasVowel && hasConsonant;
    }
}

###cpp

class Solution {
public:
    bool isValid(string word) {
        if (word.size() < 3) {
            return false;
        }
        bool has_vowel = false, has_consonant = false;
        bool vs[26]{};
        string vowels = "aeiou";
        for (char c : vowels) {
            vs[c - 'a'] = true;
        }
        for (char c : word) {
            if (isalpha(c)) {
                if (vs[tolower(c) - 'a']) {
                    has_vowel = true;
                } else {
                    has_consonant = true;
                }
            } else if (!isdigit(c)) {
                return false;
            }
        }
        return has_vowel && has_consonant;
    }
};

###go

func isValid(word string) bool {
if len(word) < 3 {
return false
}
hasVowel := false
hasConsonant := false
vs := make([]bool, 26)
for _, c := range "aeiou" {
vs[c-'a'] = true
}
for _, c := range word {
if unicode.IsLetter(c) {
if vs[unicode.ToLower(c)-'a'] {
hasVowel = true
} else {
hasConsonant = true
}
} else if !unicode.IsDigit(c) {
return false
}
}
return hasVowel && hasConsonant
}

###ts

function isValid(word: string): boolean {
    if (word.length < 3) {
        return false;
    }
    let hasVowel: boolean = false;
    let hasConsonant: boolean = false;
    const vowels: Set<string> = new Set(['a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U']);
    for (const c of word) {
        if (!c.match(/[a-zA-Z0-9]/)) {
            return false;
        }
        if (/[a-zA-Z]/.test(c)) {
            if (vowels.has(c)) {
                hasVowel = true;
            } else {
                hasConsonant = true;
            }
        }
    }
    return hasVowel && hasConsonant;
}

###rust

impl Solution {
    pub fn is_valid(word: String) -> bool {
        if word.len() < 3 {
            return false;
        }

        let mut has_vowel = false;
        let mut has_consonant = false;
        let vowels = ['a', 'e', 'i', 'o', 'u'];

        for c in word.chars() {
            if !c.is_alphanumeric() {
                return false;
            }
            if c.is_alphabetic() {
                let lower_c = c.to_ascii_lowercase();
                if vowels.contains(&lower_c) {
                    has_vowel = true;
                } else {
                    has_consonant = true;
                }
            }
        }

        has_vowel && has_consonant
    }
}

###cs

public class Solution {
    public bool IsValid(string word) {
        if (word.Length < 3) {
            return false;
        }

        bool hasVowel = false, hasConsonant = false;
        bool[] vs = new bool[26];
        foreach (char c in "aeiou") {
            vs[c - 'a'] = true;
        }

        foreach (char c in word) {
            if (char.IsLetter(c)) {
                char lower = char.ToLower(c);
                if (vs[lower - 'a']) {
                    hasVowel = true;
                } else {
                    hasConsonant = true;
                }
            } else if (!char.IsDigit(c)) {
                return false;
            }
        }

        return hasVowel && hasConsonant;
    }
}

时间复杂度 $O(n)$,空间复杂度 $O(1)$。其中 $n$ 为字符串的长度。


有任何问题,欢迎评论区交流,欢迎评论区提供其它解题思路(代码),也可以点个赞支持一下作者哈😄~

有效单词

方法一:一次遍历

思路与算法

首先,我们可以判断给定的单词长度是否大于等于 $3$,其次我们需要通过一次遍历来判断是否包含元音字母、辅音字母以及除去数字和大小写字母以外的其他字母。

代码

###C++

class Solution {
public:
    bool isValid(string word) {
        if (word.size() < 3) {
            return false;
        }
        bool has_vowel = false;
        bool has_consonant = false;
        for (auto c : word) {
            if (isalpha(c)) {
                c = tolower(c);
                if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
                    has_vowel = true;
                } else {
                    has_consonant = true;
                }
            } else if (!isdigit(c)) {
                return false;
            }
        }
        return has_vowel && has_consonant;
    }
};

###Python

class Solution:
    def isValid(self, word: str) -> bool:
        if len(word) < 3:
            return False

        has_vowel = False
        has_consonant = False

        for c in word:
            if c.isalpha():
                if c.lower() in 'aeiou':
                    has_vowel = True
                else:
                    has_consonant = True
            elif not c.isdigit():
                return False

        return has_vowel and has_consonant

###Rust

impl Solution {
    pub fn is_valid(word: String) -> bool {
        if word.len() < 3 {
            return false;
        }

        let mut has_vowel = false;
        let mut has_consonant = false;

        for c in word.chars() {
            if c.is_ascii_alphabetic() {
                let lc = c.to_ascii_lowercase();
                if "aeiou".contains(lc) {
                    has_vowel = true;
                } else {
                    has_consonant = true;
                }
            } else if !c.is_ascii_digit() {
                return false;
            }
        }

        has_vowel && has_consonant
    }
}

###Java

class Solution {
    public boolean isValid(String word) {
        if (word.length() < 3) {
            return false;
        }
        boolean hasVowel = false;
        boolean hasConsonant = false;
        for (char c : word.toCharArray()) {
            if (Character.isLetter(c)) {
                char ch = Character.toLowerCase(c);
                if (ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u') {
                    hasVowel = true;
                } else {
                    hasConsonant = true;
                }
            } else if (!Character.isDigit(c)) {
                return false;
            }
        }
        return hasVowel && hasConsonant;
    }
}

###C#

public class Solution {
    public bool IsValid(string word) {
        if (word.Length < 3) {
            return false;
        }
        bool hasVowel = false;
        bool hasConsonant = false;
        foreach (char c in word) {
            if (char.IsLetter(c)) {
                char ch = char.ToLower(c);
                if (ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u') {
                    hasVowel = true;
                } else {
                    hasConsonant = true;
                }
            } else if (!char.IsDigit(c)) {
                return false;
            }
        }
        return hasVowel && hasConsonant;
    }
}

###Go

func isValid(word string) bool {
    if len(word) < 3 {
        return false
    }
    hasVowel := false
    hasConsonant := false
    for _, c := range word {
        if unicode.IsLetter(c) {
            ch := unicode.ToLower(c)
            if ch == 'a' || ch == 'e' || ch == 'i' || ch == 'o' || ch == 'u' {
                hasVowel = true
            } else {
                hasConsonant = true
            }
        } else if !unicode.IsDigit(c) {
            return false
        }
    }
    return hasVowel && hasConsonant
}

###C

bool isValid(char* word) {
    int len = strlen(word);
    if (len < 3) {
        return false;
    }
    bool has_vowel = false;
    bool has_consonant = false;
    for (int i = 0; i < len; i++) {
        char c = word[i];
        if (isalpha(c)) {
            c = tolower(c);
            if (c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u') {
                has_vowel = true;
            } else {
                has_consonant = true;
            }
        } else if (!isdigit(c)) {
            return false;
        }
    }
    return has_vowel && has_consonant;
}

###JavaScript

var isValid = function(word) {
    if (word.length < 3) {
        return false;
    }
    let hasVowel = false;
    let hasConsonant = false;
    for (const c of word) {
        if (/[a-zA-Z]/.test(c)) {
            const ch = c.toLowerCase();
            if (ch === 'a' || ch === 'e' || ch === 'i' || ch === 'o' || ch === 'u') {
                hasVowel = true;
            } else {
                hasConsonant = true;
            }
        } else if (!/\d/.test(c)) {
            return false;
        }
    }
    return hasVowel && hasConsonant;
};

###TypeScript

function isValid(word: string): boolean {
    if (word.length < 3) {
        return false;
    }
    let hasVowel = false;
    let hasConsonant = false;
    for (const c of word) {
        if (/[a-zA-Z]/.test(c)) {
            const ch = c.toLowerCase();
            if (ch === 'a' || ch === 'e' || ch === 'i' || ch === 'o' || ch === 'u') {
                hasVowel = true;
            } else {
                hasConsonant = true;
            }
        } else if (!/\d/.test(c)) {
            return false;
        }
    }
    return hasVowel && hasConsonant;
};

复杂度分析

  • 时间复杂度:$O(n)$,其中 $n$ 是 $\textit{word}$ 的长度。由于只进行了一次遍历,因此时间复杂度为 $O(n)$。

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

❌