2026-08-27:最短唯一子数组。用go语言,给定一个整数数组,我们需要找到所有可能的连续非空片段中,那些在数组里只出现一次的片段。所谓“出现一次”,是指不存在另一个片段,长度相同且每个对应位置的数字都完全一样。我们的目标是找出所有这样的独特片段里,长度最短的那个,并返回这个最小长度值。
1 <= nums.length <= 100000。
1 <= nums[i] <= 100000。
输入: nums = [3,3,3]。
输出: 3。
解释:
长度为 1 的子数组:[3] → 出现 3 次
长度为 2 的子数组:[3, 3] → 出现 2 次
长度为 3 的子数组:[3, 3, 3] → 出现 1 次
子数组 [3, 3, 3] 是唯一的,因此最小唯一子数组的长度为 3。
题目来自力扣3934。
算法分步骤描述(基于后缀数组 + LCP) 问题核心
给定整数数组nums,需要找出所有仅出现一次的连续子数组(即不存在另一个完全相同的子数组),并返回其中最短长度。
等价于:对每个后缀nums[i:],其所有前缀中,若某个前缀在其他后缀中不再出现,则它是一个唯一子数组;我们要找所有后缀中符合条件的最短前缀长度。
步骤 1:将整数数组转化为字节序列(用于后缀数组构造)
• 题目中
nums[i] <= 1e5,每个整数可以用 3 个字节完整表示(位移操作)。• 将每个整数拆成 3 个字节(高位到低位),拼接成一个大的字节数组
tmp。• 这样做的目的是借用 Go 标准库
suffixarray直接处理字节切片,避免手动实现整数后缀数组。
(注:由于每个整数固定 3 字节,整数数组的后缀与字节序列中偏移为 3 的倍数的后缀一一对应。)
• 调用
suffixarray.New(tmp)得到后缀数组(内部为sa,类型[]int32),它记录了字节序列中所有后缀的字典序排名。• 由于整数后缀只对应偏移为3 的倍数的起始位置,我们遍历
sa,只保留p % 3 == 0的位置,并将坐标除以 3 得到原整数数组的下标。• 最终得到整数数组
nums的后缀数组sa(长度 n),sa[i]表示字典序第 i 小的后缀在原数组中的起始索引(0-based)。
rank•
rank[p]表示后缀nums[p:]在字典序中的排名(即sa[rank[p]] == p)。• 遍历
sa,对每个索引 i,令rank[ sa[i] ] = i。
height(LCP 数组)•
height[0] = 0(哨兵)。• 对于 i > 0,
height[i]= 后缀nums[ sa[i] : ]与nums[ sa[i-1] : ]的最长公共前缀长度。• 利用Kasai 算法线性计算:
• 从 i = 0 到 n-1,令 h = 当前已经匹配的长度(初始 0)。
• 若
rank[i] > 0,则与排名前一位的后缀比较,不断扩展公共前缀长度 h(同时保证不越界)。• 记录
height[ rank[i] ] = h,然后若 h>0,则 h--(因为下一次 i+1 时,前缀长度至少为 h-1)。
• 对于后缀
nums[ sa[i] : ],它与左右相邻后缀(即排名 i-1 和 i+1)的 LCP 最大值maxLCP决定了:
任何长度 ≤maxLCP的前缀都会在相邻后缀中出现,因此不唯一;
长度 ≥maxLCP + 1的前缀才可能唯一。• 因此,该后缀能贡献的最短唯一子数组长度为:
• 如果 i 不是最后一个(即 i < n-1),则考虑左右两边:
uniqueLen = max(height[i], height[i+1]) + 1。• 如果 i 是最后一个(i == n-1),则只有左边:
uniqueLen = height[i] + 1。
• 同时,
uniqueLen不能超过该后缀自身的长度(即n - sa[i]),否则子数组超出数组范围,不合理。• 取所有合法
uniqueLen的最小值,即为答案。
• 初始
ans = n(最大可能长度)。• 遍历所有后缀,更新
ans = min(ans, uniqueLen)。• 最终返回
ans。
• 后缀数组:所有后缀为
[3,3,3],[3,3],[3],字典序相同(因为元素全等),排序后可能为[0,1,2]或[2,1,0],但实际顺序任意(只要排名稳定)。• rank 数组:每个后缀排名相邻。
• height 数组:任意相邻后缀的 LCP 分别为 2 和 1(取决于排序),但最大值计算后可得:
• 对后缀
[3,3,3],与左右 LCP 最大值 = 2,则 uniqueLen = 3,合法。• 其他后缀的 uniqueLen 也会是 3(因为长度限制),最终 ans = 3。
•时间复杂度:
• 构造后缀数组:
suffixarray.New内部实现基于DC3 算法(线性),但理论上通常视为O(n),不过标准库可能采用快速排序(O(n log n))。严格来说,对于长度 n ≤ 1e5,可认为是O(n log n)。• 构建 rank 和 height:均 O(n)。
• 遍历求答案:O(n)。
• 总体O(n log n),且常数较小。
•额外空间复杂度:
• 字节数组 tmp:O(n)。
• 后缀数组 sa:O(n)。
• rank 和 height 数组:O(n)。
• 其他辅助变量 O(1)。
• 总共O(n)。
该算法利用后缀数组 + LCP 快速判断前缀重复性,将“唯一子数组”问题转化为每个后缀的最短唯一前缀问题,从而在线性扫描中得到答案。空间开销为 O(n),时间开销为 O(n log n),能够处理 n = 1e5 的数据规模。
Go完整代码如下:
package main
import (
"fmt"
"index/suffixarray"
"unsafe"
)
func max(a, b int) int {
if a > b {
return a
}
return b
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func smallestUniqueSubarray(nums []int) int {
n := len(nums)
// 将每个整数拆成 3 个字节,用于构造后缀数组
tmp := make([]byte, 0, n*3)
for _, x := range nums {
tmp = append(tmp, byte(x>>16), byte(x>>8), byte(x))
}
// 利用 unsafe 获取 suffixarray 内部的 sa 切片
type _tp struct {
_ []byte
sa []int32
}
_sa := (*_tp)(unsafe.Pointer(suffixarray.New(tmp))).sa
// 只保留偏移为 3 的倍数的位置,对应原数组的整数后缀
sa := make([]int32, 0, n)
for _, p := range _sa {
if p%3 == 0 {
sa = append(sa, p/3)
}
}
// 后缀名次数组 rank
rank := make([]int, n)
for i, p := range sa {
rank[p] = i
}
// 高度数组 height(LCP 数组)
height := make([]int, n)
h := 0
for i, rk := range rank {
if h > 0 {
h--
}
if rk > 0 {
for j := int(sa[rk-1]); i+h < n && j+h < n && nums[i+h] == nums[j+h]; h++ {
}
}
height[rk] = h
}
ans := n
for i, h := range height {
// 该后缀与左右相邻后缀的 LCP 最大值 +1 即为最小唯一前缀长度
uniqueLength := h + 1
if i < n-1 {
uniqueLength = max(h, height[i+1]) + 1
}
if uniqueLength <= n-int(sa[i]) {
ans = min(ans, uniqueLength)
}
}
return ans
}func main() {
nums := []int{3, 3, 3}
result := smallestUniqueSubarray(nums)
fmt.Println(result)
}
Python完整代码如下:
# -*-coding:utf-8-*-def build_suffix_array(nums):
"""构建整数数组的后缀数组(倍增算法)"""
n = len(nums)
if n == 1:
return [0]
# 初始排名:直接用数值(但需注意数值可能较大,排序时依然正确)
rank = list(nums)
sa = list(range(n))
k = 1
tmp = [0] * n
while True:
# 按 (rank[i], rank[i+k] if i+k else -1 ) 排序
sa.sort(key=lambda i: (rank[i], rank[i + k] if i + k < n else -1 ))
tmp[sa[ 0 ]] = 0
for i in range ( 1 , n):
prev, cur = sa[i - 1 ], sa[i]
prev_key = (rank[prev], rank[prev + k] if prev + k < n else -1 )
cur_key = (rank[cur], rank[cur + k] if cur + k < n else -1 )
tmp[cur] = tmp[prev] + ( 1 if cur_key != prev_key else 0 )
rank, tmp = tmp, rank # 交换,tmp 变为旧 rank(后续会被覆盖)
if rank[sa[ -1 ]] == n - 1 : # 所有排名都不同
break
k <<= 1
return sa
def build_lcp(nums, sa):
"" "计算 LCP 数组(height),height[i] = LCP(sa[i], sa[i-1]),height[0]=0" ""
n = len (nums)
rank = [ 0 ] * n
for i, p in enumerate(sa):
rank[p] = i
height = [ 0 ] * n
h = 0
for i in range (n):
if rank[i] > 0 :
j = sa[rank[i] - 1 ]
while i + h < n and j + h < n and nums[i + h] == nums[j + h]:
h += 1
height[rank[i]] = h
if h > 0 :
h -= 1
return height
def smallest_unique_subarray(nums):
n = len (nums)
if n == 0 :
return 0
sa = build_suffix_array(nums)
height = build_lcp(nums, sa)
ans = n
for i in range (n):
# 当前后缀与左右相邻后缀的 LCP 最大值 + 1 即为最小唯一前缀长度
unique_len = height[i] + 1
if i < n - 1 :
unique_len = max(height[i], height[i + 1 ]) + 1
# 不能超过后缀自身的长度
if unique_len <= n - sa[i]:
ans = min(ans, unique_len)
return ans
if __name__ == "__main__" :
nums = [ 3 , 3 , 3 ]
result = smallest_unique_subarray(nums)
print (result)
C++完整代码如下:
using namespace std;
// 构建后缀数组 sa,sa[i] 表示第 i 小的后缀的起始下标
vector buildSuffixArray(const vector& nums) {
int n = nums.size();
vector sa(n), rank(n), tmp(n);
// 初始排名:按第一个元素
for (int i = 0; i < n; i++) {
sa[i] = i;
rank[i] = nums[i];
}
// 倍增排序
for (int k = 1; k < n; k <<= 1) {
auto cmp = [&](int i, int j) {
if (rank[i] != rank[j]) return rank[i] < rank[j];
int ri = (i + k < n) ? rank[i + k] : -1;
int rj = (j + k < n) ? rank[j + k] : -1;
return ri < rj;
};
sort(sa.begin(), sa.end(), cmp);
tmp[sa[0]] = 0;
for (int i = 1; i < n; i++) {
tmp[sa[i]] = tmp[sa[i - 1]] + (cmp(sa[i - 1], sa[i]) ? 1 : 0);
}
rank = tmp;
if (rank[sa[n - 1]] == n - 1) break; // 全部排名不同,提前结束
}
return sa;
}
// 计算 height 数组,height[i] = LCP(sa[i], sa[i-1]),height[0] = 0
vector buildHeight(const vector& nums, const vector& sa) {
int n = nums.size();
vector rank(n);
for (int i = 0; i < n; i++) rank[sa[i]] = i;
vector height(n, 0);
int h = 0;
for (int i = 0; i < n; i++) {
if (rank[i] > 0) {
int j = sa[rank[i] - 1];
while (i + h < n && j + h < n && nums[i + h] == nums[j + h]) h++;
height[rank[i]] = h;
if (h > 0) h--;
}
}
return height;
}
int smallestUniqueSubarray(const vector& nums) {
int n = nums.size();
if (n == 0) return 0; // 根据题意不会出现
vector sa = buildSuffixArray(nums);
vector height = buildHeight(nums, sa);
int ans = n;
for (int i = 0; i < n; i++) {
// 当前后缀与左右相邻后缀的 LCP 最大值 +1 即为最小唯一前缀长度
int uniqueLen = height[i] + 1;
if (i < n - 1) {
uniqueLen = max(height[i], height[i + 1]) + 1;
}
// 不能超过后缀自身长度
if (uniqueLen <= n - sa[i]) {
ans = min(ans, uniqueLen);
}
}
return ans;
}int main() {
vector nums = {3, 3, 3};
int result = smallestUniqueSubarray(nums);
cout << result << endl;
return 0;
}
我们相信人工智能为普通人提供了一种“增强工具”,并致力于分享全方位的AI知识。在这里,您可以找到最新的AI科普文章、工具评测、提升效率的秘籍以及行业洞察。 欢迎关注“福大大架构师每日一题”,发消息可获得面试资料,让AI助力您的未来发展。
热门跟贴