算法系列博客之Dynamic Programming
本篇博客将运用动态规划的思想来解决leetcode上467号问题
问题描述:
Consider the string s to be the infinite wraparound string of “abcdefghijklmnopqrstuvwxyz”, so s will look like this: “…zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd….”.
Now we have another string p. Your job is to find out how many unique non-empty substrings of p are present in s. In particular, your input is the string p and you need to output the number of different non-empty substrings of p in the string s.
Note: p consists of only lowercase English letters and the size of p might be over 10000.
这里先对题目的关键要求简要说明如下:
· 目的是计算字符串p中满足特定需求的子串的个数
· 要求是能够在字符串s中找到相同的子串与之字面值完全相同
· 相同子串不做重复计算,且字面值相同的子串均视为相同子串
首先,观察字符串s,假定 ‘z’和 ‘a’视作连续,那么字符串s的任意子串均是字典序连续的,也即是说p中满足需求的子串也一定是连续的,这是一个非常关键的入手点。
其次,对于字符串增加一个字符,那么它所增加的子串,均是以该字符结尾向前延续的
因而如果我们做出假设
res[i]表示从0到i的p的子串所对应的满足需求的子串数目
continue[i]表示从i向前最长连续的个数
那么res[i] = res[i-1] + continue[i]
回顾之前的要求,发现它只满足了前两点,并不满足第三点
考虑一个子串可能的情况,前面的推理过程就表达出了一种区分子串的方法:
根据结尾字符和长度来对子串进行区分
而且一旦结尾字符为c长度为l的子串出现,那么结尾为c长度为1-l的子串均已经出现并进行计算了,因而可以得到下面一种方法来防止重复计算
数组f[c]表示以c结尾目前已经出现的最长的连续子串的长度
至此,再对前面的状态转移方程进行修改
res[i] = res[i-1] + (continue[i] > f[p[i]] ? continue[i] - f[p[i]] : 0 )
同时需要对f进行更新
f[p[i]] = max(continue[i], f[p[i]])
最后考虑其初始状态,如果p不为空串以0作为起始位数,i = 0时:
res[0] = 1; f[p[0]] = 1
数组f长度26, 其余元素均为0
此时,可以着手开始用代码实现:
class Solution(object):
def findSubstringInWraproundString(self, p):
if not p: return 0
res = [0] * len(p)
res[0] = 1
continue_num = [0] * len(p)
f = [0] * 26
f[ord(p[0]) - ord('a')] = 1
for i in range(1, len(p)):
if ord(p[i]) == ord(p[i-1]) + 1 or (p[i] == 'a' and p[i-1] == 'z'):
continue_num[i] = continue_num[i-1] + 1
else: continue_num[i] = 1
if continue_num[i] > f[ord(p[i]) - ord('a')]:
res[i] = res[i-1] + continue_num[i] - f[ord(p[i]) - ord('a')]
f[ord(p[i]) - ord('a')] = continue_num[i]
else: res[i] = res[i-1]
return res[len(p)-1]
经过测试,运算结果正确无误;但再仔细回顾整个推理过程,发现res和continue仅仅只是依赖于上一次的计算结果,因而完全可以简化这里的空间复杂度,得到如下的实现方式
class Solution(object):
def findSubstringInWraproundString(self, p):
if not p: return 0
res = continue_num = 1
f = [0] * 26
f[ord(p[0]) - ord('a')] = 1
for i in range(1, len(p)):
if ord(p[i]) == ord(p[i-1]) + 1 or (p[i] == 'a' and p[i-1] == 'z'):
continue_num += 1
else: continue_num = 1
if continue_num > f[ord(p[i]) - ord('a')]:
res += continue_num - f[ord(p[i]) - ord('a')]
f[ord(p[i]) - ord('a')] = continue_num
return res
比较起来,代空间复杂度降低的同时还精简了代码,使得逻辑更加清晰可读
复杂度分析,假设p的长度为n
时间上,仅仅只有一层循环,循环内无嵌套,常数指令数,因而O(n)
空间上,所用空间为常数,不依赖于任何变量,因而O(1)