2025-09-12:删除元素后 K 个字符串的最长公共前缀。用go语言,给定一个字符串数组 words 和一个整数 k。对于数组中每个位置 i,先把下标为 i

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。

分步骤描述过程

  1. 问题理解

    • 给定一个字符串数组 words 和一个整数 k
    • 对于每个位置 i,移除 words[i] 后,从剩余字符串中任意选择 k 个(如果剩余字符串少于 k 个,则结果为0),求这 k 个字符串的最长公共前缀(LCP)的最大可能长度。
    • 返回一个数组 answer,其中 answer[i] 是移除第 i 个字符串后的结果。
  2. 关键观察

    • 由于需要任意选择 k 个字符串,最优解一定来自于具有最长公共前缀的 k 个字符串。
    • 排序后,字符串的公共前缀长度在相邻字符串之间较大,因此通常最长公共前缀的 k 个字符串在排序后的数组中连续出现(因为排序后相似字符串会聚集)。
    • 因此,问题转化为:先对字符串数组排序(按字典序),然后寻找长度为 k 的连续子数组,使得该子数组的首尾字符串的LCP最大(因为连续子数组的LCP由首尾决定)。
  3. 整体思路

    • 对原数组进行排序(但需要记录原始下标),以便找到具有最大LCP的连续k个字符串组。
    • 计算整个数组中,连续k个字符串的最大LCP(记为mx)和次大LCP(记为mx2),并记录最大LCP对应的起始位置(记为mxI)。
    • 对于每个原始位置i
      • 如果移除的字符串不在最大LCP组(即排序后连续k个字符串组)中,那么剩余字符串中仍然可以选出该组,因此结果仍然是mx
      • 如果移除的字符串在最大LCP组中,那么最大LCP组无法被完整选出,此时最优解可能变为次大LCP组(即mx2)。
  4. 详细步骤

    • 步骤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组(即排序后从mxImxI+k-1)中的每个字符串(注意这些字符串对应原始下标),将其答案改为mx2(因为如果移除了最大LCP组中的某个字符串,那么最大LCP组就无法被完整选出,此时最优解可能是次大LCP组)。
    • 步骤5:返回答案
  5. 例子分析(以输入为例)

    • 原始数组:["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。
    • 最大LCPmx=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],符合输出。
  6. 为什么正确?

    • 因为最优解一定来自某个连续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))
相关推荐
Cache技术分享2 小时前
186. Java 模式匹配 - Java 21 新特性:Record Pattern(记录模式匹配)
前端·javascript·后端
Python私教2 小时前
Django全栈班v1.01 Python简介与特点 20250910
后端·python·django
AAA修煤气灶刘哥2 小时前
从 Timer 到 XXL-Job,定时任务调度的 “进化史”,看完再也不怕漏跑任务~
java·后端·架构
zjjuejin2 小时前
Docker Swarm 完全指南:从原理到实战
后端·docker
shark_chili2 小时前
深入GPU核心:理解现代并行计算的硬件架构
后端
乘风破浪酱524362 小时前
MyBatis-Plus UserMpper接口示例
后端
无奈何杨3 小时前
风控系统的事中与事后一致性与闭环
前端·后端
这里有鱼汤3 小时前
为什么指数涨你却亏钱?80%的人忽略的市场宽度指标揭晓,我用Python实现了(附源码)
后端·python
ss2733 小时前
基于Springboot + vue实现的高校大学生竞赛项目管理系统
vue.js·spring boot·后端