下面是一段用贪心算法做搜索推荐的 Python 示例。
场景:用户输入搜索问题,系统从候选内容中推荐最相关的若干条。贪心策略是:每一步都从剩余候选里选"当前得分最高且满足约束"的内容,同时考虑相关度、质量、新鲜度,以及推荐结果之间的多样性。
```python
import re
from dataclasses import dataclass
from typing import List, Set
@dataclass
class Candidate:
id: int
title: str
content: str
tags: Liststr
quality: float = 0.8 # 内容质量分
freshness: float = 0.8 # 新鲜度
click_rate: float = 0.5 # 历史点击率
length: int = 100 # 内容长度,可用于控制总推荐长度
def tokenize(text: str) -> Setstr:
"""
简单分词:
-
英文/数字按单词切分
-
中文按连续片段 + 2-gram 切分
不依赖 jieba,方便直接运行。
"""
text = text.lower()
tokens = set(re.findall(r"a-z0-9+", text))
chinese_seqs = re.findall(r"\\u4e00-\\u9fa5+", text)
for seq in chinese_seqs:
if len(seq) == 1:
tokens.add(seq)
else:
tokens.add(seq)
for i in range(len(seq) - 1):
tokens.add(seqi:i + 2)
return tokens
def jaccard(a: Setstr, b: Setstr) -> float:
"""计算两个集合的 Jaccard 相似度"""
if not a or not b:
return 0.0
return len(a & b) / len(a | b)
def base_score(query_tokens: Setstr, cand: Candidate) -> float:
"""
计算候选内容的基础得分:
相关性 + 质量 + 新鲜度 + 点击率
"""
cand_text = cand.title + " " + cand.content + " " + " ".join(cand.tags)
cand_tokens = tokenize(cand_text)
relevance = jaccard(query_tokens, cand_tokens)
标签命中额外加权
tag_tokens = tokenize(" ".join(cand.tags))
tag_relevance = jaccard(query_tokens, tag_tokens)
relevance = max(relevance, tag_relevance)
score = (
0.55 * relevance +
0.20 * cand.quality +
0.15 * cand.freshness +
0.10 * cand.click_rate
)
return score
def greedy_recommend(
query: str,
candidates: ListCandidate,
top_k: int = 5,
max_similarity: float = 0.7, # 与已选内容标签相似度超过该值则跳过
diversity_penalty: float = 0.3, # 相似度带来的得分惩罚
max_total_length: int = 1000 # 推荐内容总长度限制
) -> ListCandidate:
"""
贪心推荐:
每一步从剩余候选中选择当前得分最高、且满足多样性/长度约束的内容。
"""
query_tokens = tokenize(query)
selected: ListCandidate = \[\]
selected_tag_sets: ListSet\[str] = \[\]
total_length = 0
预计算基础得分
remaining = (base_score(query_tokens, c), c) for c in candidates
while len(selected) < top_k and remaining:
best_idx = -1
best_score = -1.0
best_cand = None
for i, (base_s, cand) in enumerate(remaining):
cand_tags = tokenize(" ".join(cand.tags))
计算与已选内容的最大标签相似度
max_overlap = 0.0
for tag_set in selected_tag_sets:
max_overlap = max(max_overlap, jaccard(tag_set, cand_tags))
多样性约束:太相似就不选
if selected and max_overlap > max_similarity:
continue
长度约束
if total_length + cand.length > max_total_length:
continue
当前得分 = 基础得分 - 多样性惩罚
current_score = base_s - diversity_penalty * max_overlap
if current_score > best_score:
best_score = current_score
best_idx = i
best_cand = cand
没有满足约束的候选,结束
if best_idx == -1:
break
选中当前最优
selected.append(best_cand)
selected_tag_sets.append(tokenize(" ".join(best_cand.tags)))
total_length += best_cand.length
remaining.pop(best_idx)
return selected
if name == "main":
candidates = [
Candidate(1, "贪心算法入门", "贪心算法是一种局部最优策略...", "算法", "贪心算法", "基础", 0.90, 0.70, 0.60, 120),
Candidate(2, "活动选择问题详解", "用贪心算法解决活动选择...", "贪心算法", "活动选择", "区间调度", 0.85, 0.80, 0.70, 150),
Candidate(3, "动态规划与贪心算法区别", "对比动态规划和贪心算法...", "算法", "动态规划", "贪心算法", 0.88, 0.60, 0.65, 180),
Candidate(4, "霍夫曼编码实现", "霍夫曼编码的贪心构造...", "贪心算法", "霍夫曼编码", "压缩", 0.80, 0.75, 0.55, 200),
Candidate(5, "Dijkstra 最短路", "Dijkstra 算法是贪心思想...", "贪心算法", "图论", "最短路", 0.90, 0.65, 0.75, 160),
Candidate(6, "0/1 背包问题", "0/1 背包不能用贪心...", "动态规划", "背包", 0.95, 0.90, 0.80, 140),
Candidate(7, "分数背包贪心解法", "分数背包可以按单位价值贪心...", "贪心算法", "背包", "分数背包", 0.82, 0.70, 0.60, 130),
Candidate(8, "贪心算法常见错误", "贪心算法不是万能的...", "贪心算法", "算法", "反例", 0.87, 0.80, 0.72, 110),
]
query = "贪心算法 活动选择"
recs = greedy_recommend(query, candidates, top_k=4, max_total_length=600)
print(f"查询:{query}\n推荐结果:")
for i, c in enumerate(recs, 1):
print(f"{i}. {c.title} | 标签:{', '.join(c.tags)} | 质量:{c.quality} | 长度:{c.length}")
```
这段代码的核心贪心逻辑在 greedy_recommend 里:
-
先计算每个候选内容的基础得分;
-
每一步遍历剩余候选;
-
计算它和已选内容的标签相似度,并做多样性惩罚;
-
选择当前得分最高、且满足相似度阈值和长度限制的内容;
-
加入推荐列表,重复直到选够 top_k 或没有可用候选。
你可以根据实际业务调整这些参数:
· top_k:推荐几条;
· max_similarity:控制内容多样性;
· diversity_penalty:相似内容的降权幅度;
· max_total_length:控制推荐总长度;
· base_score 里的权重:相关性、质量、新鲜度、点击率各占多少。
文章仅供参考用。