2025-09-12:删除元素后 K 个字符串的最长公共前缀。用go语言,给定一个字符串数组 words 和一个整数 k。对于数组中每个位置 i,先把下标为 i 的元素去掉,然后在剩下的字符串里任意挑出 k 个不同的位置(若剩下的字符串少于 k 个,则答案为 0),计算这 k 个字符串从开头连续相同的最大长度。对所有可能的 k 元组取能够达到的最大值,作为移除第 i 个元素后的结果。
返回一个数组 answer,answer[i] 表示在去掉第 i 个字符串后,剩余字符串中任意选出 k 个所能达到的"共同起始子串"的最大长度。
1 <= k <= words.length <= 100000。
1 <= words[i].length <= 10000。
words[i] 由小写英文字母组成。
words[i].length 的总和小于等于 100000。
输入: words = ["jump","run","run","jump","run"], k = 2。
输出: [3,4,4,3,4]。
解释:
移除下标 0 处的元素 "jump" :
words 变为: ["run", "run", "jump", "run"]。 "run" 出现了 3 次。选择任意两个得到的最长公共前缀是 "run" (长度为 3)。
移除下标 1 处的元素 "run" :
words 变为: ["jump", "run", "jump", "run"]。 "jump" 出现了 2 次。选择这两个得到的最长公共前缀是 "jump" (长度为 4)。
移除下标 2 处的元素 "run" :
words 变为: ["jump", "run", "jump", "run"]。 "jump" 出现了 2 次。选择这两个得到的最长公共前缀是 "jump" (长度为 4)。
移除下标 3 处的元素 "jump" :
words 变为: ["jump", "run", "run", "run"]。 "run" 出现了 3 次。选择任意两个得到的最长公共前缀是 "run" (长度为 3)。
移除下标 4 处的元素 "run" :
words 变为: ["jump", "run", "run", "jump"]。 "jump" 出现了 2 次。选择这两个得到的最长公共前缀是 "jump" (长度为 4)。
题目来自力扣3485。
分步骤描述过程
-
问题理解:
- 给定一个字符串数组
words
和一个整数k
。 - 对于每个位置
i
,移除words[i]
后,从剩余字符串中任意选择k
个(如果剩余字符串少于k
个,则结果为0),求这k
个字符串的最长公共前缀(LCP)的最大可能长度。 - 返回一个数组
answer
,其中answer[i]
是移除第i
个字符串后的结果。
- 给定一个字符串数组
-
关键观察:
- 由于需要任意选择
k
个字符串,最优解一定来自于具有最长公共前缀的k
个字符串。 - 排序后,字符串的公共前缀长度在相邻字符串之间较大,因此通常最长公共前缀的
k
个字符串在排序后的数组中连续出现(因为排序后相似字符串会聚集)。 - 因此,问题转化为:先对字符串数组排序(按字典序),然后寻找长度为
k
的连续子数组,使得该子数组的首尾字符串的LCP最大(因为连续子数组的LCP由首尾决定)。
- 由于需要任意选择
-
整体思路:
- 对原数组进行排序(但需要记录原始下标),以便找到具有最大LCP的连续k个字符串组。
- 计算整个数组中,连续k个字符串的最大LCP(记为
mx
)和次大LCP(记为mx2
),并记录最大LCP对应的起始位置(记为mxI
)。 - 对于每个原始位置
i
:- 如果移除的字符串不在最大LCP组(即排序后连续k个字符串组)中,那么剩余字符串中仍然可以选出该组,因此结果仍然是
mx
。 - 如果移除的字符串在最大LCP组中,那么最大LCP组无法被完整选出,此时最优解可能变为次大LCP组(即
mx2
)。
- 如果移除的字符串不在最大LCP组(即排序后连续k个字符串组)中,那么剩余字符串中仍然可以选出该组,因此结果仍然是
-
详细步骤:
- 步骤1:处理边界 :如果
k >= n
(即移除一个后剩余少于k个),则所有结果都为0(但题目中k<=n
,且移除一个后剩余n-1
,所以只有当k > n-1
时才会出现0?实际上代码中判断if k>=n
时返回全0数组。但注意k<=n
,且移除一个后剩余n-1
,所以当k>n-1
时剩余不够k个?但题目说"若剩下的字符串少于k个,则答案为0"。但代码中k>=n
时返回全0?实际上k
最大为n
,移除一个后剩余n-1
,所以当k==n
时移除一个后剩余n-1 < k
,因此答案为0。所以代码中判断if k>=n
是正确的(因为k==n
时移除一个后就不够k个了)。 - 步骤2:创建索引数组并排序 :创建一个索引数组
idx
,然后根据words
中字符串的字典序对idx
排序(这样idx[i]
表示排序后第i个字符串在原数组中的位置)。 - 步骤3:寻找最大和次大LCP :
- 遍历排序后的数组,对于每个起始位置
i
(从0到n-k
),计算连续k个字符串的LCP(即计算排序后第i
个字符串和第i+k-1
个字符串的LCP)。 - 记录最大的LCP值(
mx
)和次大的LCP值(mx2
),同时记录最大LCP组的起始索引mxI
(即最大LCP对应的连续k个字符串的起始位置)。
- 遍历排序后的数组,对于每个起始位置
- 步骤4:构建答案数组 :
- 初始化答案数组
ans
,所有位置先设为mx
(即默认移除该字符串后,最大LCP组仍然可用)。 - 然后,对于最大LCP组(即排序后从
mxI
到mxI+k-1
)中的每个字符串(注意这些字符串对应原始下标),将其答案改为mx2
(因为如果移除了最大LCP组中的某个字符串,那么最大LCP组就无法被完整选出,此时最优解可能是次大LCP组)。
- 初始化答案数组
- 步骤5:返回答案。
- 步骤1:处理边界 :如果
-
例子分析(以输入为例):
- 原始数组:
["jump","run","run","jump","run"]
,k=2。 - 排序后:
["jump", "jump", "run", "run", "run"]
(注意两个"jump"和三个"run")。 - 计算连续k=2个字符串的LCP:
- 第一组(索引0和1):两个"jump"的LCP=4(完整长度)。
- 第二组(索引1和2):"jump"和"run"的LCP=0(没有公共前缀)。
- 第三组(索引2和3):两个"run"的LCP=3(完整长度)。
- 第四组(索引3和4):两个"run"的LCP=3。
- 最大LCP
mx
=4(来自第一组),次大LCPmx2
=3(来自第三、四组)。 - 最大LCP组对应的原始下标:排序后第一组(索引0和1)对应原始数组中的两个"jump"(原始下标0和3)。
- 因此:
- 对于原始下标0和3(即两个"jump"),移除它们后最大LCP组不可用,所以答案变为次大值3。
- 对于其他下标(1,2,4),移除后最大LCP组(两个"jump")仍然可用,所以答案为4。
- 最终答案数组为
[3,4,4,3,4]
,符合输出。
- 原始数组:
-
为什么正确?:
- 因为最优解一定来自某个连续k个字符串组(排序后),所以最大LCP组是最优的。
- 如果移除的字符串不在最大LCP组中,那么最大LCP组仍然可用,所以结果还是
mx
。 - 如果移除的字符串在最大LCP组中,那么最大LCP组无法使用,此时最优解就是次大LCP组(注意次大LCP组可能不止一个,但这里取全局次大值)?但这里假设次大LCP组不受移除影响?实际上次大LCP组可能包含被移除的字符串吗?有可能!但代码中没有检查这一点,这是一个缺陷?但题目数据规模大,且可能次大LCP组有多个,所以通常次大LCP组不包含被移除字符串?实际上如果最大LCP组和次大LCP组有重叠,那么移除一个字符串可能同时影响多个组。但代码中简单地将所有在最大LCP组中的字符串的答案设为次大值,可能不够精确?但在实际中往往可行(因为次大值通常来自另一个组,且该组可能未被影响)。严格来说,可能需要记录多个候选?但题目约束和代码实现选择了这种方法。
时间复杂度和额外空间复杂度
-
时间复杂度:
- 排序:O(n log n * L)?但注意字符串比较可能耗时,但题目中所有字符串总长度<=100000,所以排序的时间复杂度为O(n log n + 总长度)(因为比较两个字符串最多需要O(min(L1, L2)),但总长度有限制)。
- 计算LCP:遍历排序后的数组,计算每个连续k组的LCP(即计算首尾字符串的LCP)。每次计算LCP最多需要O(最长公共前缀长度),但总次数为O(n)(因为连续k组有n-k+1个)。注意所有字符串总长度<=100000,所以所有LCP计算的总开销不会超过总长度(因为每次计算LCP都是比较两个字符串的前缀,且这些前缀可能重叠?但最坏情况可能每次比较都较长?但总长度有限制,所以总时间可以接受)。
- 因此,总时间复杂度为O(n log n + 总长度)。
-
额外空间复杂度:
- 需要索引数组
idx
:O(n)。 - 答案数组:O(n)。
- 其他变量:常数。
- 因此,总额外空间复杂度为O(n)。
- 需要索引数组
注意:排序可能使用递归?但Go的slices.SortFunc使用非递归排序?空间复杂度为O(log n)(递归栈)?但通常忽略排序的栈空间,所以额外空间主要为O(n)。
总结:
- 时间复杂度:O(n log n + L)(其中L为所有字符串总长度)。
- 额外空间复杂度:O(n)。
Go完整代码如下:
go
package main
import (
"cmp"
"fmt"
"slices"
)
// 计算 s 和 t 的最长公共前缀(LCP)长度
func calcLCP(s, t string) int {
n := min(len(s), len(t))
for i := range n {
if s[i] != t[i] {
return i
}
}
return n
}
func longestCommonPrefix(words []string, k int) []int {
n := len(words)
if k >= n { // 移除一个字符串后,剩余字符串少于 k 个
return make([]int, n)
}
idx := make([]int, n)
for i := range idx {
idx[i] = i
}
slices.SortFunc(idx, func(i, j int) int { return cmp.Compare(words[i], words[j]) })
// 计算最大 LCP 长度和次大 LCP 长度,同时记录最大 LCP 来自哪里
mx, mx2, mxI := -1, -1, -1
for i := range n - k + 1 {
// 排序后,[i, i+k-1] 的 LCP 等于两端点的 LCP
lcp := calcLCP(words[idx[i]], words[idx[i+k-1]])
if lcp > mx {
mx, mx2, mxI = lcp, mx, i
} else if lcp > mx2 {
mx2 = lcp
}
}
ans := make([]int, n)
for i := range ans {
ans[i] = mx // 先初始化成最大 LCP 长度
}
// 移除下标在 idx[mxI:mxI+k] 中的字符串,会导致最大 LCP 变成次大 LCP
for _, i := range idx[mxI : mxI+k] {
ans[i] = mx2 // 改成次大 LCP 长度
}
return ans
}
func main() {
words := []string{"jump", "run", "run", "jump", "run"}
k := 2
result := longestCommonPrefix(words, k)
fmt.Println(result)
}

Python完整代码如下:
python
# -*-coding:utf-8-*-
def calc_lcp(s: str, t: str) -> int:
"""计算 s 和 t 的最长公共前缀长度"""
n = min(len(s), len(t))
for i in range(n):
if s[i] != t[i]:
return i
return n
def longest_common_prefix(words, k):
"""
words: List[str]
k: int
返回列表 ans,ans[i] 表示移除第 i 个元素后,从剩余字符串中任意选 k 个所能达到的最长公共前缀长度。
"""
n = len(words)
# 移除一个字符串后,剩余字符串为 n-1 个,如果 n-1 < k 即 k >= n,则对任一 i 结果为 0
if k >= n:
return [0] * n
# 按字典序对下标排序
idx = list(range(n))
idx.sort(key=lambda i: words[i])
mx = -1 # 最大 LCP
mx2 = -1 # 次大 LCP
mxI = -1 # 最大 LCP 对应的窗口起始位置(在排序后的下标数组中的起始索引)
# 统计每个窗口 [i, i+k-1] 的 LCP(等于两端的 LCP)
for i in range(0, n - k + 1):
lcp = calc_lcp(words[idx[i]], words[idx[i + k - 1]])
if lcp > mx:
mx, mx2, mxI = lcp, mx, i
elif lcp > mx2:
mx2 = lcp
# 初始化答案为最大 LCP
ans = [mx] * n
# 属于最大 LCP 窗口内的原始下标,其答案改为次大 LCP
for original_index in idx[mxI:mxI + k]:
ans[original_index] = mx2
return ans
# 示例
if __name__ == "__main__":
words = ["jump", "run", "run", "jump", "run"]
k = 2
print(longest_common_prefix(words, k))
