列表

详情


1948. 删除系统中的重复文件夹

由于一个漏洞,文件系统中存在许多重复文件夹。给你一个二维数组 paths,其中 paths[i] 是一个表示文件系统中第 i 个文件夹的绝对路径的数组。

如果两个文件夹(不需要在同一层级)包含 非空且相同的 子文件夹 集合 并具有相同的子文件夹结构,则认为这两个文件夹是相同文件夹。相同文件夹的根层级 需要相同。如果存在两个(或两个以上)相同 文件夹,则需要将这些文件夹和所有它们的子文件夹 标记 为待删除。

一旦所有的相同文件夹和它们的子文件夹都被标记为待删除,文件系统将会 删除 所有上述文件夹。文件系统只会执行一次删除操作。执行完这一次删除操作后,不会删除新出现的相同文件夹。

返回二维数组 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" 不会被标记,因为相同子文件夹的集合必须是非空集合,但这两个文件夹都是空的。

 

提示:

原站题解

去查看

上次编辑到这里,代码来自缓存 点击恢复默认模板
class Solution { public: vector<vector<string>> deleteDuplicateFolder(vector<vector<string>>& paths) { } };

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
}

上一题