Contents

ARST打卡第324周

Algorithm

lc1948_删除系统中的重复文件夹

思路:

每个节点都由所有的子路径树编码成一串字符串,如果发现两个节点的编码相同,则可以删除这个编码。

编码可以由map+set/sorted_vec实现,map的key是编码,value是节点,set/sorted_vec的value是节点。

编码的生成方式:

  1. 从根节点开始,遍历所有子节点,将子节点的编码拼接起来,形成一个编码。
  2. 如果发现两个节点的编码相同,则可以删除这个编码。
  3. 如果发现两个节点的编码不同,则可以保留这个编码。

代码实现:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
use std::collections::{HashMap, HashSet};

#[derive(Debug)]
struct TreeNode {
    name: String,
    children: HashMap<String, TreeNode>,
}

impl Solution {
    pub fn delete_duplicate_folder(paths: Vec<Vec<String>>) -> Vec<Vec<String>> {
        // 构建树结构
        let mut root = TreeNode {
            name: "".to_string(),
            children: HashMap::new(),
        };
        
        for path in paths {
            let mut current = &mut root;
            for folder in path {
                current.children.entry(folder.clone()).or_insert_with(|| TreeNode {
                    name: folder.clone(),
                    children: HashMap::new(),
                });
                current = current.children.get_mut(&folder).unwrap();
            }
        }
        
        // 用于存储编码到节点的映射
        let mut encoding_map: HashMap<String, Vec<*mut TreeNode>> = HashMap::new();
        
        // 生成编码并收集重复节点
        Self::generate_encoding(&mut root, &mut encoding_map);
        
        // 标记需要删除的节点
        let mut to_delete: HashSet<*mut TreeNode> = HashSet::new();
        for (encoding, nodes) in encoding_map {
            if nodes.len() > 1 && !encoding.is_empty() {
                // 有重复的编码且编码非空,标记所有节点为删除
                for node_ptr in nodes {
                    to_delete.insert(node_ptr);
                }
            }
        }
        
        // 收集结果
        let mut result = Vec::new();
        Self::collect_paths(&root, &to_delete, &mut Vec::new(), &mut result);
        
        result
    }
    
    fn generate_encoding(node: &mut TreeNode, encoding_map: &mut HashMap<String, Vec<*mut TreeNode>>) -> String {
        if node.children.is_empty() {
            return "".to_string();
        }
        
        // 收集所有子节点的编码,包含子节点名字
        let mut child_encodings = Vec::new();
        for child in node.children.values_mut() {
            let child_encoding = Self::generate_encoding(child, encoding_map);
            // child_encodings.push(format!("({})", child_encoding));
            child_encodings.push(format!("{}({})", child.name, child_encoding));
        }
        
        // 排序确保相同结构的编码一致
        child_encodings.sort();
        
        // 生成当前节点的编码
        let encoding = child_encodings.join("");
        
        // 将编码和节点指针存储到映射中
        encoding_map.entry(encoding.clone()).or_insert_with(Vec::new).push(node as *mut TreeNode);
        
        encoding
    }
    
    fn collect_paths(
        node: &TreeNode, 
        to_delete: &HashSet<*mut TreeNode>, 
        current_path: &mut Vec<String>, 
        result: &mut Vec<Vec<String>>
    ) {
        // 如果当前节点被标记为删除,不收集路径
        if to_delete.contains(&(node as *const TreeNode as *mut TreeNode)) {
            return;
        }
        
        // 如果有路径且不是根节点,添加到结果中
        if !current_path.is_empty() {
            result.push(current_path.clone());
        }
        
        // 递归处理子节点
        for child in node.children.values() {
            current_path.push(child.name.clone());
            Self::collect_paths(child, to_delete, current_path, result);
            current_path.pop();
        }
    }
}

这个实现的关键点:

  1. 树结构构建:将输入的路径列表构建成树结构,每个节点包含名称和子节点映射。
  2. 编码生成:递归地为每个节点生成编码,编码由所有子节点的编码组成,并按照字典序排序确保相同结构的编码一致。
  3. 重复检测:使用HashMap存储编码到节点指针的映射,如果某个编码对应多个节点,说明存在重复结构。
  4. 路径收集:遍历树结构,收集所有未被标记为删除的节点的路径。
  5. 内存安全:使用原始指针来标识节点,但要注意在Rust中需要确保指针的有效性。

这个算法的时间复杂度是O(n * m),其中n是路径数量,m是平均路径长度。空间复杂度是O(n * m)。 题解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
use std::collections::HashMap;

