戳印序列

标签: 贪心 队列 字符串

难度: Hard

你想要用小写字母组成一个目标字符串 target。 

开始的时候,序列由 target.length 个 '?' 记号组成。而你有一个小写字母印章 stamp

在每个回合,你可以将印章放在序列上,并将序列中的每个字母替换为印章上的相应字母。你最多可以进行 10 * target.length  个回合。

举个例子,如果初始序列为 "?????",而你的印章 stamp 是 "abc",那么在第一回合,你可以得到 "abc??"、"?abc?"、"??abc"。(请注意,印章必须完全包含在序列的边界内才能盖下去。)

如果可以印出序列,那么返回一个数组,该数组由每个回合中被印下的最左边字母的索引组成。如果不能印出序列,就返回一个空数组。

例如,如果序列是 "ababc",印章是 "abc",那么我们就可以返回与操作 "?????" -> "abc??" -> "ababc" 相对应的答案 [0, 2]

另外,如果可以印出序列,那么需要保证可以在 10 * target.length 个回合内完成。任何超过此数字的答案将不被接受。

示例 1:

输入:stamp = "abc", target = "ababc"
输出:[0,2]
([1,0,2] 以及其他一些可能的结果也将作为答案被接受)

示例 2:

输入:stamp = "abca", target = "aabcaca"
输出:[3,0,1]

提示:

  1. 1 <= stamp.length <= target.length <= 1000
  2. stamp 和 target 只包含小写字母。

Submission

运行时间: 51 ms

内存: 16.4 MB

class Solution:
	def movesToStamp(self, stamp, target):
		self.invalid = set() #所有不能完成的开头坐标
		return self.recurtion(stamp, target, 0)
	
	def recurtion(self, stamp, target, idx):
		ans = []
		n = len(stamp)
		while True:
			l = self.left(target, idx, stamp)
			if l == 0:
				return []
			if l == n: #找到全部的stamp,转而寻找结尾相同
				break
			for i in range(1, l):
				if idx + i not in self.invalid: #最长的开头可能反而不符合条件,遍历所有可能的短开头
					tmp = self.recurtion(stamp, target, idx + i)
					if tmp:
						return ans + [idx] + tmp
					else:
						self.invalid.add(idx + i)
			ans.append(idx)
			idx += l
		idx2 = idx + l
		rl = []
		while True:
			if idx2 == len(target): #右端点同时是结尾相同部分的末尾,找到和符合要求的方法
				return ans + rl[::-1] + [idx]
			if idx2 not in self.invalid: #检验该右端点(包含全部stamp)后面是否可以作为左端点找到合法分割
				tmp = self.recurtion(stamp, target, idx2)
				if tmp:
					return ans + rl[::-1] + [idx] + tmp
				else:
					self.invalid.add(idx2)
			r = self.right(target, idx2, stamp)
			if r == 0:
				idx0, tmp = self.check(target, idx2, stamp)
				if tmp:
					return [idx0] + ans + rl[::-1] + [idx] + tmp
				return []
			rl.append(idx2 + r - n)
			idx2 += r
		return ans
	
	def left(self, t, idx, s): #寻找开头与stamp相同的部分
		i = 1
		while i <= len(s) and t[idx: idx + i] == s[:i]:
			i += 1
		return i - 1
	
	def right(self, t, idx, s):
		for l in range(1, len(s)):
			if t[idx: idx + l] == s[-l:]:
				return l
		return 0
	
	#当前是结尾优于当前是开头(不能接结尾),开头->结尾须要一个完整的stamp
	def check(self, t, idx, s):
		l = 1
		while idx + l < len(t) and l < len(s) - 1:
			if t[idx + l] == s[0]:
				i = 0
				while i + l < len(s):
					if t[idx: idx + l] == s[i: i + l]:
						break
					i += 1
				if i + l < len(s) and idx + l not in self.invalid:
					tmp = self.recurtion(s, t, idx + l)
					if tmp:
						return idx - i, tmp
					else:
						self.invalid.add(idx + l)
			l += 1
		return -1, []

Explain

