推荐系统多样性算法实战:从华为OD机试题到工业级重排序

📅 2026/7/9 23:10:58
推荐系统多样性算法实战:从华为OD机试题到工业级重排序
1. 项目概述从一道机试题看推荐系统的“多样性”实战最近在帮几个准备华为OD机试的朋友做模拟发现“推荐多样性”这个题目出现的频率不低而且往往被归在D卷或C卷的“中等偏难”档位。很多同学一看到“多样性算法”就觉得头大感觉是推荐系统里一个很玄乎的概念不知道从何下手。其实这道题目的核心远没有名字听起来那么复杂它本质上是一个带约束的排序与筛选问题考察的是你如何将业务逻辑多样性要求转化为清晰的程序逻辑和高效的数据结构操作。简单来说题目通常会给你一个待推荐的物品列表比如商品、视频、文章每个物品有它的基础得分比如点击率、购买率预测分同时属于一个或多个类别。如果一股脑只推荐得分最高的物品用户很快会感到单调比如全是手机壳推荐。因此需要引入“多样性”机制在最终的推荐列表中不允许连续出现N个比如2个或3个属于同一类别的物品。你的任务就是设计一个算法在满足这个硬性约束的前提下尽可能让列表的总得分最高或者说让高得分的物品尽可能靠前。这不仅仅是机试题目更是推荐系统在实际业务中信息流、电商、内容平台每天都要处理的核心问题之一。今天我就结合这道题目把多样性推荐的思路、几种核心解法的优劣、以及跨语言C/Java/JavaScript/Python/C的实现细节和避坑指南一次性讲透。无论你是为了备战机试还是想了解推荐系统的一个实用侧面这篇文章都能给你直接的参考。2. 核心思路拆解如何将业务问题转化为算法问题面对“推荐多样性”问题第一步也是最关键的一步是问题抽象与建模。我们不能被“算法”二字吓到而是要清晰地定义输入、输出和约束。2.1 输入输出与约束的形式化定义典型的输入格式如下items: 一个列表长度为 M。每个元素是一个对象或结构体包含id: 物品唯一标识。score: 物品的基础推荐得分通常是一个浮点数或整数。这是排序的首要依据。categories: 物品所属的类别标签列表。一个物品可以属于多个类别这是容易忽略的难点。N: 多样性约束值一个整数。表示在最终的推荐列表中不允许出现连续 N 个物品共享至少一个相同的类别。K: 最终需要输出的推荐列表长度。输出是一个长度为 K 的物品id列表满足上述多样性约束并且尽可能使列表的整体“质量”通常定义为物品得分的总和或序列的某种效用最高。约束的精确理解这里的“连续 N 个物品共享至少一个相同类别”是关键。例如 N2就意味着任何相邻的两个物品它们的所有类别交集必须为空。如果物品A属于类别[“科技”, “数码”]物品B属于类别[“数码”, “生活”]那么它们都含有“数码”类别因此不能相邻放置。2.2 算法设计核心思想贪心与回溯的权衡这个问题本质上是一个带约束的序列生成问题。最直观的暴力方法是枚举所有排列但复杂度是阶乘级的完全不可行。我们需要更聪明的策略。1. 基于优先队列的贪心算法最常用、最直观这是面试和机试中最期望看到的解法。思路是模拟“逐个选取”的过程数据结构使用一个最大堆优先队列始终维护当前可被选择的物品集合。可被选择的定义是选择它放入结果列表后不会立即违反多样性约束即不会与结果列表末尾的 N-1 个物品发生类别冲突。过程将所有物品按score从大到小放入一个大堆中。初始化一个空的结果列表result和一个“暂存区”temp用于存放因冲突暂时不能选的物品。循环直到result长度达到 K a. 从大堆中弹出堆顶当前得分最高的物品。 b.检查该物品是否与result末尾的 N-1 个物品有类别冲突 c. 如果无冲突将其加入result并将本轮暂存在temp中的所有物品重新加回大堆。清空temp。 d. 如果有冲突则将其放入temp暂存继续检查下一个堆顶物品。边界处理如果大堆被弹空但result长度未到 K且temp非空说明无法找到满足约束的下一个物品。此时一种策略是回溯从result末尾移除一个物品将其重新放入大堆同时需要调整冲突检查的历史状态然后连同temp里的物品一起重新尝试。另一种更简单的策略适用于某些题目要求是直接返回已找到的结果。为什么贪心是有效的在大多数情况下尤其是物品得分差异明显、类别分布较均匀时贪心算法能快速得到一个近似最优解。它保证了每一步都局部最优选当前能选的最好的虽然对于全局不一定绝对最优但效率和效果平衡得很好。2. 动态规划DP思路对于追求绝对最优解的场景可以考虑DP。定义dp[i][state]表示已经选择了 i 个物品且最近选择的 N-1 个物品的类别组合状态为state时能获得的最大总得分。state需要编码最近 N-1 个物品的所有类别信息这会导致状态空间非常庞大“类别”需要映射为整数状态数是类别总数的指数级仅当类别总数很少比如10且N较小时可行。在机试和实际工程中这种方法很少用。3. 搜索与剪枝类似于回溯法在贪心遇到死胡同时进行回溯。可以设定一个回溯深度当贪心失败时回退几步选择次优的物品然后继续贪心。这比纯贪心更鲁棒但代码复杂度更高。对于华为OD机试掌握贪心优先队列简单回溯或处理无解情况的思路并能够清晰实现就完全足够了。3. 关键实现细节与数据结构选择思路清晰后实现环节的“魔鬼”就藏在细节里。不同的语言细节处理各有不同。3.1 冲突检查的高效实现这是算法的性能瓶颈之一。最笨的方法是每次检查时遍历当前物品的类别列表与结果列表末尾每个物品的类别列表分别求交集。# 低效示例 (Python) def has_conflict(current_cats, last_items, N): for item in last_items: if set(current_cats) set(item.categories): return True return False当类别较多时集合操作开销不小。优化技巧利用位运算或哈希集合进行预计算和快速检查。位图法Bitmask如果总类别数不超过64或32取决于语言可以将每个物品的类别集合编码为一个整数bitmask。检查冲突时只需要进行按位与操作结果为0则表示无冲突。这是最高效的方法但受限于类别数量。// C 示例假设类别已映射为0~63的整数 long long current_mask item_mask[current_id]; long long last_mask 0; for (auto prev_item : last_items) { last_mask | item_mask[prev_item.id]; } if (current_mask last_mask) { // 有冲突 }哈希集合法更通用的方法。维护一个“最近 N-1 个物品所有类别的并集”的哈希集合unordered_setin C,HashSetin Java,Setin JS/Python。每次添加新物品到结果时更新这个集合每次从结果末尾移除物品时也需要更新移除该物品独有的类别。检查冲突时只需判断当前物品的类别是否有任何一个存在于这个哈希集合中O(1)平均时间复杂度。3.2 优先队列与自定义比较器我们需要一个能随时取出最大score物品的容器。各语言实现如下C:priority_queue默认最大堆。需要为自定义结构体重载运算符或提供自定义比较类Compare。struct Item { string id; double score; vectorstring categories; // 方法1重载 运算符注意priority_queue默认是less即大顶堆需要“”返回true表示优先级低 bool operator(const Item other) const { return score other.score; // 分数大的优先级高 } }; priority_queueItem pq; // 方法2使用lambda和decltype更灵活 auto cmp [](const Item a, const Item b) { return a.score b.score; }; priority_queueItem, vectorItem, decltype(cmp) pq(cmp);Java:PriorityQueue。需要传入一个自定义的Comparator。PriorityQueueItem pq new PriorityQueue((a, b) - Double.compare(b.score, a.score)); // 降序Python:heapq模块只提供最小堆。需要将score取负存入或使用heapq.heappushpop并自定义元组(-score, item)。import heapq heap [] heapq.heappush(heap, (-item.score, item)) # 取负号实现最大堆 _, top_item heapq.heappop(heap)JavaScript: 没有内置堆。可以自己实现一个简单的二叉堆类或者在机试环境下用一个数组每次排序效率低但代码简单对于数据量小的题目可能过关。C: 没有标准容器。需要自己用数组实现排序或简单的优先队列逻辑代码量会大一些。3.3 处理多类别与回溯多类别处理如前所述一个物品有多个类别。在冲突检查时只要当前物品的任意一个类别与历史窗口中的任意一个物品的任意一个类别相同即视为冲突。在实现哈希集合法时直接将该物品的所有类别加入历史类别集合进行判断即可。回溯的实现当贪心算法“卡住”堆空但结果未满时简单的回溯策略是如果结果列表result为空说明无解直接返回。否则从result末尾弹出一个物品last_item。将last_item重新放回优先队列注意它的得分没变依然有资格参与后续竞争。同时需要更新“最近类别集合”移除last_item的类别注意只有当这些类别在最近窗口的其他物品中也不存在时才能移除所以维护一个类别计数器更稳妥。将暂存区temp里的物品也重新加回优先队列。然后继续贪心流程。注意回溯可能会陷入循环或性能问题。在实际机试中题目设计往往能保证贪心算法有解或者明确要求输出“能找到的最长无冲突序列”。因此实现时可以先不写复杂的回溯而是处理无解情况如返回已构建的部分列表并根据题目要求调整。4. 跨语言代码实现与对比解析下面我将给出五种语言的核心解法框架。为了聚焦算法逻辑假设输入已解析为合适的数据结构并简化了回溯逻辑采用“无法扩展则终止”的策略。4.1 C 实现强调效率与控制C的实现可以利用STL容器的高效和位运算的便捷如果类别可映射。#include iostream #include vector #include queue #include unordered_set #include string #include algorithm using namespace std; struct Item { string id; double score; vectorstring categories; // 为了哈希集合比较方便也可以存一个类别字符串拼接的签名但这里直接用vector }; vectorstring diverseRecommendation(vectorItem items, int N, int K) { vectorstring result; if (items.empty() || K 0) return result; // 最大堆按score降序 auto cmp [](const Item* a, const Item* b) { return a-score b-score; }; priority_queueItem*, vectorItem*, decltype(cmp) pq(cmp); for (auto item : items) { pq.push(item); } // 记录最近 N-1 个物品的所有类别 vectorItem* lastItems; unordered_setstring recentCategories; while (result.size() K !pq.empty()) { vectorItem* temp; // 暂存因冲突无法立即选择的物品 bool found false; while (!pq.empty() !found) { Item* cur pq.top(); pq.pop(); // 检查冲突当前物品的类别是否在 recentCategories 中 bool conflict false; for (const string cat : cur-categories) { if (recentCategories.find(cat) ! recentCategories.end()) { conflict true; break; } } if (!conflict) { // 选择该物品 result.push_back(cur-id); lastItems.push_back(cur); // 更新最近类别集合 for (const string cat : cur-categories) { recentCategories.insert(cat); } // 维护窗口大小不超过 N-1 if (lastItems.size() N) { Item* toRemove lastItems[lastItems.size() - N]; // 注意不能直接删除toRemove的所有类别可能其他窗口内物品也含有 // 需要更精细的计数管理这里简化处理仅适用于N2或特定情况 // 更健壮的做法是为每个类别维护一个计数器 for (const string cat : toRemove-categories) { // 假设需要实现引用计数这里省略... // recentCategories.erase(cat); // 简单直接删除可能出错 } } // 将暂存物品重新入堆 for (Item* it : temp) { pq.push(it); } found true; break; } else { // 冲突暂存 temp.push_back(cur); } } if (!found) { // 堆已空且未找到合适物品贪心失败尝试简单回溯或终止 // 这里选择终止返回已找到的结果 break; } } // 简化处理窗口类别集合的维护在N2时需用mapstring, int计数 // 完整实现需要考虑当窗口滑动时减少类别计数计数为0时才从集合删除 return result; }C实现要点使用指针Item*入堆避免在堆中拷贝整个Item对象。冲突检查使用unordered_set平均O(1)。难点滑动窗口的类别集合维护。当N2时从窗口移除一个物品时不能直接将其类别从集合中删除因为窗口内其他物品可能也有相同类别。正确做法是维护一个unordered_mapstring, int作为类别计数器添加时计数1移除时计数-1减到0时才从集合中删除。这是面试官可能追问的细节。4.2 Java 实现面向对象与健壮性Java的实现更注重清晰和健壮性利用其丰富的集合框架。import java.util.*; class Item { String id; double score; ListString categories; // 构造器、getter/setter省略 } public class DiverseRecommender { public ListString diverseRecommendation(ListItem items, int N, int K) { ListString result new ArrayList(); if (items null || items.isEmpty() || K 0) { return result; } // 最大堆 PriorityQueueItem pq new PriorityQueue((a, b) - Double.compare(b.score, a.score)); pq.addAll(items); // 滑动窗口及类别计数器 LinkedListItem window new LinkedList(); MapString, Integer categoryCount new HashMap(); while (result.size() K !pq.isEmpty()) { ListItem temp new ArrayList(); boolean found false; while (!pq.isEmpty() !found) { Item cur pq.poll(); // 检查冲突 boolean conflict false; for (String cat : cur.categories) { if (categoryCount.getOrDefault(cat, 0) 0) { conflict true; break; } } if (!conflict) { // 选中 result.add(cur.id); window.add(cur); // 更新类别计数器 for (String cat : cur.categories) { categoryCount.put(cat, categoryCount.getOrDefault(cat, 0) 1); } // 维护窗口大小不超过 N-1 if (window.size() N) { Item toRemove window.removeFirst(); for (String cat : toRemove.categories) { int count categoryCount.get(cat) - 1; if (count 0) { categoryCount.remove(cat); } else { categoryCount.put(cat, count); } } } // 暂存物品重新入队 for (Item it : temp) { pq.offer(it); } found true; } else { temp.add(cur); } } if (!found) { // 无法找到合适物品终止 break; } } return result; } }Java实现要点使用PriorityQueue并传入自定义比较器实现最大堆。使用LinkedList作为滑动窗口方便在头部移除元素。使用HashMapString, Integer作为类别计数器这是处理滑动窗口类别维护的正确且通用的方法比C示例中的简化版更健壮。Java的垃圾回收机制使得我们不需要太关注对象拷贝代码更简洁。4.3 Python 实现简洁与开发效率Python代码以其简洁著称非常适合快速原型和算法表达。import heapq from collections import deque, defaultdict from typing import List class Item: def __init__(self, id: str, score: float, categories: List[str]): self.id id self.score score self.categories categories def diverse_recommendation(items: List[Item], N: int, K: int) - List[str]: result [] if not items or K 0: return result # 最大堆通过分数取负实现 heap [(-item.score, idx, item) for idx, item in enumerate(items)] # 加入idx避免比较Item对象 heapq.heapify(heap) window deque() # 滑动窗口存放Item对象 cat_counter defaultdict(int) # 类别计数器 while len(result) K and heap: temp [] found False while heap and not found: neg_score, _, cur_item heapq.heappop(heap) # 弹出得分最高的取负后最小 # 检查冲突 conflict any(cat_counter.get(cat, 0) 0 for cat in cur_item.categories) if not conflict: result.append(cur_item.id) window.append(cur_item) for cat in cur_item.categories: cat_counter[cat] 1 # 维护窗口大小 if len(window) N: removed_item window.popleft() for cat in removed_item.categories: cat_counter[cat] - 1 if cat_counter[cat] 0: del cat_counter[cat] # 将暂存物品重新入堆 for neg_s, idx, it in temp: heapq.heappush(heap, (neg_s, idx, it)) found True else: temp.append((neg_score, idx, cur_item)) # 暂存idx沿用之前的 if not found: break return resultPython实现要点heapq是最小堆通过存储(-score, ...)来实现最大堆逻辑。在堆中存储了(neg_score, idx, item)元组。idx是关键因为heapq在元组第一个元素相等时会比较第二个元素。如果第二个元素是Item对象且没有定义比较方法会报错。加入一个唯一索引idx可以避免此问题并保证排序稳定性。使用collections.deque作为滑动窗口popleft()操作是O(1)。使用collections.defaultdict(int)作为类别计数器代码非常简洁。any()函数和生成器表达式让冲突检查的代码一目了然。4.4 JavaScript 实现灵活与前端思维JavaScript在OD机试中也可能出现尤其在Web相关的岗位。这里给出一种实现。function diverseRecommendation(items, N, K) { const result []; if (!items || items.length 0 || K 0) { return result; } // 手动实现一个简单最大堆或使用数组排序模拟 // 为简化这里使用数组排序模拟优先队列注意效率不高仅适用于小数据量演示 let heap [...items].sort((a, b) b.score - a.score); const window []; // 滑动窗口 const catCounter new Map(); // 类别计数器 while (result.length K heap.length 0) { const temp []; let found false; while (heap.length 0 !found) { const curItem heap.shift(); // 取出“堆顶”数组第一个 // 检查冲突 let conflict false; for (const cat of curItem.categories) { if (catCounter.has(cat) catCounter.get(cat) 0) { conflict true; break; } } if (!conflict) { result.push(curItem.id); window.push(curItem); for (const cat of curItem.categories) { catCounter.set(cat, (catCounter.get(cat) || 0) 1); } // 维护窗口大小 if (window.length N) { const removedItem window.shift(); for (const cat of removedItem.categories) { const count catCounter.get(cat) - 1; if (count 0) { catCounter.delete(cat); } else { catCounter.set(cat, count); } } } // 重新加入暂存物品 heap.push(...temp); heap.sort((a, b) b.score - a.score); // 重新排序 found true; } else { temp.push(curItem); } } if (!found) { break; } } return result; }JavaScript实现要点没有内置堆这里用数组sort模拟每次操作后重新排序时间复杂度为O(M log M)在M不大时可以接受。在正式面试或对性能有要求时需要自己实现一个二叉堆类。使用Map对象作为类别计数器比普通对象更合适键可以是字符串。使用数组的shift()和push()模拟队列但注意shift()在数组开头删除元素是O(n)操作。对于性能敏感场景可以自己维护头尾指针或使用链表。代码逻辑与其他语言版本基本一致体现了算法的语言无关性。4.5 C 语言实现底层与性能控制C语言的实现最能体现对内存和底层数据结构的掌控虽然代码量最大。#include stdio.h #include stdlib.h #include string.h #include stdbool.h #define MAX_CAT_PER_ITEM 10 #define MAX_CAT_NAME_LEN 50 typedef struct { char id[20]; double score; char categories[MAX_CAT_PER_ITEM][MAX_CAT_NAME_LEN]; int cat_count; } Item; typedef struct { Item* items; int size; int capacity; } MaxHeap; // 简单的字符串哈希函数用于类别快速查找这里简化实际可能需要更复杂的结构 // 由于C没有内置集合实现完整的滑动窗口和类别计数管理非常复杂。 // 以下仅勾勒核心贪心循环框架省略了堆、哈希表等完整实现细节。 // 假设已实现MaxHeap* createHeap(int capacity); void push(MaxHeap* heap, Item item); Item pop(MaxHeap* heap); // 假设已实现哈希表用于categoryCount (key: char*, value: int*) Item* diverse_recommendation(Item* items, int m, int N, int K, int* return_size) { *return_size 0; if (m 0 || K 0) return NULL; // 1. 构建最大堆按score MaxHeap* heap createHeap(m); for (int i 0; i m; i) { push(heap, items[i]); } // 2. 初始化结果列表、窗口、类别计数器这里需要自己实现动态数组和哈希表 Item* result (Item*)malloc(K * sizeof(Item)); Item* window (Item*)malloc((N-1) * sizeof(Item)); // 循环队列 int front 0, rear 0; // HashTable* catCounter createHashTable(); // 3. 贪心选择循环伪代码逻辑 while ((*return_size) K heap-size 0) { Item temp[100]; // 简化固定大小暂存区 int temp_size 0; bool found false; while (heap-size 0 !found) { Item cur pop(heap); // 检查冲突遍历cur.categories查询catCounter bool conflict false; for (int i 0; i cur.cat_count; i) { // if (hashTableGet(catCounter, cur.categories[i]) 0) { conflict true; break; } } if (!conflict) { // 加入结果 result[*return_size] cur; (*return_size); // 加入窗口更新catCounter // ... // 维护窗口大小 // ... // 将temp中物品重新入堆 for (int i 0; i temp_size; i) { push(heap, temp[i]); } found true; } else { temp[temp_size] cur; } } if (!found) { break; } } // 清理内存... return result; }C语言实现要点需要手动管理所有内存结果数组、堆、窗口、哈希表等。需要自己实现最大堆、哈希表或使用第三方库如uthash、动态数组等基础数据结构。这是最大的挑战。代码冗长但性能可控。在OD机试中除非岗位要求或题目明确否则一般不会要求用C实现如此复杂的数据结构组合。更可能考察的是算法思路的表述。如果必须用C一个取巧的办法是固定最大物品数和类别数用二维数组和简单排序来模拟但会丧失通用性。5. 常见陷阱、优化与面试延伸问题在实际编码和面试中以下几个点是容易出错和被追问的地方。5.1 高频陷阱与避坑指南忽略物品的多类别属性这是最常见的错误。冲突检查必须是“类别集合是否存在交集”而不是“类别是否完全相同”。务必遍历所有类别。滑动窗口的类别管理错误当N2时不能简单地用一个set来存最近N-1个物品的类别并直接删除。必须使用mapstring, int进行引用计数。这是区分是否考虑周全的关键点。优先队列的比较函数错误在C、Java、Python中构建最大堆的方式各有不同写错比较函数会导致排序错误。务必测试基础用例。Python中堆的元素比较问题如前所述向heapq中放入复杂对象时需要确保对象可比较或者通过添加索引idx作为元组的第二个元素来避免比较对象本身。贪心算法的无解处理题目不一定保证总能选出K个物品。你的代码是应该返回空列表、已选列表还是进行回溯务必仔细阅读题目输出说明。一个健壮的程序应该能处理无解情况。性能问题在JavaScript中用数组shift()、unshift()或在Python中用list.pop(0)模拟队列在数据量大时会是性能瓶颈。应使用dequePython或自己维护指针JS/C。5.2 算法优化方向如果题目数据量极大M 10^5上述O(M log M)的贪心算法可能仍需优化冲突检查优化如果类别总数有限且较少如64强烈推荐使用位运算Bitmask。将每个物品的类别集合编码为一个long longC/Java或intPython的位图。检查冲突时只需将当前物品的位图与“窗口类别位图”进行按位与结果为0则无冲突。更新窗口位图时使用按位或|和按位异或^需配合计数不能简单异或。这能将冲突检查从O(C)降到O(1)。堆的优化在贪心过程中大量物品因冲突被暂存后又重新入堆。可以思考是否有更高效的数据结构能同时维护“按得分排序”和“按类别过滤”的信息但这通常很复杂。贪心算法本身的复杂度在大多数情况下是可接受的。5.3 面试官可能追问的问题你的算法一定能得到最优解吗不能贪心算法是启发式的可能得到的是近似最优解。可以构造反例比如有三个物品(A, 10分, [科技]), (B, 9分, [科技]), (C, 8分, [数码])N2K2。贪心会选A然后因为冲突不能选B只能选C得到[A, C]总分18。但最优解是[B, A]总分19先选B再选A。此时可以讨论回溯或动态规划的必要性。如果物品的得分是动态更新的怎么办这涉及到在线推荐场景。我们可以考虑使用“重排序”策略定期或每收到一批新物品运行一次本算法对候选列表进行多样性重排。或者使用Bandit等在线学习算法在探索多样性和利用得分之间平衡。除了连续N个不重复还有别的多样性衡量方式吗当然有比如类别比例控制最终列表中各类别的比例不能超过某个阈值。整体多样性最大化使得列表中所有物品两两之间的类别差异总和最大类似最大熵。基于用户历史避免推荐与用户近期已交互物品类别重复的。 这些都需要不同的建模方法如整数规划、贪心权重调整等。6. 从机试题到工程实践这道“推荐多样性”机试题是工业界推荐系统重排序阶段的一个高度简化模型。在实际的推荐系统中流程通常是召回从海量物品中快速筛选出几百个可能相关的候选如基于协同过滤、Embedding向量检索。精排用复杂的机器学习模型如DeepFM、DIN对这几百个候选物品进行精准打分score。重排序对精排后的有序列表进行“纠偏”引入多样性、新鲜度、商业规则等约束生成最终展示给用户的列表。本文讨论的算法正是重排序阶段的一个核心策略。在工程实践中问题会更复杂打分维度多元score可能不是单一值而是多个目标点击率、点赞率、时长的加权或模型预估的综合效用。约束复杂除了类别多样性还可能有多样性作者、来源、打散同一作者作品间隔、业务保量某些类别必须出现等混合约束。性能要求极高需要在几十毫秒内完成对上百个物品的排序。在线学习需要根据用户实时反馈调整策略。因此常用的工业级解法包括基于规则的贪心打散类似本文算法但规则更复杂。基于DP或搜索的优化在约束不多时可以用DP求得精确最优。基于强化学习的序列生成将列表生成视为一个序列决策过程用强化学习来优化长期用户满意度。开源工具像Google的OR-Tools这样的优化求解库可以方便地定义约束并求解。所以当你熟练掌握了这道机试题背后的贪心、队列、哈希计数、回溯等技巧你不仅能够通过考试更是摸到了推荐系统实战中一扇重要的大门。下次再看到信息流里琳琅满目又不显重复的内容或许你就能会心一笑知道背后是怎样的代码在支撑着这份“恰到好处的丰富”。