#[derive(Default)]
struct Trie {
    serial: String, // 当前节点结构的序列化表示
    children: HashMap<String, Trie>, // 当前节点的子节点
}

impl Solution {
    pub fn delete_duplicate_folder(paths: Vec<Vec<String>>) -> Vec<Vec<String>> {
        let mut root = Trie::default(); // 根节点
        // 构建字典树
        for path in paths {
            let mut cur = &mut root;
            for node in path {
                cur = cur.children.entry(node.clone()).or_default();
            }
        }

        let mut freq = HashMap::new(); // 哈希表记录每一种序列化表示的出现次数

        // 基于深度优先搜索的后序遍历,计算每一个节点结构的序列化表示
        fn construct(node: &mut Trie, freq: &mut HashMap<String, usize>) {
            if node.children.is_empty() {
                return; // 如果是叶节点,无需操作
            }

            let mut v = Vec::new();
            for (folder, child) in node.children.iter_mut() {
                construct(child, freq);
                v.push(format!("{}({})", folder, child.serial));
            }

            v.sort();
            node.serial = v.join("");
            *freq.entry(node.serial.clone()).or_default() += 1;
        }
        construct(&mut root, &mut freq);
        let mut ans = Vec::new();
        let mut path = Vec::new();

        // 操作字典树,删除重复文件夹
        fn operate(node: &Trie, freq: &HashMap<String, usize>, path: &mut Vec<String>, ans: &mut Vec<Vec<String>>) {
            if freq.get(&node.serial).unwrap_or(&0) > &1 {
                return; // 如果序列化表示出现超过1次,需要删除
            }

            if !path.is_empty() {
                ans.push(path.clone());
            }

            for (folder, child) in &node.children {
                path.push(folder.clone());
                operate(child, freq, path, ans);
                path.pop();
            }
        }
        operate(&root, &freq, &mut path, &mut ans);

        ans
    }
}

编码错误卡很久

分析测试用例:

  • 输入:[["a"],["c"],["d"],["a","b"],["c","b"],["d","a"]]
  • 预期输出:[["d"],["d","a"]]

分析编码过程:

  1. ["a","b"]["c","b"] 都是叶子节点,编码为空字符串
  2. ["a"] 有子节点 ["b"],编码是 "()"(因为 ["b"] 的编码是空字符串)
  3. ["c"] 有子节点 ["b"],编码也是 "()"
  4. ["d","a"] 是叶子节点,编码为空字符串
  5. ["d"] 有子节点 ["a"],编码是 "()"(因为 ["a"] 的编码是空字符串)

所以 ["a"]["c"]["d"] 的编码都是 "()",它们被认为是重复的,都被删除了。

但是预期输出是 [["d"],["d","a"]],说明 ["d"] 应该保留。

问题在于:编码方式没有区分不同名字的子节点!

编码是:

1
child_encodings.push(format!("({})", child_encoding));

这样 ["a"]["c"] 的编码都是 "()",因为它们的子节点 ["b"] 的编码都是空字符串。

但是 ["d"] 的子节点是 ["a"]["a"] 的编码是 "()",所以 ["d"] 的编码也是 "()"

正确的编码应该包含子节点的名字:

1
child_encodings.push(format!("{}({})", child.name, child_encoding));

这样:

  • ["a"] 的编码是 "b()"(子节点名字是 “b”,编码是空字符串)
  • ["c"] 的编码是 "b()"(子节点名字是 “b”,编码是空字符串)
  • ["d"] 的编码是 "a()"(子节点名字是 “a”,编码是空字符串)

这样 ["a"]["c"] 的编码相同,会被删除,而 ["d"] 的编码不同,会被保留。

总结:编码方式缺少了子节点的名字信息,导致不同名字但结构相同的子树被误认为是相同的。

Review

【TED演讲】当计算机变得比我们更聪明时会发生什么

AI很强大,未来可能会强大到失控,所以应该提前让AI学习道德约束,控制好很多控制性问题,防止AI失控后毁灭人类。

Tips

链上皇0xSun推荐的币圈新闻推特

币圈相关新闻推特,最主流和权威的, 英文源DB @tier10k 和Tree @TreeNewsFeed , 中文源方程式 @bwenews , 虽然他们有时候也会出错,但是大新闻99%是这几家最先推送。

Share

Claude Code 的常见工作流程

实践证明: 学习 Claude 的常见工作流程,就算不用 Claude Code, 只 Cursor 调用 Claude 的 API 也可以使得使用效果大幅提升。