class Solution {
public:
vector<vector<string>> deleteDuplicateFolder(vector<vector<string>>& paths) {
}
};
1948. 删除系统中的重复文件夹
由于一个漏洞,文件系统中存在许多重复文件夹。给你一个二维数组 paths
,其中 paths[i]
是一个表示文件系统中第 i
个文件夹的绝对路径的数组。
["one", "two", "three"]
表示路径 "/one/two/three"
。如果两个文件夹(不需要在同一层级)包含 非空且相同的 子文件夹 集合 并具有相同的子文件夹结构,则认为这两个文件夹是相同文件夹。相同文件夹的根层级 不 需要相同。如果存在两个(或两个以上)相同 文件夹,则需要将这些文件夹和所有它们的子文件夹 标记 为待删除。
"/a"
和 "/b"
相同。它们(以及它们的子文件夹)应该被 全部 标记为待删除:
/a
/a/x
/a/x/y
/a/z
/b
/b/x
/b/x/y
/b/z
"/b/w"
,那么文件夹 "/a"
和 "/b"
就不相同。注意,即便添加了新的文件夹 "/b/w"
,仍然认为 "/a/x"
和 "/b/x"
相同。一旦所有的相同文件夹和它们的子文件夹都被标记为待删除,文件系统将会 删除 所有上述文件夹。文件系统只会执行一次删除操作。执行完这一次删除操作后,不会删除新出现的相同文件夹。
返回二维数组 ans
,该数组包含删除所有标记文件夹之后剩余文件夹的路径。路径可以按 任意顺序 返回。
示例 1:
输入:paths = [["a"],["c"],["d"],["a","b"],["c","b"],["d","a"]] 输出:[["d"],["d","a"]] 解释:文件结构如上所示。 文件夹 "/a" 和 "/c"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为 "b" 的空文件夹。
示例 2:
输入:paths = [["a"],["c"],["a","b"],["c","b"],["a","b","x"],["a","b","x","y"],["w"],["w","y"]] 输出:[["c"],["c","b"],["a"],["a","b"]] 解释:文件结构如上所示。 文件夹 "/a/b/x" 和 "/w"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为 "y" 的空文件夹。 注意,文件夹 "/a" 和 "/c" 在删除后变为相同文件夹,但这两个文件夹不会被删除,因为删除只会进行一次,且它们没有在删除前被标记。
示例 3:
输入:paths = [["a","b"],["c","d"],["c"],["a"]] 输出:[["c"],["c","d"],["a"],["a","b"]] 解释:文件系统中所有文件夹互不相同。 注意,返回的数组可以按不同顺序返回文件夹路径,因为题目对顺序没有要求。
示例 4:
输入:paths = [["a"],["a","x"],["a","x","y"],["a","z"],["b"],["b","x"],["b","x","y"],["b","z"]] 输出:[] 解释:文件结构如上所示。 文件夹 "/a/x" 和 "/b/x"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含名为 "y" 的空文件夹。 文件夹 "/a" 和 "/b"(以及它们的子文件夹)都会被标记为待删除,因为它们都包含一个名为 "z" 的空文件夹以及上面提到的文件夹 "x" 。
示例 5:
输入:paths = [["a"],["a","x"],["a","x","y"],["a","z"],["b"],["b","x"],["b","x","y"],["b","z"],["b","w"]] 输出:[["b"],["b","w"],["b","z"],["a"],["a","z"]] 解释:本例与上例的结构基本相同,除了新增 "/b/w" 文件夹。 文件夹 "/a/x" 和 "/b/x" 仍然会被标记,但 "/a" 和 "/b" 不再被标记,因为 "/b" 中有名为 "w" 的空文件夹而 "/a" 没有。 注意,"/a/z" 和 "/b/z" 不会被标记,因为相同子文件夹的集合必须是非空集合,但这两个文件夹都是空的。
提示:
1 <= paths.length <= 2 * 104
1 <= paths[i].length <= 500
1 <= paths[i][j].length <= 10
1 <= sum(paths[i][j].length) <= 2 * 105
path[i][j]
由小写英文字母组成原站题解
java 解法, 执行用时: 196 ms, 内存消耗: 63.7 MB, 提交时间: 2023-06-13 14:15:00
class Trie { String ss; Map<String, Trie> children; Trie() { this.ss = ""; this.children = new HashMap<>(); } //------------ 向trie树中插入path public void insert_path(List<String> path) { Trie root = this; for (String p : path) { if (root.children.containsKey(p) == false) root.children.put(p, new Trie()); root = root.children.get(p); } } } class Solution { // Trie T = new Trie(); // Map<String, Integer> ss_cnt = new HashMap<>(); // List<List<String>> res = new ArrayList<>(); // List<String> path = new ArrayList<>(); //回溯过程中的路径 Trie T; Map<String, Integer> ss_cnt; List<List<String>> res; List<String> path; public List<List<String>> deleteDuplicateFolder(List<List<String>> paths) { this.T = new Trie(); this.ss_cnt = new HashMap<>(); this.res = new ArrayList<>(); this.path = new ArrayList<>(); //---------- (1)将所有的path都插入trie树T中 for (List<String> path: paths) T.insert_path(path); //--------- (2)在T上进行dfs_LRN,求得每个结点对应的ss dfs_LRN_get_trieNode_ss(this.T); //-------- path可能会很长。回溯算法求res backtrace_get_res(this.T); return this.res; } //------ dfs_LRN 获得每个trie_node的ss public void dfs_LRN_get_trieNode_ss(Trie node) { if (node.children.size() == 0) return ; List<String> ss = new ArrayList<>(); for (Map.Entry<String, Trie> kv : node.children.entrySet()) { String child = kv.getKey(); Trie child_trie = kv.getValue(); dfs_LRN_get_trieNode_ss(child_trie); ss.add(child + "(" + child_trie.ss + ")"); } Collections.sort(ss); for (String s: ss) node.ss += s; // System.out.println(node.ss); ss_cnt.put(node.ss, ss_cnt.getOrDefault(node.ss, 0) + 1); } //----- 回溯算法,求res public void backtrace_get_res(Trie node) { if (ss_cnt.getOrDefault(node.ss, 0) >= 2) return ; if (path.size() > 0) { // List<String> tmp = new ArrayList<>(); // for (String p: path) // tmp.add(p); // res.add(tmp); res.add(new ArrayList<>(path)); } for (Map.Entry<String, Trie> kv : node.children.entrySet()) { String child = kv.getKey(); Trie child_trie = kv.getValue(); path.add(child); backtrace_get_res(child_trie); path.remove(path.size() - 1); } } }
python3 解法, 执行用时: 408 ms, 内存消耗: 43.6 MB, 提交时间: 2023-06-13 14:14:02
class Trie: # 当前节点结构的序列化表示 serial: str = "" # 当前节点的子节点 children: dict def __init__(self): self.children = dict() class Solution: def deleteDuplicateFolder(self, paths: List[List[str]]) -> List[List[str]]: # 根节点 root = Trie() for path in paths: cur = root for node in path: if node not in cur.children: cur.children[node] = Trie() cur = cur.children[node] # 哈希表记录每一种序列化表示的出现次数 freq = Counter() # 基于深度优先搜索的后序遍历,计算每一个节点结构的序列化表示 def construct(node: Trie) -> None: # 如果是叶节点,那么序列化表示为空字符串,无需进行任何操作 if not node.children: return v = list() # 如果不是叶节点,需要先计算子节点结构的序列化表示 for folder, child in node.children.items(): construct(child) v.append(folder + "(" + child.serial + ")") # 防止顺序的问题,需要进行排序 v.sort() node.serial = "".join(v) # 计入哈希表 freq[node.serial] += 1 construct(root) ans = list() # 记录根节点到当前节点的路径 path = list() def operate(node: Trie) -> None: # 如果序列化表示在哈希表中出现了超过 1 次,就需要删除 if freq[node.serial] > 1: return # 否则将路径加入答案 if path: ans.append(path[:]) for folder, child in node.children.items(): path.append(folder) operate(child) path.pop() operate(root) return ans
golang 解法, 执行用时: 420 ms, 内存消耗: 16 MB, 提交时间: 2023-06-13 14:13:22
// 字典树 + 括号表示法 + 哈希表 type folder struct { son map[string]*folder val string // 文件夹名称 del bool // 删除标记 } func deleteDuplicateFolder(paths [][]string) (ans [][]string) { root := &folder{} for _, path := range paths { // 将 path 加入字典树 f := root for _, s := range path { if f.son == nil { f.son = map[string]*folder{} } if f.son[s] == nil { f.son[s] = &folder{} } f = f.son[s] f.val = s } } folders := map[string][]*folder{} // 存储括号表达式及其对应的文件夹节点列表 var dfs func(*folder) string dfs = func(f *folder) string { if f.son == nil { return "(" + f.val + ")" } expr := make([]string, 0, len(f.son)) for _, son := range f.son { expr = append(expr, dfs(son)) } sort.Strings(expr) subTreeExpr := strings.Join(expr, "") // 按字典序拼接所有子树 folders[subTreeExpr] = append(folders[subTreeExpr], f) return "(" + f.val + subTreeExpr + ")" } dfs(root) for _, fs := range folders { if len(fs) > 1 { // 将括号表达式对应的节点个数大于 1 的节点全部删除 for _, f := range fs { f.del = true } } } // 再次 DFS 这颗字典树,仅访问未被删除的节点,并将路径记录到答案中 path := []string{} var dfs2 func(*folder) dfs2 = func(f *folder) { if f.del { return } path = append(path, f.val) ans = append(ans, append([]string(nil), path...)) for _, son := range f.son { dfs2(son) } path = path[:len(path)-1] } for _, son := range root.son { dfs2(son) } return }