.md
文章目录
- 请添加图片描述
- 一 问题描述
- 测试样例
- 示例
- 二 思路
- 个人思路(ERROR)
- 思路(right)
- !!解题思路 详细
- 答案
- 三、理解
- 1、 理解嵌套循环:也就是连续子串的所有可能性位置-看懂了
- 2、问题又来了,即使确定了能得到不同长度连续子串,也有不同种变换方式。这是又是怎么处理的?_答:不具体考虑替换成啥,只考虑截取字段数量能否满足外界替换需求量(因为题目只要求找到最短子串,而不要求提供具体替换子串长啥样,只要数学上存在可行性,这个子串就列入可选)
一 问题描述
小F得到了一个特殊的字符串,这个字符串只包含字符A、S、D、F,其长度总是4的倍数。他的任务是通过尽可能少的替换,使得A、S、D、F这四个字符在字符串中出现的频次相等。求出实现这一条件的最小子串长度。
测试样例
样例1:
输入:
input = "ADDF"
输出:1
样例2:
输入:
input = "ASAFASAFADDD"
输出:3
样例3:
输入:
input = "SSDDFFFFAAAS"
输出:1
样例4:
输入:
input = "AAAASSSSDDDDFFFF"
输出:0
样例5:
输入:
input = "AAAADDDDAAAASSSS"
输出:4
示例
def solution(input):# Please write your code herereturn -2if __name__ == "__main__":# You can add more test cases hereprint(solution("ADDF") == 1 )print(solution("ASAFASAFADDD") == 3)
二 思路
个人思路(ERROR)
完全没想法
思路(right)
疑问:替换字母,让最终每个字母出现次数一样,所以不明白啥是最小子串。
答:需要连续替换字母,而不是单独替换(区别DNA序列编辑位置)
关键点在于:替换必须是连续的字母
要找到能让整个字符串平衡的最短连续子串
如果原字符串已经平衡,返回0
如果无法通过替换达到平衡,返回-1
!!解题思路 详细
让我一步步解释解题思路是如何推导出来的。
- 首先分析问题的特点:
- 要让所有字母出现次数相等
- 我们要找的是一段连续的子串去替换
- 需要找最短的符合条件的子串
- 从简单情况开始思考:ok
# 例如: "ADDF"
# 当前统计:A(1次), D(2次), F(1次), S(0次)
# 要让每个字母出现1次,显然只需要把一个D改成S就行
-
然后归纳解题步骤:
1、具体解题思路分解:
# 1. 先写一个函数判断字符串是否平衡
def is_balanced(s):freq = {'A': 0, 'S': 0, 'D': 0, 'F': 0}for c in s:freq[c] += 1# 如果所有字符出现次数相等,返回Truereturn len(set(freq.values())) == 1# 2. 如果已经平衡,直接返回0
if is_balanced(input_str):return 0# 3. 否则,从长度1开始尝试:
for length in range(1, len(input_str) + 1):# 对于每个长度,尝试所有可能的起始位置for start in range(len(input_str) - length + 1):# 判断替换这段子串是否能达到平衡
2、关键点是怎么判断一个子串是否可以替换:数学可行性
# 计算每个字符还需要多少个needed = {'A': 0, 'S': 0, 'D': 0, 'F': 0}valid = Truefor char in new_freq:if new_freq[char] > target: # 如果某个字符太多,无法通过替换达到平衡valid = Falsebreakneeded[char] = target - new_freq[char]# 如果所需字符数等于子串长度,说明可以通过替换达到平衡if valid and sum(needed.values()) == length:return length
3、让我们用一个具体例子走一遍:
# 输入:"ADDF"
# n = 4, target = 1 (每个字符应该出现1次)# 初始频次:
# A: 1
# D: 2
# F: 1
# S: 0# 尝试长度为1的子串:
# 当考虑替换第二个D时:
# - 减去这个D后的频次:A(1), D(1), F(1), S(0)
# - 需要补充:S(1)
# - 子串长度也是1
# 所以这是一个可行解,返回1
这道题的思路核心是:
-
它直接模拟了替换过程
通过计算替换后的频次和所需补充的字符数来判断是否可行
从最小长度开始尝试,保证找到的是最小解
答案
def solution(input_str):n = len(input_str)target = n // 4 # 因为是4的倍数,所以每个字符应该出现n/4次# 统计每个字符出现的次数freq = {'A': 0, 'S': 0, 'D': 0, 'F': 0}for c in input_str:freq[c] += 1# 如果已经平衡,返回0if all(f == target for f in freq.values()):return 0# 从小到大尝试子串长度for length in range(1, n + 1):# 遍历所有可能的起始位置for start in range(n - length + 1):# 尝试替换这个子串后的频次new_freq = freq.copy()# 减去要替换的子串中的字符频次for i in range(start, start + length):new_freq[input_str[i]] -= 1# 检查是否可以通过替换这个子串达到平衡needed = {'A': 0, 'S': 0, 'D': 0, 'F': 0}valid = Truefor char in new_freq:if new_freq[char] > target: # 如果某个字符频次超过目标值,无法通过替换达到平衡valid = Falsebreakneeded[char] = target - new_freq[char]if valid and sum(needed.values()) == length:return lengthreturn -1if __name__ == "__main__":# You can add more test cases hereprint(solution("ADDF") == 1 )print(solution("ASAFASAFADDD") == 3)
三、理解
1、 理解嵌套循环:也就是连续子串的所有可能性位置-看懂了
# 3. 否则,从长度1开始尝试:
for length in range(1, len(input_str) + 1):# 对于每个长度,尝试所有可能的起始位置for start in range(len(input_str) - length + 1):# 判断替换这段子串是否能达到平衡
让我用一个具体的例子来解释这段代码的逻辑。
假设我们有输入字符串 "ASAFASAFADDD"
(长度为12)
# 外层循环: length从1开始尝试
for length in range(1, len(input_str) + 1):# length = 1, 2, 3, ... , 12
让我们看看当 length = 3
时的内层循环是如何工作的:
让我们详细拆解:
- 为什么是
range(len(input_str) - length + 1)
?
# 假设 length = 3:
input_str = "ASAFASAFADDD" # 长度12# range(12 - 3 + 1) = range(10)
# 也就是从0到9的位置都可以作为起始位置
# 为什么到9?因为从位置9开始取3个字符正好到末尾
# 如果从10开始就不够3个字符了# 具体示例:
start = 0: "ASA"FASAFADDD
start = 1: A"SAF"ASAFADDD
start = 2: AS"AFA"SAFADDD
start = 3: ASA"FAS"AFADDD
...
start = 9: ASAFASAFA"DDD"
- 完整示例代码帮助理解:
def print_all_substrings(input_str, length):for start in range(len(input_str) - length + 1):substring = input_str[start:start + length]print(f"位置{start}: {substring}")# 测试
s = "ASAFASAFADDD"
print("当length=3时,所有可能的子串:")
print_all_substrings(s, 3)
输出会是:
当length=3时,所有可能的子串:
位置0: ASA
位置1: SAF
位置2: AFA
位置3: FAS
位置4: ASA
位置5: SAF
位置6: AFA
位置7: FAD
位置8: ADD
位置9: DDD
- 为什么要这样遍历?
- 我们需要找到一个连续的子串进行替换
- 这个子串可能在字符串的任何位置
- 通过这两层循环,我们可以:
- 尝试所有可能的子串长度(外层循环)
- 对每个长度,尝试所有可能的起始位置(内层循环)
- 直到找到一个可以通过替换达到平衡的子串
- 在实际解题中:
for length in range(1, len(input_str) + 1):for start in range(len(input_str) - length + 1):# 对于每个子串,检查替换它是否能让整个字符串平衡if can_balance_with_substring(input_str, start, length):return length # 找到答案就返回
这就像是在一个窗口在字符串上滑动:
- 先用长度为1的窗口从左到右滑动
- 如果找不到解,就用长度为2的窗口重新滑动
- 依此类推,直到找到一个可以替换的子串
这样理解这段代码的作用了吗?它就是在系统地尝试所有可能的"连续子串",看哪一个可以通过替换来达到我们的目标。
2、问题又来了,即使确定了能得到不同长度连续子串,也有不同种变换方式。这是又是怎么处理的?_答:不具体考虑替换成啥,只考虑截取字段数量能否满足外界替换需求量(因为题目只要求找到最短子串,而不要求提供具体替换子串长啥样,只要数学上存在可行性,这个子串就列入可选)
子串替换判断逻辑
以 “ADDF” 为例:
让我们看看代码是如何处理的:
def solution(input_str):n = len(input_str)target = n // 4 # 目标:每个字符应该出现的次数# 初始频次统计freq = {'A': 0, 'S': 0, 'D': 0, 'F': 0}for c in input_str:freq[c] += 1# 对于 "ADDF":# freq = {'A': 1, 'D': 2, 'F': 1, 'S': 0}# target = 1for length in range(1, n + 1):for start in range(n - length + 1):# 复制一份频次字典,准备模拟替换new_freq = freq.copy()# 减去要替换的子串中的字符for i in range(start, start + length):new_freq[input_str[i]] -= 1# 举例:当length=1, start=1时(第二个位置的D)# 减去这个D后:# new_freq = {'A': 1, 'D': 1, 'F': 1, 'S': 0}# 计算还需要多少个各类字符needed = {'A': 0, 'S': 0, 'D': 0, 'F': 0}valid = Truefor char in new_freq:if new_freq[char] > target:valid = Falsebreakneeded[char] = target - new_freq[char]# 对于上面的例子:# needed = {'A': 0, 'S': 1, 'D': 0, 'F': 0}# sum(needed.values()) = 1,正好等于子串长度# 关键判断:所需字符数是否等于子串长度if valid and sum(needed.values()) == length:return length
关键点在于:
- 我们不需要具体知道怎么替换
- 我们只需要知道"是否可能通过替换达到平衡"
- 具体替换成什么字符并不重要,只要数学上成立即可
- 判断方法:
- 去掉要替换的子串中的字符
- 计算还缺少多少个各类字符
- 如果缺少的字符总数等于子串长度,就说明可以通过替换达到平衡
- 举个具体例子:
# "ADDF",考虑替换第二个D
原始频次:A(1), D(2), F(1), S(0)
目标频次:都应该是11. 去掉D后的频次:A(1), D(1), F(1), S(0)
2. 计算需要补充的数量:A需要:0个(已经1个)D需要:0个(已经1个)F需要:0个(已经1个)S需要:1个(缺1个)
3. 总共需要1个字符 = 子串长度1说明可以通过替换这个D来达到平衡
所以,我们不需要关心具体的替换方式,只需要:
- 确定替换这个位置是否有可能达到平衡
- 通过计算频次和所需字符数来判断
- 如果数学上可行,就说明一定存在一种替换方式
这种方法的巧妙之处在于:
- 避免了枚举所有可能的替换方式
- 只需要证明存在可行解
- 大大简化了问题的复杂度
用数学方法避免了枚举所有可能的替换方式。