这个题解采用了回溯的思路。从目标字符串的左端开始,先找到与印章开头相同的部分,然后递归地寻找右边剩余部分的合法分割。如果找不到,就回溯尝试更短的开头。当找到一个与印章完全相同的子串时,转而从右端开始寻找与印章结尾相同的部分,直到找到一个完整的合法分割。在回溯的过程中,用一个集合记录所有不能完成的开头坐标,避免重复搜索。

时间复杂度: O(m^(n/m))

空间复杂度: O(n^2/m)

class Solution:
	def movesToStamp(self, stamp, target):
		self.invalid = set() #所有不能完成的开头坐标
		return self.recurtion(stamp, target, 0)
	
	def recurtion(self, stamp, target, idx):
		ans = []
		n = len(stamp)
		while True:
			l = self.left(target, idx, stamp)
			if l == 0:
				return []
			if l == n: #找到全部的stamp,转而寻找结尾相同
				break
			for i in range(1, l):
				if idx + i not in self.invalid: #最长的开头可能反而不符合条件,遍历所有可能的短开头
					tmp = self.recurtion(stamp, target, idx + i)
					if tmp:
						return ans + [idx] + tmp
					else:
						self.invalid.add(idx + i)
			ans.append(idx)
			idx += l
		idx2 = idx + l
		rl = []
		while True:
			if idx2 == len(target): #右端点同时是结尾相同部分的末尾,找到和符合要求的方法
				return ans + rl[::-1] + [idx]
			if idx2 not in self.invalid: #检验该右端点(包含全部stamp)后面是否可以作为左端点找到合法分割
				tmp = self.recurtion(stamp, target, idx2)
				if tmp:
					return ans + rl[::-1] + [idx] + tmp
				else:
					self.invalid.add(idx2)
			r = self.right(target, idx2, stamp)
			if r == 0:
				idx0, tmp = self.check(target, idx2, stamp)
				if tmp:
					return [idx0] + ans + rl[::-1] + [idx] + tmp
				return []
			rl.append(idx2 + r - n)
			idx2 += r
		return ans
	
	def left(self, t, idx, s): #寻找开头与stamp相同的部分
		i = 1
		while i <= len(s) and t[idx: idx + i] == s[:i]:
			i += 1
		return i - 1
	
	def right(self, t, idx, s):
		for l in range(1, len(s)):
			if t[idx: idx + l] == s[-l:]:
				return l
		return 0
	
	#当前是结尾优于当前是开头(不能接结尾),开头->结尾须要一个完整的stamp
	def check(self, t, idx, s):
		l = 1
		while idx + l < len(t) and l < len(s) - 1:
			if t[idx + l] == s[0]:
				i = 0
				while i + l < len(s):
					if t[idx: idx + l] == s[i: i + l]:
						break
					i += 1
				if i + l < len(s) and idx + l not in self.invalid:
					tmp = self.recurtion(s, t, idx + l)
					if tmp:
						return idx - i, tmp
					else:
						self.invalid.add(idx + l)
			l += 1
		return -1, []

Explore

在`recurtion`函数中,每次递归调用都是通过改变索引`idx`来确保问题规模的逐步减小。在成功匹配了印章的一部分后,将`idx`增加匹配的长度`l`,这样每次递归都是基于目标字符串的新的子字符串。此外,通过在集合`invalid`中记录那些已经被证实无法完成任务的起始点,可以防止在相同的位置重复进行无效的递归调用,这也有助于减少问题规模并避免无限递归。

当`left`函数中找到的匹配长度小于印章`stamp`的长度时,这表示只有部分印章与目标字符串相匹配。这种部分匹配可能导致无法直接在当前位置实现完整的印章覆盖,从而需要进一步的递归调用来尝试其他可能的匹配。这种部分匹配允许算法在不立即找到完整匹配的情况下,通过递归探索不同的匹配可能性,逐步构建出一个有效的戳印序列。

在处理结尾相同的部分时,从右向左递归搜索是为了确保在构建戳印序列时可以有效地覆盖目标字符串的末端。从右向左的搜索可以帮助确认在已经部分匹配的情况下,目标字符串的剩余部分是否可以被继续有效地覆盖。这种方向的变换允许算法在遇到难以从左端开始完全匹配的情况时,从另一个方向尝试可能的解决方案,增加了算法的灵活性和覆盖率。从效率上讲,这种双向递归策略可以在某些情况下减少不必要的递归深度,更快地找到解决方案。