本次博客我是通过Notion软件写的,转md文件可能不太美观,大家可以去我的博客中查看:北天的 BLOG,持续更新中,另外这是我创建的编程学习小组频道,想一起学习的朋友可以一起!!!
沿路有一排共 N N N 个农场。
不幸的是农场并没有编号,这使得约翰难以分辨他在这条路上所处的位置。
然而,每个农场都沿路设有一个彩色的邮箱,所以约翰希望能够通过查看最近的几个邮箱的颜色来唯一确定他所在的位置。
每个邮箱的颜色用 A . . Z A..Z A..Z 之间的一个字母来指定,所以沿着道路的 N N N 个邮箱的序列可以用一个长为 N N N 的由字母 A . . Z A..Z A..Z 组成的字符串来表示。
某些邮箱可能会有相同的颜色。
约翰想要知道最小的 A . . Z A..Z A..Z 的值,使得他查看任意连续 K K K 个邮箱序列,他都可以唯一确定这一序列在道路上的位置。
例如,假设沿路的邮箱序列为 ABCDABC
。
约翰不能令 K = 3 K = 3 K=3,因为如果他看到了 ABC
,则沿路有两个这一连续颜色序列可能所在的位置。
最小可行的 K 的值为 K = 4 K = 4 K=4,因为如果他查看任意连续 4 个邮箱,那么可得到的连续颜色序列可以唯一确定他在道路上的位置。
输入格式
输入的第一行包含 N N N,第二行包含一个由 N N N 个字符组成的字符串,每个字符均在 A . . Z A..Z A..Z 之内。
输出格式
输出一行,包含一个整数,为可以解决农夫约翰的问题的最小 K K K 值。
数据范围
1 ≤ N ≤ 100 1≤ N ≤100 1≤N≤100
输入样例:
7
ABCDABC
输出样例:
4
1、暴力枚举法
对于暴力枚举法,我们可以使用两重循环,外层循环枚举K的长度,内层循环枚举所有的子串,并在后面的部分中查找该子串是否出现过。如果没有找到,则表示当前长度K可行,输出K并结束程序。如果内层循环结束后仍未找到合适的K,则需要继续外层循环进行下一轮枚举。
2、二分法
对于二分法,我们可以将判断一个长度K是否可行转化为判断以每个位置为起点的长度为K的子串是否互不相同。具体地,我们可以将所有以长度K的子串存入一个集合中,然后判断集合中的元素个数是否等于N-K+1。如果等于,则表示当前长度K可行,否则不可行。因为如果有重复的子串,那么集合中的元素个数会小于N-K+1,如果没有重复的子串,则集合中的元素个数会等于N-K+1。根据这个判断结果来缩小二分法的搜索范围,直到找到最小可行的K。
n = int(input()) # 输入农场数
s = input().strip() # 输入邮筒序列
res = n # 初始化最小连续颜色长度res为n
for k in range(1, n+1): # 外层循环枚举连续颜色长度k,从1到n
seen = set() # 定义一个集合seen,用于存储当前长度为k的所有子串
unique = True # 初始化unique为True
# 内层循环枚举字符串s中长度为k的所有子串
for i in range(n-k+1):
sub = s[i:i+k] # 取出从i开始长度为k的子串sub
if sub in seen: # 如果sub已经在seen中出现过了,说明有重复子串,此时unique为False
unique = False
break
else:
seen.add(sub) # 否则将sub加入seen集合中
if unique: # 如果unique为True,说明当前枚举的k值可以唯一确定任意连续k个邮箱序列在道路上的位置
res = k # 更新最小连续颜色长度res
break
print(res) # 输出最小的k值,即可以解决农夫约翰的问题的最小K值
该段代码的时间复杂度为 O ( n 2 ) O(n^2) O(n2),因为外层循环枚举了 k k k 个长度,内层循环每次需要枚举 n − k + 1 n-k+1 n−k+1 个长度为 k k k 的子串,并且使用了 set 进行查重。set 的查找时间复杂度为 O ( 1 ) O(1) O(1),所以内层循环的时间复杂度为 O ( ( n − k + 1 ) × 1 ) = O ( n − k + 1 ) O((n-k+1) \times 1) = O(n-k+1) O((n−k+1)×1)=O(n−k+1)。因此,总的时间复杂度为: ∑ k = 1 n ( n − k + 1 ) = O ( n 2 ) \sum_{k=1}^{n} (n-k+1) = O(n^2) ∑k=1n(n−k+1)=O(n2)
其中, ∑ k = 1 n ( n − k + 1 ) \sum_{k=1}^{n} (n-k+1) ∑k=1n(n−k+1) 是等差数列求和公式的展开形式。
二分解法:
n = int(input()) # 输入农场数
s = input().strip() # 输入邮筒序列
def check(k): # 定义check函数用来检查k的取值是否满足条件
substrings = set() # 用set存储s中所有长度为k的不同的子串
for i in range(n-k+1):
substrings.add(s[i:i+k])
return len(substrings) == n-k+1 # 如果set中的元素个数为n-k+1则说明所有长度为k的子串均不相同
left, right = 1, n # 初始时,left=1,right=n
while left < right: # 当left < right时,循环继续
mid = (left+right)//2 # 计算中间值mid
if check(mid): # 如果check(mid)返回True,则说明k=mid满足条件,应该继续往左找
right = mid
else: # 如果check(mid)返回False,则说明k=mid不满足条件,应该往右找
left = mid+1
print(left) # 最终left就是满足条件的最小的k
该二分法代码的时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)*,*其中 n n n为字符串的长度。主要耗时的是check函数,时间复杂度为 O ( n k ) O(nk) O(nk), k k k是检查的子串长度,当 k = n k=n k=n时,时间复杂度最大为 O ( n 2 ) O(n^2) O(n2)。而二分法中循环的次数最多为 O ( l o g n ) O(logn) O(logn),因此总时间复杂度为 O ( n ∗ l o g n ) O(n*logn) O(n∗logn)。