zoukankan      html  css  js  c++  java
  • 推荐算法相关

    推荐算法相关

    推荐系统介绍

    What:分类目录、搜索引擎、推荐系统

    Why:需要在信息过载、用户需求不明确的背景下,留住用户和内容生产者,实现商业目标

    评估指标

    • 准确性

      • 学界:RMSE、MAE、Recall、Precision
      • 业界
        • 头条:阅读 - 阅读时长 - 分享/评论 - 关注/付费
        • 快手:播放 - 播放完成 - 点赞/评论 - 关注/跟拍
        • 淘宝:点击 - 浏览深度 - 加购/收藏 - 购买 - 重购/好评
    • 覆盖率

      • 面积覆盖率、信息熵、基尼系数、分层流量比
    • 多样性(推荐列表中两两物品的不相似性)、新颖性(未曾关注的类别、作者;推荐结果的平均流行度)、惊喜度(历史不相似(惊)但很满意(喜))

      EE:exploitation(选择当前最佳)和exploration(选择效果不确定的)的权衡

      • Bandit算法(了解)
        • Epsilon-Greedy:以1-epsilon的概率进行exploitation, 以epsilon的概率随机选取一个方案。发现最佳方案的大致“方向”,epsilon逐渐减小。
        • Upper Confidence Bound:均值越大,标准差越小,被选中的概率会越来越大 。(ar{x}_j(t)+sqrt{2ln t/T_{j,t}}),其中t代表次数,T代表第j方案被选择了的次数。结果几乎一样,收敛慢
        • Thompson Sampling:每个方案维护一个beta(good, bad)分布, 每次用现有的beta分布产⽣一个随机数,选择随机数最⼤的方案。结果有一定随机性,计算量小,收敛快
      • Bandit算法应用(了解)
        • 兴趣探索、冷启动探索
        • LinUCB:加入特征信息,用User和Item的特征预估回报及其置信区间,选择置信区间上界最大的Item推荐,观察回报后更新线性关系的参数
        • COFIBA:bandit结合协同过滤
      • EE的实践
        • 兴趣扩展:相似话题,搭配推荐
        • 人群算法:userCF、用户聚类
        • Bandit算法
        • graph walking
        • 平衡个性化推荐和热门推荐比例
        • 随机丢弃用户行为历史
        • 随机扰动模型参数
    • 商业目标、用户留存、满意度(总体)、信任度、实时性:

      • 探索伤害用户体验,可能导致用户流失
      • 探索带来的长期收益(留存率)评估周期长,KPI压力大
      • 如何平衡实时兴趣和长期兴趣?
      • 如何平衡短期产品体验和长期系统生态?
      • 如何平衡大众又味和小众需求?
      • 如何避免劣币趋势良币?
    • 鲁棒性、可扩展性:技术问题

    评估方法

    • 问卷调查:成本高
    • 离线评估:
      • 流程:利用某段时间的数据进行训练,在其他时间段数据进行测试,比较当前算法和新算法中点击/推荐比例的大小。
      • 优缺点
        • 只能在用户看到过的候选集上做评估,且跟线上真实效果存在偏差
        • 只能评估少数指标
        • 速度快,不损害用户体验
    • 在线评估:A/B testing
      • 流程:定义指标(不同业务适用不同指标),分出部分user用于测试。

    实践:离线评估和在线评估相结合,定期做问卷调查

    推荐系统发展

    • 1.0:关联规则、热门推荐等统计方法
    • 2.0(2006):矩阵分解等ML方法,离线计算推荐列表
    • 3.0(2010):召回 + learning to rank重排序
    • 4.0(2013):召回&排序实时化
    • 5.0(2016):end2end深度学习,一切皆embedding
    • 6.0:智能化推荐系统

    推荐系统架构:收集 - 学习 - 服务 - 反馈 - 收集

    Web Api -

    Match:

    • 各种算法计算各自的topk,各自的k总和由rank决定,k的比例根据学习得到。这样能减少rank层的压力
    • 把所有推荐在rank层合并筛选

    Rank:

    • 提取match返回item和user的特征(与模型特征一致),打分系统根据这些特征结合模型得出分数。

    相关算法

    • 基于领域:
      • item-CF,根据user对item的联系得出item的相似度矩阵,根据用户点击过的item的相似item完成推荐;user-CF,根据item对user的联系得出user的相似度矩阵,根据相似用户的点击完成推荐
      • LFM:用户-物品矩阵,通过矩阵分解得出user和item的隐向量,将两个向量的点乘topk得出推荐
      • personal rank:根据用户与商品的二分图中的随机游走得出用户对物品的倾向度PR。PR的topk得出推荐。
    • 基于内容:
      • 对item和user的刻画串联起来得出推荐
    • 基于NN:
      • item2vec:根据用户行为得出item句子,从这些句子中得出item embedding向量,通过这些向量计算item的相似度矩阵。最后根据用户历史点击进行相似推荐。
      • WDL、FFN、PNN、DeepFM、DIN、DCN等

    LFM算法

    矩阵分解算法。用户的点击不同类别的商品,但不代表商品相似。LFM算法根据用户的点击来推测u2i和i2i的关系。

    • 点散矩阵:user和item的矩阵,点击为1,未点击为0,未展示为null。模型迭代是根据user和item有交互的pair来计算的。分解后的向量维度为一超参。

    • 场景:用户toplike、item的topsim、item的topic

    • 数学理论:

      (p^{LFM}(u,i)=sum_{f=1}^Fp_{uf}q_{if}) ,loss function:(sum_{(u,i)in D}(p(u,i)-p^{LFM}(u,i))^2+lambda|p_u|^2lambda|p_i|^2)

      通过梯度下降求解。

      数学推导过程:

      (L(X,Y) = sum (r _{ui} - x_u^Ty_i)+lambda (vert x_u vert^2+vert y_i vert^2))

      (frac{partial L(X,Y)}{partial x_u} = -2sum_{i}^n y_i(r_{ui}-x_u^Ty_i)+2lambda x_u), 这里向量yi在左边还是右边都可以,因为括号内是一常数。

      (0 = -Yr_u+lambda x_u +(sum_{i}^n y_iy_i^T)x_u) 这里注意,yi之所以变为矩阵,是因为rui变为了ru向量,后面yxy的变化是,xy本身是常数,所以两者可以转置而不影响结果。

      (0 = -Yr_u+lambda x_u +(y_0y_0^T+y_1y_1^T+cdots+y_0y_0^T)x_u)

      (0 = -Yr_u+lambda x_u +Y_{k*n}Y_{k*n}^Tx_u)

      最后把xu放到一边即可。

    • 其他参数

      • 负样本选取:用户未点击的item数量极大,要从中选取出与点击item数量相称的未点击的item(负样本)。例如对负样本被其他用户点击/展示的次数取topn。
      • 隐特征F、正则参数、学习效率
    • 与CF的比较

      LFM的离线计算空间更小,复杂度比CF稍大,但同一数量级。LFM不能很好地实现实时调整。

    • 代码:下面以python代码作为理解,实践上并非最优,连起码的np的矢量化计算都没有,另外对于大数据,应采用Spark内置的ALS模型进行计算。

      def lfm_train(train_data, F, alpha, beta, step):
          """
          Args:
            train_data: a list: [(userid, itemid, label)...]; label: 0 or 1, dislike or like
            F: the dimension of user vector and item vector
            alpha: regularization factor
            beta: learning rate
            step: iteration
          Return:
            dict: key itemid, value np.array
            dict: key userid, value np.array
          """
      
          user_vec, item_vec = init_model(train_data, F)
          
          for step_index in range(step):
              for data_instance in train_data:
                  userid, itemid, label = data_instance
                  delta = label - model_predict(user_vec[userid], item_vec[itemid])
                  for index in range(F):
                      user_vec[userid][index] += beta * (delta * item_vec[itemid][index] - alpha * user_vec[userid][index])
                      item_vec[itemid][index] += beta * (delta * user_vec[userid][index] - alpha * item_vec[itemid][index])
                  beta = beta * 0.9
      
          return user_vec, item_vec
      
      
      def init_model(train_data, vector_len):
          user_vec = {}
          item_vec = {}
          for data_instance in train_data:
              userid, itemid, label = data_instance
              if userid not in user_vec:
                  user_vec[userid] = np.random.randn(vector_len)
              if itemid not in item_vec:
                  item_vec[itemid] = np.random.randn(vector_len)
          return user_vec, item_vec
      
      
      def model_predict(user_vector, item_vector):
          res = np.dot(user_vector, item_vector) / (np.linalg.norm(user_vector) * np.linalg.norm(item_vector))
          return res
      
      
      def get_recom_result(user_vec, item_vec, userid, result_num):
          if userid not in user_vec:
              return []
          record = {}
          recom_list = []
          user_vector = user_vec[userid]
          for itemid in item_vec:
              item_vector = item_vec[itemid]
              res = np.dot(user_vector, item_vector) / (np.linalg.norm(user_vector) * np.linalg.norm(item_vector))
              record[itemid] = res
          for comb in sorted(record.items(), key=_operator.itemgetter(1), reverse=True)[:result_num]:
              itemid = comb[0]
              score = round(comb[1], 3)
              recom_list.append((itemid, score))
          return recom_list
      

    Personal Rank算法

    用户行为容易表示为图。下面的二分图中左边为user,右边为items,连线表示关联(点击、购买等)。根据下面提到的三个指标来判断对与A而言,没有关联的c和e哪个更值得推荐。

    两个顶点之间连通路径数(越多)、长度(总长度越短)、经过顶点的出度(出度和越小)。当第一个因素相同时才考虑下一个。从第一条规则来看,A和c的路径数为2,即AaBc、AdDc;而A和e的路径数为1,即AbCe。第二条略。第三条,AaBc的出度为3+2+2+2.

    • 求解过程:对A进行推荐时,从A开始进行random walk,以alpha的概率从A中等概率选择一条路径,如果到达a,那么继续有alpha的概率继续从a中等概率选择一条路径,或者(1-alpha)的概率回到起点(A),多次迭代后各个顶点对用户A的重要度收敛。alpha为超参,一般0.6~0.8.

    • 数学理论:

      PR值为不同item对user的重要度。初始化起点user的PR值为1,其他顶点PR值为0。下面为PR公式的矩阵形式。

      (r=(1-alpha)r_0+alpha M^{T}r),其中(M_{ij}=frac{1}{|out(i)|} if jin out(i) else 0)

      公式可化简为(r=(E-alpha M^T)^{-1}(1-alpha)r_0)

    • 代码:略

    item2vec算法

    将用户的行为系列转化为item组成的句子,模仿word2vec训练word embedding将item embedding。

    • 缺陷:用户的行为序列时序性缺失;用户行为序列中的item强度无区分性

    • 流程:

      • 从log中抽取用户行为序列
      • 将行为序列当成语料训练word2vec得到item embedding
      • 得到item sim关系用于推荐
    • Word2vec Model

      • CBOW:..., W(t-2), W(t-1), W(t+1), W(t+2),..., 全连接得到W(t)。每个W是一个向量。由于语料库的词很多,t只是其中一个,即正样本1个,n-1个负样本,这样训练的时间会很长,通过负采样的出部分负样本来代替。目标是得到W(t)的概率最大,得到W(非t)的概率最小。

        数学理论:

        (g(w)=prod p(u|Context(w))),其中(p(.)=sigma(X_w^T heta^u)^{I^w(u)}(1-sigma(X^T_w heta^u))^{1-I^w(u)}),其中I是指示函数,当u=w时为1,反之为0.

        (Loss = log(g(w))=sum(I^w(u) imes log(sigma(X_w^T heta^u))+(1-I^w(u)) imes log(1-sigma(X_w^T heta^u))))

        ( heta)和x求导:(frac{partial Loss}{partial heta^u}=(I^w(u)-delta(x_w^T heta^u))x_w)(frac{partial Loss}{partial x_w}=(I^w(u)-delta(x_w^T heta^u)) heta^u)

        更新(注意先更新theta):( heta^u = heta^u + alpha imes frac{partial Loss}{partial heta^u})(v(w_{context})=v(w_{context})+sumalpha imes frac{partial Loss}{partial x_w})

      • Skip Gram:已知W(t),全连接得到..., W(t-2), W(t-1), W(t+1), W(t+2),...。

        数学理论:略。

    • 代码:spark有相关工具(略)

    Content Based

    可解析性强,跨领域推荐弱。

    • 流程:

      Item profile(类别、关键字):Topic Finding、Genre Classify。存储在搜索引擎

      User profile:Topic/Genre、Time Decay。存储在kv

      Online Recommendation:topk Topic/Genre、best n item from fixed Topic/Genre

    • 代码:例子流程

      1.获得电影平均分
      2.获取dict1 {itemid: dict {cat: ratio}}
      3.获取dict2 {cat:[itemid1, itemid2, ...]},每个cat的topk
      4.获取dict3 {userid: [(cat, ratio), (cat, ratio)]},用户的Genre属性。遍历每条数据,只对用户喜欢的电影的数据进行计算(rating大于阈值),根据每条数据中的item的cat来更新user的genre属性
      record[userid][fix_cat] += rating * time_score * item_cat[itemid][fix_cat]
      取用户genre的topk并归一。time_score由 1/(1+timestamp最大值-当前ts) 得出
      
      5.推荐,需要dict2和3、userid和topk中的k
      先遍历dict3,利用num = topk*ratio + 1得出从dict2对应cat中取前num的数量
      

    LR + GBDT

    将个性化match的物品候选集根据物品本身属性结合用户属性,上下文等信息给出展现优先级的过程。

    rank还可分为PreRank、Rank和ReRank。

    model分为单一浅层、组合浅层、深度学习

    • LR模型

      • CTR中的benchmark模型。由于线性模型本身的局限,可以进行一些非线性的转换,如连续特征离散化、特征之间的交叉等。

        • 离散化:可以训练一个划分单特征的决策树,这样每个区间上目标的分布就可能不同,从而对每个区间产生不同的权重,进而得出分段线性函数的关系,也就成了非线性了。离散化可以使得单特征的不同阶段与目标形成不同的关系,使模型更robust(异常值被分到某个区间,不过过度影响目标)和stable(数据微小变化会留在相同的区间)
        • 特征交叉:如女性化妆、男性手表等,甚至土豆和马铃薯(但这样的组合很少出现,所以lr难以学习,解决方法参考WDL)。

        特征工程耗费了大量的精力,而且即使有经验的工程师也很难穷尽所有的特征交叉组合。

      • 数学理论:参考《机器学习总结》

    • GBDT模型

      • 数学理论:参考《机器学习总结》
    • LR + GBDT

      • 典型的例子就是Facebook 2014年的论文中介绍的通过GBDT模型解决LR模型的特征组合问题。特征工程分为两部分,一部分特征用于训练一个GBDT模型,把GBDT模型每颗树的叶子节点编号作为新的特征,加入到原始特征集中,再用LR模型训练最终的模型。例如x输入到GBDT,其中GBDT有两棵树,产生了分别产生了3个和2个叶子节点,结果之一为[0,1,0,1,0]。这个结果会被作为LR的输入。

        GBDT模型能够学习高阶非线性特征组合,对应树的一条路径(叶子节点为1表示出口)。通常把一些连续值特征、值空间不大的categorical特征都丢给GBDT模型(1.树模型不适合学习高度稀疏数据的特征组合,因为效率低,2.而且不能学习很少出现或者没有出现的特征组合);空间很大的ID特征(比如商品ID)留在LR模型中训练,既能做高阶特征组合又能利用线性模型易于处理大规模稀疏数据的优势。

      • 流程:

        • 先利用GBDT进行预测,得出每棵树预测的叶子节点,把每棵树的结果,即输出的是第几个节点,进行合并[15, 17, 15...],其中15表示第一棵树的第16个节点(从0开始计算,并假设为第一个叶子节点),17表示第二棵树的...如此类推。树的深度和数量要根据数据和特征的比进行调整。如果数据不够多(数据量:特征=100:1),那么要调小深度和减少数量。另外,如果深度为4,那么2^4=16,代表第16个节点为非叶子节点。如果上面输出的结果小于15,代表树没有完全建立。
        • 将合并的结果转化为one-hot的稀疏矩阵。
        • 利用LR来拟合这些特征
      • 代码:基于Spark的GBDT + LR模型实现

    FM、FFM

    通过特征对之间的隐变量内积来提取特征组合。这能够克服树模型的第二个缺点,因为即使i和j的组合没有出现过,只要i和l,l和j出现过,那么i和j依旧能建立关系。

    FM公式:

    [y=(w_0 + sum_{i=1}^{n}w_i x_i) + (sum_{i=1}^{n}sum_{j=i+1}^n langle v_i,v_j angle x_i x_j) ]

    优化后计算复杂度为O(kn),效率比树模型快。

    上面公式可理解为lr和特征交叉两个部分。特征交叉以User-Item的推荐模型为例,如果用embedding的方式表示,user和item各连接到自己的m维的embedding层,然后embedding层再组合成m个x作为最终(f)的输入。

    FFM模型是对FM模型的扩展,通过引入field的概念,FFM把相同性质的特征归于同一个field。二次参数由FM的kn个变为kfn个。

    MLR

    alibaba的ctr模型,目标函数为

    [f(x)=sum_{i=1}^m pi_i(x,mu)cdot eta_i(x,w)=sum_{i=1}^m frac{e^{mu_i^T x}}{sum_{j=1}^m e^{mu_j^T x}} cdot frac{1}{1+e^{-w^Tx}} ]

    其中m为分片数,当m=1时退化为LR。

    (pi_i(x,mu)= frac{e^{mu_i^T x}}{sum_{j=1}^m e^{mu_j^T x}})是聚类参数,决定分片空间的划分,即某个样本属于某个特定分片的概率

    (eta_i(x,w) = frac{1}{1+e^{-w^Tx}})是分类参数,决定分片空间内的预测

    (mu)(w) 都是待学习的参数。最终模型的预测值为所有分片对应的子模型的预测值的期望。上述模型可以看作是两个embedding,聚类和分类,最后通过内积后加全平均得出预测值。

    MLR模型在大规模稀疏数据上探索和实现了非线性拟合能力,在分片数足够多时,有较强的非线性能力;同时模型复杂度可控,有较好泛化能力;同时保留了LR模型的自动特征选择能力。但该模型非凸非光滑,从而不适用传统GD。

    WDL

    Google在2016年paper提出的模型,它分为wide和deep两部分联合训练。单看wide部分与LR模型并没有什么区别;deep部分则是先对不同的ID类型特征做embedding,在embedding层接一个全连接的MLP(多层感知机),用于学习特征之间的高阶交叉组合关系。由于Embedding机制的引入,WDL相对于单纯的wide模型有更强的泛化能力。

    FFN

    先用FM模型学习到特征的embedding表示,再用学到的embedding向量代替原始特征作为最终模型的特征。

    PNN

    主要关注点在Embedding Layer和Product Layer之间的变化。Embedding Layer和Product Layer之间的权重为常量1,在学习过程中不更新。Product Layer的节点分为两部分,z向量和p向量。z向量的维数与输入层的field个数相同(N)。p向量是pair-wise,经过product操作(分为内积和外积),有N x (N-1)个。这里的(f_i)是field i的embedding向量, !(f_i=W_0^i x[start_i : end_i]) ,其中 x是输入向量, (x[start_i : end_i])是field i的one-hot编码向量。

    DeepFM

    深度神经网络对于学习复杂的特征关系非常有潜力。目前也有很多基于CNN与RNN的用于CTR预估的模型。但是基于CNN的模型比较偏向于相邻的特征组合关系提取,基于RNN的模型更适合有序列依赖的点击数据。

    DeepFM的结构中包含了因子分解机部分以及深度神经网络部分,分别负责低阶特征的提取和高阶特征的提取。

    FM部分:(y_{FM}=langle w,x angle + sum_{i=1}^d sum_{j=i+1}^d langle V_i,V_j angle x_i x_j) 其中 (w∈R^d,V_i∈R^k) 。加法部分反映了一阶特征的重要性,而内积部分反应了二阶特征的影响。

    深度部分:(y_{DNN}=sigma(W^{H+1} cdot a^H + b^{H+1})) 其中H是hidden layer的数量。一个前馈神经网络,在第一层hidden前增加embedding层将数据压缩到底维稠密向量。

    上图中红色箭头所表示的链接权重恒定为1,在训练过程中不更新,可以认为是把节点的值直接拷贝到后一层,再参与后一层节点的运算操作。

    与Wide&Deep Model不同,DeepFM共享相同的输入与embedding向量。在Wide&Deep Model中,因为在Wide部分包含了人工设计的成对特征组,所以输入向量的长度也会显著增加,这也增加了复杂性。

    FNN模型首先预训练FM,再将训练好的FM应用到DNN中。PNN网络的embedding层与全连接层之间加了一层Product Layer来完成特征组合。PNN和FNN与其他已有的深度学习模型类似,都很难有效地提取出低阶特征组合。WDL模型混合了宽度模型与深度模型,但是宽度模型的输入依旧依赖于特征工程。

    上述模型要不然偏向于低阶特征或者高阶特征的提取,要不然依赖于特征工程。而DeepFM模型可以以端对端的方式来学习不同阶的组合特征关系,并且不需要其他特征工程。

    DIN

    阿里17年论文模型。根据历史行为数据进行两个假设:多样性(一个用户可能对多种品类的东西感兴趣)和部分对应(只有一部分的历史数据对目前的点击预测有帮助,比如点击泳镜会与泳衣产生关联,而对书本关系不大)

    DIN设计了一个attention结构,对用户的历史数据和待估算的广告之间部分匹配,从而得到一个权重值,用来进行embedding间的加权求和。

    用户特征由用户历史行为的不同实体ID序列组成。在对用户的表示计算上引入了attention network (也即图中的Activation Unit) 。DIN把用户特征、用户历史行为特征进行embedding操作,视为对用户兴趣的表示,之后通过attention network,对每个兴趣表示赋予不同的权值。这个权值是由用户的兴趣和待估算的广告进行匹配计算得到的。

    Attention network 的公式:(V_u=f(V_a)=sum_{i=1}^N w_i cdot V_i =sum_{i=1}^N g(V_i,V_a) cdot V_i) 其中,V_u 代表用户表示向量, V_i 是用户行为 i 的embedding向量,V_a代表广告的表示向量。核心在于用户的表示向量不仅仅取决于用户的历史行为,而且还与待评估的广告有直接的关联。

    Deep & Cross Network(DCN)

    google 和 Stanford 在2017年的paper提出。模型对sparse和dense的输入自动学习特征交叉,可以有效地捕获有限阶(bounded degrees)上的有效特征交叉,无需人工特征工程或暴力搜索(exhaustive searching),并且计算代价较低。

    模型结构如下:

    Cross network:目的是有效地应用显式特征交叉。

    公式:(x_{l+1} = x_0 x_l^T w_l + b_l + x_l = f(x_l, w_l, b_l) + x_l),由公式可知,the degress of cross features随着layer的深入而增加。涉及的参数数量为(d imes L imes 2),其中d为x的维度数,L为层数。

    cross network的复杂度和传统的DNN在同一水平上,因为$ x_0 x_l^T$的实现避免存储/计算整个matrix

    Deep network:目的是捕获高阶非线性交叉。

    公式:(h_{l+1} = f(W_l h_l + b_l))

    涉及参数数量:(d×m+m+(m^2+m)×(L_d−1)​)

    • 提出一种新型的交叉网络结构,可以用来提取交叉组合特征,并不需要人为设计的特征工程;
    • 这种网络结构足够简单同时也很有效,可以获得随网络层数增加而增加的多项式阶(polynomial degree)交叉特征;
    • 优化的实现十分节约内存,并且易于使用;
    • 实验结果表明,DCN相比于其他模型有更出色的效果,与DNN模型相比,较少的参数却取得了较好的效果。

    代码:基于Spark和Tensorflow构建DCN模型进行CTR预测

    参考:

    主流CTR预估模型的演化及对比

    镶嵌在互联网技术上的明珠:漫谈深度学习时代点击率预估技术进展

    深度CTR预估模型中的特征自动组合机制演化简史

    玩转企业级Deep&Cross Network模型你只差一步

    个性化推荐算法实战

  • 相关阅读:
    题目1522:包含min函数的栈
    [Swift]LeetCode1157. 子数组中占绝大多数的元素 | Online Majority Element In Subarray
    [Swift]LeetCode1156. 单字符重复子串的最大长度 | Swap For Maximum Repeated Substring
    [Swift]LeetCode1153. 字符串转化 | String Transforms Into Another String
    [Swift]LeetCode1151. 最少交换次数来组合所有的 1 | Minimum Swaps to Group All 1's Together
    [Swift]LeetCode1154. 一年中的第几天 | Ordinal Number Of Date
    [Swift]LeetCode1152. 用户网站访问行为分析 | Analyze User Website Visit Pattern
    [Swift]LeetCode1150. 检查一个数是否在数组中占绝大多数 | Check If a Number Is Majority Element in a Sorted Array
    [Swift]LeetCode1146. 快照数组 | Snapshot Array
    [Swift]LeetCode1147. 段式回文 | Longest Chunked Palindrome Decomposition
  • 原文地址:https://www.cnblogs.com/code2one/p/10366238.html
Copyright © 2011-2022 走看看