DAY19: LeetCode 28 找出字符串中第一个匹配项的下标

LeetCode 28 找出字符串中第一个匹配项的下标

  • [LeetCode 28 找出字符串中第一个匹配项的下标](#LeetCode 28 找出字符串中第一个匹配项的下标)
  • [一、前置知识:`for ... else`](#一、前置知识:for ... else)
  • 二、我的第一反应:先找可能的起点,再逐个字符比较
  • [三、第一个问题:`break` 以后还是执行了 `return i`](#三、第一个问题:break 以后还是执行了 return i)
  • [四、第二个问题:`i` 不能一直走到 haystack 最后](#四、第二个问题:i 不能一直走到 haystack 最后)
  • [五、完整代码:`for ... else`](#五、完整代码:for ... else)
  • [六、使用 `matched` 的写法](#六、使用 matched 的写法)
  • [七、还能怎么优化 -- KMP](#七、还能怎么优化 -- KMP)
    • [KMP 是怎么利用前面信息的?](#KMP 是怎么利用前面信息的?)
    • [`lps` 是什么?](#lps 是什么?)
    • [先构造 lps](#先构造 lps)
      • [如果不相同,为什么不能直接把 `length` 变成 0?](#如果不相同,为什么不能直接把 length 变成 0?)
      • [如果已经退到 `length == 0` 还是不相同呢?](#如果已经退到 length == 0 还是不相同呢?)
    • [再使用 `lps` 进行匹配](#再使用 lps 进行匹配)
    • 完整代码

LeetCode 28 找出字符串中第一个匹配项的下标

LeetCode 28 找出字符串中第一个匹配项的下标

题目给出两个字符串 haystack 和 needle,要求在 haystack 中找到 needle 第一次完整出现的位置。如果找不到,就返回 -1。

例如:

text 复制代码
haystack = "sadbutsad"
needle   = "sad"

sad 分别出现在下标 0 和 6,题目要求的是第一个匹配位置,所以返回:

text 复制代码
0

所以, 这道题最直接的想法就是:把 haystack 中每一个可能的位置都当成起点,然后从这个位置开始,一个字符一个字符和 needle 比较。


一、前置知识:for ... else

在 Python 中,for 可以和 else 搭配:

python 复制代码
for x in nums:
    if 某个条件:
        break
else:
    # for 没有被 break 打断时执行

这里的 else 不是像往常一样和 if 配对,而是和 for 配对。

规则是:

text 复制代码
for 正常遍历结束 → 执行 else

for 中途遇到 break → 不执行 else

例如:

python 复制代码
for x in [1, 2, 3]:
    if x == 4:
        break
else:
    print("没有找到")

整个循环都没有触发 break,因此最后会执行 else。

所以, 这种写法特别适合这样的逻辑:

我要检查一系列条件,只要其中一个失败就 break;

如果一直没有失败,就说明全部检查通过。

这正好可以用在这道字符串匹配题里。


二、我的第一反应:先找可能的起点,再逐个字符比较

我的想法是先遍历 haystack:

python 复制代码
for i in range(len(haystack)):

这里的 i 表示:

当前尝试把 haystack[i] 当作 needle 的起点。

比如:

text 复制代码
haystack = "sadbutsad"
needle   = "sad"

如果:

text 复制代码
i = 0

首先比较:

text 复制代码
haystack[0] 和 needle[0]

如果第一个字符都不同,那当前位置肯定不可能匹配,可以直接:

python 复制代码
continue

去尝试下一个起点。

如果第一个字符相同,再继续比较后面的字符:

python 复制代码
for j in range(1, len(needle)):

此时可以设定:

text 复制代码
haystack[i + j]

表示从当前起点 i 往后移动 j 位,而:

text 复制代码
needle[j]

表示 needle 中对应位置的字符。

所以整体思路其实就是:

text 复制代码
枚举起点 i
→ 第一个字符不同,直接换下一个起点
→ 第一个字符相同,继续比较后面的字符
→ 中间有一个字符不同,当前起点失败
→ 所有字符都相同,返回当前 i

我最开始按照这个思路写出了:

python 复制代码
class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        for i in range(len(haystack)):
            if haystack[i] != needle[0]:
                continue
            else:
                for j in range(1, len(needle)):
                    if haystack[i+j] != needle[j]:
                        break
            return i

        return -1

思路本身没有问题,但是这里有两个容易忽略的漏洞。


三、第一个问题:break 以后还是执行了 return i

原来的代码中:

python 复制代码
for j in range(1, len(needle)):
    if haystack[i + j] != needle[j]:
        break

return i

我原本的想法是:

text 复制代码
发现字符不同
→ break
→ 当前 i 失败

但实际上,break 只会退出里面的 for j,退出以后程序会继续往下执行,于是马上遇到:

python 复制代码
return i

所以实际逻辑变成了:

text 复制代码
第一个字符相同
→ 开始比较后面的字符
→ 中间发现不同
→ break
→ 还是 return i

也就是说,代码没有真正区分:

text 复制代码
因为匹配失败而退出循环
和
因为所有字符都比较成功而结束循环

这里可以有两种解决方式。

方法一:使用 for ... else

也就是前面前置知识教的:

python 复制代码
for j in range(1, len(needle)):
    if haystack[i + j] != needle[j]:
        break
else:
    return i

如果中间有字符不同,就会触发 break,因此不会进入 else;

只有整个 for j 正常结束,也就是所有字符都匹配成功,才会执行:

python 复制代码
return i

所以这里的逻辑刚好是:

text 复制代码
出现不同
→ break
→ 当前 i 失败

从头到尾都没有 break
→ 所有字符匹配成功
→ return i

方法二:自己用变量记录

如果不用 for ... else,也可以自己用一个变量记录当前起点是否匹配成功:

python 复制代码
matched = True

for j in range(1, len(needle)):
    if haystack[i + j] != needle[j]:
        matched = False
        break

if matched:
    return i

这里的 matched 表示:

当前这个起点 i 到目前为止,有没有发现匹配失败。

可能会疑惑:为什么一开始就写:

python 复制代码
matched = True

明明后面的字符还没有比较,怎么就先认为匹配成功了?

其实, 这里不是在说"已经证明成功",而是在表示:

先假设当前起点可以匹配,只要后面发现任何一个反例,就把它改成 False。

因为前面已经检查过:

python 复制代码
if haystack[i] != needle[0]:
    continue

所以能走到 matched = True 这里时,至少第一个字符已经相同。

接下来只需要检查剩余字符。

例如:

text 复制代码
haystack = "sax..."
needle   = "sad"
i = 0

第一个字符:

text 复制代码
s == s

已经通过,所以先:

python 复制代码
matched = True

然后继续检查:

text 复制代码
a == a

仍然没有发现问题,matched 保持 True。

再检查:

text 复制代码
x != d

这时候终于发现当前起点不成立,于是:

python 复制代码
matched = False
break

离开内层循环后:

python 复制代码
if matched:

因为现在是 False,所以不会 return i,外层循环会继续尝试下一个起点。

反过来,如果整个内层循环都没有找到任何不同:

text 复制代码
第 1 位相同
第 2 位相同
第 3 位相同
......

那么就没有任何地方执行:

python 复制代码
matched = False

所以 matched 一直保持 True。

这时候才说明:

所有需要检查的字符都通过了,因此当前 i 确实是一个完整匹配的位置。

于是:

python 复制代码
if matched:
    return i

就是安全的。

可以把这个逻辑理解成:

text 复制代码
matched = True → 暂时没有发现问题

发现任何一个字符不同 → matched = False

整个检查结束后还是 True
→ 从头到尾都没有发现问题
→ 当前起点匹配成功

这里还有一个很重要的细节:matched = True 必须放在每一次新的 i 尝试里面。

也就是说:

python 复制代码
for i in ...:
    matched = True

每换一个新的起点,都要重新开始判断。

否则前一个起点失败后留下的 False,可能会影响下一个起点。

这个方法最核心的就是:

matched = True 是"目前还没有找到失败的证据";
只要出现一次不匹配,就立刻改成 False。


两种方法本质一样:

text 复制代码
for ... else → 用"有没有 break"判断是否匹配成功

matched → 自己使用布尔值保存匹配状态

四、第二个问题:i 不能一直走到 haystack 最后

原来写的是:

python 复制代码
for i in range(len(haystack)):

但后面还要访问:

python 复制代码
haystack[i + j]

所以如果 i 已经走得太靠后,而剩余字符数量又不足以放下整个 needle,就可能发生数组越界。

例如:

text 复制代码
haystack = "abcde"
needle   = "cde"

haystack 长度为 5,needle 长度为 3。

合法起点其实只有:

text 复制代码
i = 0
i = 1
i = 2

因为当:

text 复制代码
i = 2

检查的位置是:

text 复制代码
2、3、4

刚好还能放下长度为 3 的 needle。

但如果:

text 复制代码
i = 3

后面只剩下两个字符:

text 复制代码
d e

无论内容是什么,都已经不可能匹配长度为 3 的 needle,所以这些位置根本没有必要继续尝试。

因此最后一个合法起点应该是:

text 复制代码
len(haystack) - len(needle)

因为 range 的右边取不到,所以应该写成:

python 复制代码
for i in range(len(haystack) - len(needle) + 1):

这样既避免越界,也直接排除了那些长度上就不可能成功的位置。


五、完整代码:for ... else

结合上面两个问题,可以写成:

python 复制代码
class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        for i in range(len(haystack) - len(needle) + 1):

            if haystack[i] != needle[0]:
                continue

            for j in range(1, len(needle)):
                if haystack[i + j] != needle[j]:
                    break
            else:
                return i

        return -1

这里可以把两个循环的职责分开理解:

text 复制代码
i → 枚举 needle 可能出现的起点

j → 从这个起点开始,检查 needle 中的每一个字符

如果当前起点失败,就让外层的 i 继续寻找下一个位置;

如果整个 j 循环都没有失败,就直接返回当前 i。


六、使用 matched 的写法

如果觉得 for ... else 比较陌生,也可以使用 matched:

python 复制代码
class Solution:
    def strStr(self, haystack: str, needle: str) -> int:
        for i in range(len(haystack) - len(needle) + 1):

            if haystack[i] != needle[0]:
                continue

            matched = True

            for j in range(1, len(needle)):
                if haystack[i + j] != needle[j]:
                    matched = False
                    break

            if matched:
                return i

        return -1

七、还能怎么优化 -- KMP

目前这套方法本质上属于暴力字符串匹配。

假设:

text 复制代码
haystack 长度 = n
needle 长度   = m

最多需要尝试大约 n - m + 1 个起点,而每个起点最坏需要比较 m 个字符,所以最坏时间复杂度是:

text 复制代码
O(n × m)

例如:

text 复制代码
haystack = "aaaaaaaaaaaaaaaaab"
needle   = "aaaaab"

很多起点都会出现:

text 复制代码
前面连续匹配很多个 a
→ 到最后才发现不同
→ 换下一个起点
→ 又把前面的 a 重新比较一遍

这里会产生浪费:匹配失败以后,已经比较过的信息几乎全部丢掉了。

这和前面做其他优化题时的思路很像,可以继续问:

当前这次匹配虽然失败了,但是已经匹配成功的那一部分信息,能不能留给下一次使用?

这就会引出经典的 KMP(Knuth-Morris-Pratt)字符串匹配算法。

KMP 会提前分析 needle 本身的前缀和后缀关系,使得匹配失败时,不需要让 haystack 的比较位置完全退回重新开始,而是利用已经得到的信息继续匹配。

它可以把最坏时间复杂度从:

text 复制代码
O(n × m)

降低到:

text 复制代码
O(n + m)

不过对于这道简单题来说,先把现在这种"双循环逐字符匹配"的逻辑完全理解清楚已经足够。KMP 更适合作为后续字符串算法的扩展再单独学习。

这里就简单说说:

继续往下看,KMP 真正要解决的就是:

既然前面已经有一部分匹配成功了,那么失败以后,能不能不要全部推倒重来,而是利用这部分已经匹配成功的信息?

KMP 是怎么利用前面信息的?

例如:

text 复制代码
haystack = "ababab..."
needle   = "ababc"

从开头开始匹配:

text 复制代码
haystack: a b a b a b ...
needle:   a b a b c
          ✓ ✓ ✓ ✓ ×

前面的 "abab" 已经匹配成功,只是比较下一个字符时出现了:

text 复制代码
haystack:a
needle:  c

暴力方法会把当前起点判定为失败,然后换一个新的起点,再从 needle[0] 开始比较。这样做的问题是:前面已经成功比较过的 "abab" 信息全部被丢掉了。

KMP 会继续观察这段已经匹配成功的 "abab":

text 复制代码
abab
^^      前缀 "ab"

abab
  ^^    后缀 "ab"

可以发现,它的前缀 "ab" 和后缀 "ab" 是相同的。

这意味着,虽然完整匹配失败了,但是 haystack 刚刚匹配过的末尾两个字符已经确定是 "ab",而 needle 开头两个字符也正好是 "ab"。

所以这两个字符没有必要重新比较,可以直接把 needle 调整到一个新的位置继续:

text 复制代码
haystack: a b a b a ...
needle:       a b a b c
              ✓ ✓

这里最关键的一点是:

haystack 不需要回退,我们只需要知道 needle 应该退到哪里,才能继续利用前面已经匹配成功的信息。

为了提前知道"失败以后应该退到哪里",KMP 会根据 needle 构造一个 lps 数组。


lps 是什么?

LPS 全称是:

Longest Proper Prefix which is also Suffix

先解释一下这里的前缀和后缀。

对于一个字符串:

  • Prefix(前缀):从字符串最左边开始截取的一段内容。
  • Suffix(后缀):从字符串最右边开始截取的一段内容。

例如字符串 "abab" 的前缀有 "a"、"ab"、"aba",后缀有 "b"、"ab"、"bab"。

其中最长的相同前缀和后缀是 "ab",长度为 2。

这里的 Proper Prefix / Proper Suffix 表示不能把整个字符串自己算进去,否则任何字符串都可以直接拿自己和自己比较,就没有意义了。

所以:

lps[i] 表示:对于 needle[0:i+1] 这一段字符串,它最长的相同前缀和后缀有多长。

例如:

text 复制代码
needle = "ababc"

下标:     0  1  2  3  4
字符:     a  b  a  b  c
lps:      0  0  1  2  0

当 i = 2 时,当前这一段是 "aba",它最长的相同前后缀是 "a":

text 复制代码
前缀:"a"
后缀:"a"

所以:

text 复制代码
lps[2] = 1

当 i = 3 时,当前这一段是 "abab",最长的相同前后缀是 "ab":

text 复制代码
前缀:"ab"
后缀:"ab"

所以:

text 复制代码
lps[3] = 2

这个 2 就是在告诉我们:如果已经成功匹配 "abab",但下一个字符失败,那么前面还有长度为 2 的 "ab" 可以继续复用,不需要完全从头开始。


先构造 lps

直接看构造代码会比较绕,所以先看我们到底在做什么。

假设前一个位置已经算出了:

text 复制代码
lps[i - 1] = length

这意味着:

到 i - 1 为止,已经存在长度为 length 的相同前缀和后缀。

现在加入新的 needle[i],我们首先会想:

原来这组相同前后缀,能不能再往后延长一个字符?

这就是为什么要去比较:

python 复制代码
needle[i] == needle[length]

这里的 needle[i] 是当前新加入的字符,而 needle[length] 是前缀中下一个等待匹配的字符。

例如当前已经有:

text 复制代码
"abab"

最长相同前后缀:"ab"

length = 2

如果再加入 "a",变成:

text 复制代码
"ababa"

原来的 "ab" 想继续延长,就需要比较:

text 复制代码
当前新字符:needle[4] = "a"
前缀下一个字符:needle[2] = "a"

两者相同,所以原来的:

text 复制代码
"ab" == "ab"

可以延长成:

text 复制代码
"aba" == "aba"

因此:

python 复制代码
length += 1
lps[i] = length

此时 lps[4] = 3。

所以第一种情况就是:

python 复制代码
if needle[i] == needle[length]:
    length += 1
    lps[i] = length
    i += 1

意思就是:

当前字符能够接在已有的相同前后缀后面,所以相同前后缀长度增加 1。


如果不相同,为什么不能直接把 length 变成 0?

这里的"不相同",具体指的是:

python 复制代码
needle[i] != needle[length]

那这两个位置为什么要比较?

因为此时我们已经知道:前面存在一组长度为 length 的相同前缀和后缀,现在想看看它们能不能再同时延长一个字符。

举个例子:

text 复制代码
needle = "aabaaab"

假设现在准备计算 i = 5 这一位,也就是当前这一段:

text 复制代码
a a b a a a
0 1 2 3 4 5
          ↑
          i

在加入第 5 个字符之前,我们已经算出了:

text 复制代码
"aabaa"

它最长的相同前缀和后缀是:

text 复制代码
前缀:"aa"
后缀:"aa"

所以:

text 复制代码
length = 2

可以把当前状态看成:

text 复制代码
a a b a a
^^^^        前缀 "aa"
      ^^^^  后缀 "aa"

现在又来了一个新的字符:

text 复制代码
needle[5] = "a"

我们首先想:

原来的 "aa" == "aa",能不能继续延长成长度 3?

如果想让长度从 2 变成 3,前缀这一边接下来应该是什么字符?

原来的前缀已经用了:

text 复制代码
needle[0:2] = "aa"

所以下一个字符就是:

python 复制代码
needle[length]

也就是:

text 复制代码
needle[2] = "b"

而后缀这一边新加入的字符就是:

text 复制代码
needle[i] = needle[5] = "a"

于是我们比较:

text 复制代码
needle[i]      = "a"
needle[length] = "b"

发现:

text 复制代码
"a" != "b"

这就是代码里的:

python 复制代码
needle[i] != needle[length]

它表达的不是"整个字符串失败了",而只是:

原来长度为 2 的 "aa" 这组前后缀,不能继续延长成长度 3。

因为如果能延长,本来应该得到:

text 复制代码
前缀:"aab"
后缀:"aaa"

但:

text 复制代码
"aab" != "aaa"

所以长度 3 这条路失败了。


但为什么不能直接:

python 复制代码
length = 0

因为:

长度 2 的方案失败,不代表长度 1 的方案也失败。

我们原来用的是:

text 复制代码
"aa"

而 "aa" 自己还有一个更短的相同前缀和后缀:

text 复制代码
"a"

也就是说,可以退一步,不再尝试:

text 复制代码
长度 2:"aa"

而改成尝试:

text 复制代码
长度 1:"a"

这个"更短的可用长度"之前其实已经算过了:

text 复制代码
lps[1] = 1

所以代码写成:

python 复制代码
length = lps[length - 1]

原来:

text 复制代码
length = 2

变成:

text 复制代码
length = lps[1] = 1

这时候 i 不动,仍然是:

text 复制代码
i = 5
needle[i] = "a"

只是换成长度为 1 的方案重新试。

现在比较:

text 复制代码
needle[i]      = needle[5] = "a"
needle[length] = needle[1] = "a"

这次:

text 复制代码
"a" == "a"

成功了。

所以原来的长度 1 可以继续延长:

text 复制代码
length = 1 + 1 = 2

最终:

text 复制代码
lps[5] = 2

再看当前字符串:

text 复制代码
"aabaaa"

它确实有:

text 复制代码
前缀:"aa"
后缀:"aa"

所以答案正好是:

text 复制代码
lps[5] = 2

因此, 流程就是:

text 复制代码
原来已经有 length = 2 → 前后缀都是 "aa"

想继续延长 → 比较 needle[i] 和 needle[length]

"a" != "b" → 长度 2 的方案无法继续延长

但不是直接清零 → 因为 "aa" 自己还有更短的可用前后缀 "a"

length = lps[length - 1] → length 从 2 退到 1

还是拿同一个 needle[i] = "a" 再试

"a" == "a" → 长度 1 的方案可以延长

最终 length = 2 → lps[i] = 2

所以, 写代码为:

python 复制代码
elif length > 0:
    length = lps[length - 1]

当前最长的前后缀没办法继续延长,那就退到下一个更短、已经被证明有效的前后缀,再拿当前字符重新尝试。

而 i 为什么不增加,也就很好理解了:因为我们还没有算完 lps[i],只是换了一个候选长度,当前这个字符还得继续试。


如果已经退到 length == 0 还是不相同呢?

这时候说明已经没有任何更短的前后缀可以继续尝试了,因此:

python 复制代码
lps[i] = 0
i += 1

也就是:

当前这一段不存在相同的非空前缀和后缀,当前答案就是 0,然后继续计算下一格。

所以整个构造过程其实只有三个问题:

text 复制代码
1. 当前最长前后缀还能继续延长吗?
能
→ length += 1
→ lps[i] = length
→ i += 1


2. 不能延长,但还有更短的前后缀可以尝试吗?
有
→ length = lps[length - 1]
→ i 不动
→ 用当前字符重新尝试


3. 已经没有任何前后缀可以尝试了吗?
是
→ lps[i] = 0
→ i += 1

对应代码就是:

python 复制代码
lps = [0] * len(needle)

length = 0
i = 1

while i < len(needle):
    if needle[i] == needle[length]:
        length += 1
        lps[i] = length
        i += 1

    elif length > 0:
        length = lps[length - 1]

    else:
        lps[i] = 0
        i += 1

这里两个变量的职责就是:

  • i:当前正在计算哪一个 lps[i]
  • length:当前正在尝试的相同前后缀长度,同时也是前缀中下一个需要比较的位置

再使用 lps 进行匹配

lps 构造完成以后,就可以真正开始比较 haystack 和 needle。

python 复制代码
i = 0
j = 0

while i < len(haystack):
    if haystack[i] == needle[j]:
        i += 1
        j += 1

        if j == len(needle):
            return i - j

    else:
        if j > 0:
            j = lps[j - 1]
        else:
            i += 1

这里:

  • i:当前检查 haystack 的哪个字符
  • j:当前检查 needle 的哪个字符

如果:

python 复制代码
haystack[i] == needle[j]

说明当前字符匹配成功,两个指针一起向后移动:

python 复制代码
i += 1
j += 1

如果:

python 复制代码
j == len(needle)

说明整个 needle 已经匹配完成。

此时 i 已经走到了匹配内容的后一个位置,而 j 正好等于 needle 的长度,所以起点就是:

python 复制代码
i - j

如果字符不同,则分两种情况。

如果:

python 复制代码
j == 0

说明连 needle 的第一个字符都没有匹配成功,前面没有任何信息可以继续利用,所以只能:

python 复制代码
i += 1

继续检查 haystack 的下一个字符。

如果:

python 复制代码
j > 0

说明前面已经有一部分字符匹配成功,这时候 haystack 的 i 不需要回退,只调整:

python 复制代码
j = lps[j - 1]

意思就是:

根据刚才已经匹配成功的那一段,找到一个更短但仍然可以复用的前后缀,然后继续拿当前的 haystack[i] 比较。

所以 KMP 和暴力方法最大的区别就是:

暴力:

text 复制代码
匹配失败
→ 换一个起点
→ needle 从头重新比较
→ 前面已经匹配成功的信息基本全部丢掉

而 KMP 是:

text 复制代码
匹配失败
→ haystack 不回退
→ 根据 lps 调整 needle 的位置
→ 继续复用前面已经匹配成功的信息

完整代码

python 复制代码
class Solution:
    def strStr(self, haystack: str, needle: str) -> int:

        # 1. 构造 lps
        lps = [0] * len(needle)

        length = 0
        i = 1

        while i < len(needle):
            if needle[i] == needle[length]:
                length += 1
                lps[i] = length
                i += 1

            elif length > 0:
                length = lps[length - 1]

            else:
                lps[i] = 0
                i += 1

        # 2. KMP 匹配
        i = 0
        j = 0

        while i < len(haystack):
            if haystack[i] == needle[j]:
                i += 1
                j += 1

                if j == len(needle):
                    return i - j

            else:
                if j > 0:
                    j = lps[j - 1]
                else:
                    i += 1

        return -1

所以 KMP 最值得记住的并不是 lps 的代码,而是两个"复用":

构造 lps 时,如果最长前后缀失败,就利用之前算好的 lps 去寻找更短的前后缀;真正匹配时,如果 needle 匹配失败,又利用 lps 决定 j 应该退到哪里。

也就是说,KMP 从头到尾都在做同一件事:

已经算过的信息不要轻易丢掉,能复用就继续复用。


🚩 如果只是 Python 实际开发,不要求手写匹配算法,也可以直接使用:

python 复制代码
haystack.find(needle)

它本身就会返回第一次出现的位置,找不到则返回 -1。

但在算法题里,自己实现这一遍的意义主要还是理解:

text 复制代码
枚举起点
→ 逐字符验证
→ 失败后换起点
→ 成功后返回第一个匹配位置

以及进一步思考:

暴力算法中有哪些已经计算过的信息被重复丢弃了,而这些信息能不能被下一轮复用。

相关推荐
海宇AI1 小时前
零信任架构实战:基于海宇活体识别V步骤1构建自动化直播开播鉴权网关
运维·人工智能·架构·自动化
数商思语行1 小时前
Palantir Study 27|Global Branching:安全修改生产 Ontology
人工智能·ai·供应链·商业分析·ontology·本体·fde
不会就选b1 小时前
算法日常・每日刷题--<动态规划>6
数据结构·算法·leetcode
️公子1 小时前
决策模型开战:Jev / Decisions API / Strands Decider 的 ClosedSet、置信度闸门与混合 Agent
人工智能·大模型·软件工程·agent·决策模型
skywalk81631 小时前
光明(Light)中文编程语言 · 宣传文档 版本:**v0.4.0-rc2*更新日期:2026-10-03
人工智能·编程·光明
释厄6231 小时前
AI犯错即异质论——正确认知=免疫与安全防控技术算法
大数据·算法·安全
漂流瓶jz1 小时前
UVA-12545 比特变换器 题解答案代码 算法竞赛入门经典第二版
数据结构·算法·题解·aoapc·算法竞赛入门经典·uva·12545
YOLO数据集集合1 小时前
光伏组件热斑与缺陷检测数据集 | 光伏组件 热斑检测 PID识别 红外检测 光伏巡检9148期
人工智能·yolo·目标检测·光伏·红外光伏·红外检测
hhzz1 小时前
【YOLO 入门到精通 08】推理预测完全指南:多数据源、流式推理与性能优化
人工智能·python·yolo·计算机视觉·性能优化