知识图谱驱动的知识点关联:从孤立的题目到网络化的学习路径
知识图谱驱动的知识点关联:从孤立的题目到网络化的学习路径
一、一道题就是一棵知识树上的一个节点
刷了 200 道 LeetCode 后,我发现一个规律:真正决定解题能力的不是"刷了多少题",而是在大脑中建立了怎样的知识关联。"两数之和"用哈希表,"三数之和"用排序+双指针——二者同属数组问题,但解题思路完全不同。如果系统只能告诉你"先刷 A 再刷 B",却说不清 A 和 B 为什么关联,那它就是一本"目录",而不是一位"导师"。
知识图谱把题目、知识点、前置关系组织成一个有向图。有了这张图,系统可以回答:"这道题涉及哪些知识点,前置是什么,学会之后可以解锁哪些题,哪些题跟它思路相似但角度不同。"
flowchart LR A[数组基础] --> B[双指针] A --> C[哈希表] B --> D[滑动窗口] B --> E[三数之和] C --> F[两数之和] C --> G[字母异位词分组] D --> H[最小覆盖子串] E --> I[四数之和] B --> J[排序+双指针] C --> K[前缀和+哈希表] style A fill:#ccf style B fill:#cfc style C fill:#cfc二、知识图谱的三层结构
知识点层(Node):每个知识点是一个节点,包含名称、难度权重、前置知识点、后继知识点。如"双指针"前置是"数组基础",后继是"滑动窗口"。
题目层(Problem):每道题关联 1-3 个知识点,形成"题→知识点"的多对多关系。题目的难度和知识点的难度权重共同决定推荐的梯度。
路径层(Path):两个知识点之间有向边权重表示"从 A 到 B 的学习转移成本"。权重 = N 道题目中 A 和 B 同时出现的次数 / 所有题目的平均关联数。
三、知识图谱构建与学习路径生成的实现
""" 知识图谱驱动的学习路径生成 功能:构建知识点图 → BFS 生成学习路径 → 推荐下一个知识点 """ from typing import List, Dict, Set, Tuple, Optional from dataclasses import dataclass, field from collections import defaultdict, deque import math @dataclass class KnowledgePoint: """知识点节点""" kp_id: str name: str difficulty: float # 0-1 category: str # "data_structure", "algorithm", "math" description: str = "" @dataclass class Problem: """题目节点""" problem_id: int title: str difficulty: float knowledge_points: List[str] # 关联的知识点 ID acceptance_rate: float = 0.5 class KnowledgeGraph: """知识点知识图谱""" def __init__(self): self.knowledge_points: Dict[str, KnowledgePoint] = {} self.problems: Dict[int, Problem] = {} # 邻接表:kp_id → {next_kp_id: weight} self.graph: Dict[str, Dict[str, float]] = defaultdict(dict) # 前置关系(有向边) self.prerequisites: Dict[str, List[str]] = defaultdict(list) # 题目→知识点的反向索引 self.kp_to_problems: Dict[str, List[int]] = defaultdict(list) def add_kp(self, kp: KnowledgePoint): """添加知识点""" self.knowledge_points[kp.kp_id] = kp def add_problem(self, problem: Problem): """添加题目,自动建立题目→知识点关联""" self.problems[problem.problem_id] = problem for kp_id in problem.knowledge_points: self.kp_to_problems[kp_id].append(problem.problem_id) def add_edge(self, from_kp: str, to_kp: str): """添加知识点之间的前置关系""" self.prerequisites[to_kp].append(from_kp) # 更新图权重 self.graph[from_kp][to_kp] = ( self.graph[from_kp].get(to_kp, 0) + 1.0 ) def build_from_problems(self): """从题目数据自动构建知识点关联""" # 如果两道题共享知识点,在这些知识点之间建立边 for pid_a, pa in self.problems.items(): for pid_b, pb in self.problems.items(): if pid_a >= pid_b: continue shared = set(pa.knowledge_points) & set(pb.knowledge_points) for kp in shared: for other_kp in set(pa.knowledge_points) | set(pb.knowledge_points): if other_kp != kp: self.add_edge(kp, other_kp) def get_learning_path(self, start_kp: str, target_kp: Optional[str] = None, max_depth: int = 5) -> List[str]: """ BFS 生成学习路径 从 start_kp 出发,按拓扑顺序遍历 """ if start_kp not in self.knowledge_points: return [] visited = {start_kp} queue = deque([(start_kp, [start_kp])]) while queue: current, path = queue.popleft() # 如果指定了目标,找到就返回 if target_kp and current == target_kp: return path if len(path) >= max_depth: continue # 按权重排序相邻节点 neighbors = sorted( self.graph.get(current, {}).items(), key=lambda x: x[1], reverse=True ) for next_kp, weight in neighbors: if next_kp not in visited: visited.add(next_kp) queue.append((next_kp, path + [next_kp])) return [] # 未找到路径 def recommend_next_kp(self, mastered: List[str], top_k: int = 5) -> List[Tuple[str, float]]: """ 推荐下一个知识点:BFS + 难度过滤 """ candidates = defaultdict(float) for kp_id in mastered: if kp_id not in self.graph: continue for next_kp, weight in self.graph[kp_id].items(): if next_kp in mastered: continue # 已掌握,跳过 # 检查前置是否满足 prereqs = self.prerequisites.get(next_kp, []) if not all(p in mastered for p in prereqs): continue # 前置未满足 # 分数 = 关联权重 + 难度适配 kp = self.knowledge_points.get(next_kp) if kp: candidates[next_kp] += weight * (1 - kp.difficulty * 0.3) sorted_candidates = sorted( candidates.items(), key=lambda x: x[1], reverse=True ) return sorted_candidates[:top_k] def get_review_candidates(self, mastered: List[str], decay_days: Dict[str, int], top_k: int = 5) -> List[str]: """ 根据艾宾浩斯遗忘曲线推荐复习知识点 遗忘率 = 1 - e^(-t/S),其中 S 是知识点稳定性 """ candidates = [] for kp_id in mastered: kp = self.knowledge_points.get(kp_id) if not kp: continue days = decay_days.get(kp_id, 0) stability = 7 * (1 - kp.difficulty) + 3 forgetting_rate = 1 - math.exp(-days / stability) if forgetting_rate > 0.5: candidates.append((kp_id, forgetting_rate)) candidates.sort(key=lambda x: x[1], reverse=True) return [kp_id for kp_id, _ in candidates[:top_k]] if __name__ == "__main__": kg = KnowledgeGraph() kps = [ KnowledgePoint("array", "数组基础", 0.1, "data_structure"), KnowledgePoint("hash", "哈希表", 0.3, "data_structure"), KnowledgePoint("two_pointers", "双指针", 0.4, "algorithm"), KnowledgePoint("sliding_window", "滑动窗口", 0.5, "algorithm"), KnowledgePoint("sort", "排序", 0.35, "algorithm"), KnowledgePoint("prefix_sum", "前缀和", 0.45, "algorithm"), ] for kp in kps: kg.add_kp(kp) kg.add_problem(Problem(1, "两数之和", 0.2, ["array", "hash"])) kg.add_problem(Problem(2, "三数之和", 0.4, ["array", "two_pointers", "sort"])) kg.add_problem(Problem(3, "最小覆盖子串", 0.6, ["sliding_window", "hash"])) kg.build_from_problems() path = kg.get_learning_path("array", "sliding_window") print(f"学习路径 array→sliding_window: {path}") recs = kg.recommend_next_kp(["array", "hash"]) print(f"掌握 [array, hash] 后推荐: {recs}") review = kg.get_review_candidates( ["array", "hash", "two_pointers"], {"array": 10, "hash": 3, "two_pointers": 1}, ) print(f"需要复习: {review}")四、知识图谱的构建难点
自动抽取 vs 人工标注:题目知识点的标注目前主要依赖人工(LeetCode 标签),准确率约 90%。自动抽取(NLP 从题解中提取)的准确率约 75%。主流程用人工标注 + 自动补全的方案。
知识点粒度的权衡:太细(每个具体算法是一个知识点)→ 图谱过于稀疏,学习路径碎片化。太粗("数组"包含一切)→ 推荐不够精准。工程上用三层粒度:大类(数据结构/算法/数学)、中类(数组/链表/树)、细类(双指针/滑动窗口/前缀和)。
图谱演化:新的算法和题型不断出现,知识图谱需要持续更新。方案:每周自动从新增的题解数据中提取新知识点候选,人工审核后加入图谱。
五、总结
- 知识图谱把"刷题"变成"学习路径":不是推荐一道题,而是推荐一条有向学习路径。
- BFS + 前置关系确保路径可行:不推荐前置未掌握的题。
- 遗忘曲线驱动复习计划:系统知道哪些知识点需要复习,而不是用户自己猜。
- 图谱的维护成本高于构建成本:持续的标注审核是质量保障。
知识图谱是刷题系统从"工具"到"导师"转变的关键基础设施。它让用户从"盲目刷题"变成了"有策略地学习"。
本文实现了知识点图谱的构建、BFS 学习路径生成、前置约束推荐和遗忘曲线复习四个核心模块。KnowledgeGraph 类可直接作为学习路径推荐的基础组件。
