题目:原题链接(困难)
标签:哈希表、回溯算法、递归、字典树
解法 | 时间复杂度 | 空间复杂度 | 执行用时 |
---|---|---|---|
Ans 1 (Python) | – | – | 超出时间限制 (28/34) |
Ans 2 (Python) | – | – | 2216ms (50.82%) |
Ans 3 (Python) |
解法一(暴力递归):
class Solution:
def maxRectangle(self, words: List[str]) -> List[str]:
# 依据单词长度分组
# 时间:O(W) 其中W为单词总数(W<=1000)
word_lst = collections.defaultdict(list) # 依据单词长度分组
for word in words:
word_lst[len(word)].append(word)
# 生成单词长度列表
# 时间:O(L) 其中L为单词的不同长度数量(L<=100,L<=W)
len_lst = list(sorted(word_lst.keys()))
len_size = len(len_lst)
# 初始化答案矩阵、答案矩阵的面积
ans_lst, ans_val = [], 0
# 遍历所有可能的单词长度组合
# 循环次数(两层循环累计):O(L^2)
for i1 in range(len_size - 1, -1, -1):
# 计算矩阵的短边边长:l1为短边
l1 = len_lst[i1]
# 计算短边中每个位置的出现的字母集合
# 每次执行:O(w) 其中w为长度为l1的单词数量(w<=W<=1000)
# 累计执行:O(W)
ch_set = collections.defaultdict(set) # 每个长度的单词的开头字母列表
for word in word_lst[l1]:
for i, ch in enumerate(word):
ch_set[i].add(ch)
for i2 in range(len_size - 1, i1 - 1, -1):
# 计算矩阵的长边边长:l2为长边(l1可以与l2相等)
l2 = len_lst[i2]
# 如果矩阵面积小于已经得到的答案,则直接跳过当前情况
if l1 * l2 <= ans_val:
continue
# 遍历寻找所有可能的长边中的每一行
# 每次执行:O(w1×l1) 其中w1为长度为l2的单词数量(w<=W<=1000),l1为单词的字母数量(l1<=100)
maybe_lst = [[] for _ in range(l1)]
for word in word_lst[l2]:
for i in range(l1):
if set(word) <= ch_set[i]:
maybe_lst[i].append(word)
# 如果存在任何一行没有被匹配成功则跳过当前情况
if min(len(maybe_words) for maybe_words in maybe_lst) == 0:
continue
# print(l1, l2, ":", maybe_lst)
# 回溯计算所有可能的结果
# 理论最坏情况的递归函数执行次数:O(w1^l1) —— 即每个值在每一行都有可行(但此时需要大量的长度为l1的单词支持,w1应该会比最大值小很多)
def track(now, col_lst, res):
""" 回溯函数
:param now: 下一个需要选择的行序号
:param col_lst: 当前已经确定的列
:param res: 已经完成匹配的部分
"""
# print("[回溯]", now, res, col_lst)
# 处理回溯完成的情况
if now == l1:
return [res]
# 遍历当前行序号的所有选择
# 循环累计执行:O(w2*l2) 当前行的所有可能选择
maybe_res = []
for maybe in maybe_lst[now]:
# 复制每列的选择列表
# O(w1)
col_lst_copy = [copy.copy(col) for col in col_lst]
# 检查当前选择是否符合要求
# 循环执行次数:O(l2)
fail = False
for j in range(l2):
# 移除当前列不符合要求的值
# 累计执行:O(w1)
for maybe_word in list(col_lst_copy[j]):
if maybe_word[now] != maybe[j]:
col_lst_copy[j].remove(maybe_word)
# 如果当前列剩余的选择数为0则剪枝
if len(col_lst_copy[j]) == 0:
fail = True
break
# 如果不符合要求则跳过当前选择
if fail:
continue
# 处理符合要求的循环
# O(w1)
maybe_res.extend(track(now + 1, col_lst_copy, res + [maybe]))
return maybe_res
results = track(0, [set(word_lst[l1]) for _ in range(l2)], [])
# print(results)
if results:
ans_lst, ans_val = results[0], l1 * l2
return ans_lst
解法二(字典树提速+调整逻辑):
class Solution:
def maxRectangle(self, words: List[str]) -> List[str]:
# 依据单词长度分组
# 时间:O(W) 其中W为单词总数(W<=1000)
word_lst = collections.defaultdict(list) # 依据单词长度分组
for word in words:
word_lst[len(word)].append(word)
# 生成单词长度列表
# 时间:O(L) 其中L为单词的不同长度数量(L<=100,L<=W)
len_lst = list(sorted(word_lst.keys()))
len_size = len(len_lst)
# 构造字典树
# 时间:O(W×L)
word_tree = collections.defaultdict(dict)
for l in len_lst:
for word in word_lst[l]:
node = word_tree[l]
for ch in word:
if ch not in node:
node[ch] = {}
node = node[ch]
# 初始化答案矩阵、答案矩阵的面积
ans_lst, ans_val = [], 0
# 遍历所有可能的单词长度组合
# 循环次数(两层循环累计):O(L^2)
for i1 in range(len_size - 1, -1, -1):
# 计算矩阵的短边边长:l1为短边
l1 = len_lst[i1]
# 计算短边中每个位置的出现的字母集合
# 每次执行:O(w*l1) 其中w为长度为l1的单词数量(w<=W<=1000)
# 累计执行:O(W*l1)
ch_set = collections.defaultdict(set) # 每个长度的单词的开头字母列表
for word in word_lst[l1]:
for i, ch in enumerate(word):
ch_set[i].add(ch)
for i2 in range(len_size - 1, i1 - 1, -1):
# 计算矩阵的长边边长:l2为长边(l1可以与l2相等)
l2 = len_lst[i2]
# print(l1, l2)
# 如果矩阵面积小于已经得到的答案,则直接跳过当前情况
if l1 * l2 <= ans_val:
continue
# 遍历寻找所有可能的长边中的每一行
# 每次执行:O(w1×l1) 其中w1为长度为l2的单词数量(w<=W<=1000),l1为单词的字母数量(l1<=100)
maybe_lst = [[] for _ in range(l1)]
for word in word_lst[l2]:
set_word = set(word)
for i in range(l1):
if set_word <= ch_set[i]:
maybe_lst[i].append(word)
# 如果存在任何一行没有被匹配成功则跳过当前情况
if min(len(maybe_words) for maybe_words in maybe_lst) == 0:
continue
# print(l1, l2, ":", maybe_lst)
# print("当前用时:", time.time() - start)
# 回溯计算所有可能的结果
# 理论最坏情况的递归函数执行次数:O(w1^l1) —— 即每个值在每一行都有可行(但此时需要大量的长度为l1的单词支持,w1应该会比最大值小很多)
def track(now, col_lst, res):
""" 回溯函数
:param now: 下一个需要选择的行序号
:param col_lst: 每列当前在字典树中的节点
:param res: 已经完成匹配的部分
"""
# print("[回溯]", now, res, col_lst)
# 处理回溯完成的情况
if now == l1:
return [res]
# 遍历当前行序号的所有选择
maybe_res = []
for maybe in maybe_lst[now]:
new_col_lst = []
# 检查当前选择是否存在结果
# 循环执行次数:O(l2)
fail = False
for j in range(l2):
# 如果当前列剩余
if maybe[j] in col_lst[j]:
new_col_lst.append(col_lst[j][maybe[j]])
else:
fail = True
break
# 如果不符合要求则跳过当前选择
if fail:
continue
# 处理符合要求的循环
maybe_res.extend(track(now + 1, new_col_lst, res + [maybe]))
return maybe_res
results = track(0, [word_tree[l1]] * l2, [])
# print(results)
if results:
ans_lst, ans_val = results[0], l1 * l2
return ans_lst