分支界定算法实践(三) 精确求解TSP

2023-10-11 00:59

本文主要是介绍分支界定算法实践(三) 精确求解TSP,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

TSP问题是非常经典的NP难问题,问题描述很简单,但是求解难度却很大。首先还是给出TSP的数学描述:

  • G ( V , E ) G(V,E) G(V,E)是一个无向简单图,集合 V V V包含了顶点 1 , 2 , . . . , n 1,2,...,n 1,2,...,n,集合 E E E包含了所有边 e ( i , j ) , i , j ∈ V e(i,j), i,j\in V e(i,j),i,jV. 每条边 e ( i , j ) e(i,j) e(i,j)都有对应的权重 w i j ≥ 0 w_{ij}\geq0 wij0. 需要找到一个边集合 T ⊂ E T\subset E TE,其包含的边构成了图的最短路径,最短路径的长度可以定义为 ∑ ( i , j ) ∈ T w i j \sum_{(i,j)\in T}w_{ij} (i,j)Twij,图上的每对顶点 s , t ∈ V s,t\in V s,tV都存在只属于 T T T的边来相通,并且每个顶点都只有两个属于 T T T的边被确定

一般会把TSP问题用01变量或者置换队列来进行数学描述, 如果是01变量的表述形式,通用的分支界定算法也可以求解,不过我们可以使用一些更加特异化的分支界定算法,更具效率,本文就介绍其中一种

分支界定是一种求精确解的算法,如果TSP的顶点数太多,无论多高明的分支界定算法也无能为力,所以它只适用于规模较小的情况。分支界定算法只是一种算法框架,要应用该算法,需要首先从整体上确定算法中的三个主要成分的实现手段,然后具体细化所有的算法步骤。这三个算法主要成分是:

  • 分支策略:我们需要决定如何对父结点进行分支,也就是添加新约束来产生子问题结点;
  • 定界策略:在每个问题结点上,需要求解当前约束下的问题下界,同时整个算法也维护一个问题上界,如果该问题的下界大于上界,那么该结点可以进行剪枝;
  • 分支结点选择:对于多个可以进行分支的结点,我们也需要用一种选择策略选取结点进行分支

下面我们具体分析. 首先需要确定的是如何表述TSP问题和约束,也就是决策变量是哪个。这里我们明确决策变量是所有边的状态,如果边 e i j = 1 e_{ij}=1 eij=1,表示该边在路径上(included edge);如果 e i j = 0 e_{ij}=0 eij=0,表示该边不在路径上(excluded edge),这样求解TSP的过程可以看作是从图中选取边来构成一条最短的路径。

确定分支策略。每一次产生子问题,就是在父问题的图中选择一条尚未确定状态的边来设定状态,加入路径或者排除出路径。在根节点,所有的边都没有确定状态,随着分支过程的进行,越来越多的边的状态被确定,最终达到一些可行解。在选择边的具体实现上,有两种方式:

  • 按照字典序选边(Lexicographic Order, LG)。这是比较简单直接的方式,例如在当前结点,边 e 1 , 2 e_{1,2} e1,2被加入路径,对其进行分支,那么按照字典序就选择边 e 1 , 3 e_{1,3} e1,3,如果边 e 1 , 3 e_{1,3} e1,3已经被确定了状态,那就继续看边 e 1 , 4 e_{1,4} e1,4,以此类推
  • 按照边长升序选择(Increasing Length, IL)。就是说选择剩余未明确边中最小长度的边,这种选取方式基于思想是选取更小的边加入路径,更有可能较早地找到最短路径

而在通过分支加入新的边约束之后,一些额外的边状态检查和确定工作也需要被执行,来达到一种类似"约束传播"的效果,减小搜索空间:

  • 如果某个顶点上除了两条相邻边,都被排除出了路径,那么这两条边肯定要被加入路径
  • 如果某个顶点上已经有两条加入路径的相邻边,那么所有其他的相邻边都要被排除出路径
  • 对于一条未确定状态的边,如果把它加入路径会导致子回路产生,那么这条边一定要被排除出路径

接着确定定界策略。全局上界很明显就是当前的最短路径,每次找到一个更短的全路径,就用这个值更新上界;而在每个子问题结点上,如何计算下界是需要着重讨论的。在分支界定算法中,在每个结点上的最优解的目标值是肯定不会比下界更小的,在TSP问题中,下界就是当前结点对应的图中最短路径所能达到的理论最小值(所谓理论,就是除了已经确定好的边,其他未确定状态边可以任意设定状态)。同样我们也有两种计算下界的方法:

  • 简单下界(Simple lower bound, SB)。对每个顶点 i i i,假设 ( a i , i ) (a_i,i) (ai,i) ( b i , i ) (b_i,i) (bi,i)是顶点 i i i的两个相邻边,这两条边要么是已经明确在路径中的,要么是未确定状态边中最短的,那么下界就是所有顶点的这两条边长的总和:
    L = 1 2 ∑ i = 1 n w a i i + w b i i L=\frac{1}{2} \sum_{i=1}^{n}{w_{a_ii}+w_{b_ii}} L=21i=1nwaii+wbii
  • 1-tree下界(OT)。1-tree是最小生成树的变体,首先生成树指的是指一个连通图的子图,它含有图中全部 n n n个顶点,但只有足以构成一棵树的 n − 1 n-1 n1条边,如果生成树中再添加一条边,则必定成环;所有生成树中边权重和最小的叫最小生成树(MST),下图是生成树和最短生成树的示意图:
    在这里插入图片描述
    在最小生成图的基础上,我们这样定义1-tree:图 G ( V , E ) G(V,E) G(V,E)是全连接无向图,1-tree是指顶点 { 2 , . . . , n } \{2,...,n\} {2,...,n}加上顶点1及顶点1两条相邻边构成的生成树,总权重最小的1-tree则是最小1-tree。最小1-tree可以通过顶点 { 2 , . . . , n } \{2,...,n\} {2,...,n}的MST加上顶点1的两条最短边构成,下图是一个最小1-tree的示意图
    在这里插入图片描述

回到TSP问题,可以用最小1-tree的值来作为下界值,1-tree里的顶点1就是TSP的起点。

最后是分支结点的选择。我们对于最小化问题,一般都会采取更小下界优先的原则,也就是先选下界更小的结点来分支,因为这样找到一个更短路径的可能性更大一些,也就意味着尽可能早的更新全局上界,这样后续分支时也更可能剪枝,提升算法速度。对TSP问题,我们采用同样的策略。

下面用python代码实现一个TSP实例,采用IL+SB策略组合的分支界定算法。首先给出一个距离矩阵作为问题实例,这是一个numpy二维数组,尺寸是14X14,也就是14个顶点:

size=14
costs=np.array([ [0,  141,  118,  171,  126,  69,   158, 79, 166, 208, 65, 67, 98, 97],[141,  0,  266,   34,  212,  208,  82, 114, 305, 324, 165, 75, 187, 118],[118, 266, 0, 232, 56, 107, 194, 109, 229, 122, 61, 151, 60, 201],[171, 34, 232, 0, 200, 233, 63, 128, 332, 347, 188, 104, 180, 146],[126, 212, 56, 200, 0, 105, 145, 84, 228, 170, 55, 123, 19, 181],[69, 208, 107, 233, 105, 0, 212, 113, 123, 144, 54, 133, 96, 163],[158, 82, 194, 63, 145, 212, 0, 104, 324, 323, 161, 93, 137, 161],[79, 114, 109, 128, 84, 113, 104, 0, 236, 225, 65, 51, 58, 133],[166, 305, 229, 332, 228, 123, 324, 236, 0, 187, 177, 230, 219, 239],[208, 324, 122, 347, 170, 144, 323, 225, 187, 0, 165, 249, 198, 298],[65, 165, 61, 188, 55, 54, 161, 65, 177, 165, 0, 91, 48, 140],[67, 75, 151, 104, 123, 133, 93, 51, 230, 249, 91, 0, 104, 73],[98, 187, 60, 180, 19, 96, 137, 58, 219, 198, 48, 104, 0, 161],[97, 118, 201, 146, 181, 163, 161, 133, 239, 298, 140, 73, 161, 0] ])

定义Node类,代表分支界定的每个结点。Node类包含名为constraints的属性,它是二维数组,其元素表示了各条边的状态,为0表示该边排除出路径,为1表示该边加入路径,为2表示该边的状态尚未确定

import math
import copy
import numpy as npclass Node:def __init__ (self, size , costs ,sortedEdges ,allSortedEdges ,parent_constr , extra_constr = None):self.size = size self.costs = costs self.sortedEdges = sortedEdgesself.allSortedEdges = allSortedEdgesself.extra_constr = extra_constrself.constraints = self.determine_constr(parent_constr)self.lowerBound = self.computeLowerBound()def computeLowerBound(self):'''计算当前结点的下界, 采用the simple lower bound(SB)的方式,即所有顶点的两个最小可行临边的和'''lb = 0for i in range(self.size):lower = 0t = 0for j in range(self.size):if self.constraints[i][j] == 1:lower += self.costs[i][j]t += 1tt = 0while t < 2:shortest = self.sortedEdges[i][tt]if self.constraints[i][shortest] == 2:lower += self.costs[i][shortest]t += 1tt += 1if tt >= self.size:lower = math.infbreaklb += lowerreturn lbdef determine_constr(self,parent_constr):'''在父结点的基础上加入分支约束来构成当前结点的边约束'''constraints = copy.deepcopy(parent_constr)if self.extra_constr == None:return constraintsfr = self.extra_constr[0]to = self.extra_constr[1]constraints[fr][to] = self.extra_constr[2]constraints[to][fr] = self.extra_constr[2]for i in range(2):constraints = self.removeEdges(constraints)constraints = self.addEdges(constraints)return constraintsdef removeEdges(self,constraints):'''对于当前结点, 去除掉所有不可行边(Excluding edges):1) 如果某个顶点已经有两条相邻边是Including edges, 那么其余相邻边排除2) 如果加入这条边会导致子回路产生, 该边排除'''for i in range(self.size):t = 0for j in range(self.size):if (i != j) and (constraints[i][j] == 1):t += 1if t >= 2:for j in range(self.size):if constraints[i][j] == 2:constraints[i][j] = 0constraints[j][i] = 0# 检测任意的i和j之间是否存在回路可能, 如果从j可以从边l(i,j)外的边回溯到i, 那么边l(i,j)要被排除for i in range(self.size):for j in range(self.size):t = 1prev = ifr = jcycle = FalsenextOne = self.next_one(prev ,fr,constraints)while (nextOne[0]):t += 1next = nextOne[1]if next == i:cycle = Truebreakif t > self.size:breakprev = frfr = nextnextOne = self.next_one(prev ,fr,constraints)if (cycle) and (t < self.size) and (constraints[i][j] == 2):constraints[i][j] = 0constraints[j][i] = 0return constraintsdef addEdges(self,constraints):'''对于当前结点下的每个顶点, 如果它的相邻边只剩两条没有被排除, 那么这两条边肯定要被包含'''for i in range(self.size):t = 0for j in range(self.size):if constraints[i][j] == 0:t += 1if t == self.size-2:for j in range(self.size):if constraints[i][j] == 2:constraints[i][j] = 1constraints[j][i] = 1return constraintsdef next_one(self,prev ,fr,constraints):'''从顶点prev到顶点fr, 检测是否存在第三个顶点到fr, 如果没有, 返回false如果有, 返回true, 并返回这第三个顶点'''for j in range(self.size):if (constraints[fr][j] == 1) and (j != prev):return [True ,j]return [False]def isTour(self):'''检测当前结点是否是一个全路径: 如果一个图的每个顶点都只有两个相邻边, 其他相邻边都被排除, 那么该图是一个全路径'''for i in range(self.size):num_zero = 0num_one = 0for j in range(self.size):if self.constraints[i][j] == 0:num_zero += 1elif self.constraints[i][j] == 1:num_one += 1if (num_zero != self.size-2) or (num_one != 2):return Falsereturn Truedef contains_subtour(self):'''检测当前结点的图中是否存在子回路:对每个顶点, 检测是否存在长度小于顶点数的回溯路线'''for i in range(self.size):next = self.next_one(i,i,self.constraints)if next[0]:prev = ifr = next[1]t = 1next = self.next_one(prev , fr, self.constraints)while next[0]:t += 1prev = frfr = next[1]if (fr == i) and (t < self.size):return Truenext = self.next_one(prev ,fr,self.constraints)if t == self.size:return Falsereturn Falsedef tourLength(self):'''返回全路径时的路径长度'''length = 0fr = 0to = self.next_one(fr,fr,self.constraints)[1]for i in range(self.size):length += self.costs[fr][to]temp = frfr = toto = self.next_one(temp ,to,self.constraints)[1]return lengthdef next_constraint(self):'''决定用于下一次分支的边, 使用IL方法'''for edge in self.allSortedEdges:i = edge[0]j = edge[1]if self.constraints[i][j] == 2:return edgedef __str__ (self):if self.isTour():result = '0'fr = 0to = Nonefor j in range(self.size):if self.constraints[fr][j] == 1:to = jresult += '-' + str(j)breakfor i in range(self.size-1):for j in range(self.size):if (self.constraints[to][j] == 1) and (j != fr):result += '-' + str(j)fr = toto = jbreakreturn resultelse:print('This node is not a tour')

定义TSP类,作为算法的主类。在构造方法中初始化IL和SB所需的数据结构,在findSolution中递归调用BranchAndBound方法进行分支界定算法计算

from Node import Node
import math
import time
import copyclass TSP:def __init__(self,size ,costs ,bestTour = math.inf):self.size = sizeself.costs = costsself.bestTour = bestTourself.bestNode = Noneself.bestNodeTime = 0self.num_createdNodes = 0self.num_prunedNodes = 0self.sortedEdges = self.sort_edges()self.allSortedEdges = self.sort_allEdges()def findSolution(self):root = self.create_root()self.num_createdNodes += 1T1 = time.perf_counter()self.BranchAndBound(root)T2 = time.perf_counter()print('---------------------------------------------------------------------')print('The shortest tour is:', self.bestNode)print('It has a length of', self.bestTour , 'km')print('Found in',T2 - T1, 'seconds')print('Best tour was found after:',self.bestNodeTime ,'seconds')print('Number of nodes created:',self.num_createdNodes)print('Number of nodes pruned:',self.num_prunedNodes)def sort_edges(self):'''对距离矩阵每行进行升序排序result的每行表示第i个顶点, 每行上的元素是其他顶点号码, 按距离升序排列'''result = []for i in range(self.size):result.append([x for (y, x) in sorted(zip(self.costs[i],list(range(self.size))))])return resultdef sort_allEdges(self):'''所有边按边长升序排列'''edges = []lengths = []for i in range(self.size):for j in range(i + 1, self.size):edges.append([i, j])lengths.append(self.costs[i][j])result = [z for (l, z) in sorted(zip(lengths , edges))]return resultdef create_root(self):'''root结点, 没有任何边约束'''no_constraints = []for i in range(self.size):row_i = []for j in range(self.size):if (i != j):row_i.append(2)else:row_i.append(0)no_constraints.append(row_i)root = Node(self.size ,self.costs ,self.sortedEdges ,self.allSortedEdges ,no_constraints)return rootdef BranchAndBound(self,node):if node.isTour():if node.tourLength() < self.bestTour:self.bestTour = node.tourLength()self.bestNode = nodeself.bestNodeTime = time.perf_counter()print('Found better tour:', self.bestNode ,'of length',self.bestTour , 'km')else:new_constraint = copy.copy(node.next_constraint())new_constraint.append(1) # 左节点上包含这条边leftChild = Node(self.size ,self.costs ,self.sortedEdges ,self.allSortedEdges ,node.constraints , new_constraint)new_constraint[2] = 0 # 右节点上排除这条边rightChild = Node(self.size ,self.costs ,self.sortedEdges,self.allSortedEdges ,node.constraints , new_constraint)self.num_createdNodes += 2if self.num_createdNodes % 401 == 0:print('Number of nodes created so far:', self.num_createdNodes)print('Number of nodes pruned so far:', self.num_prunedNodes)if self.num_createdNodes % 51 == 0:print('.')if (leftChild.contains_subtour()) or (leftChild.lowerBound > 2 * self.bestTour):leftChild = Noneself.num_prunedNodes += 1if (rightChild.contains_subtour()) or (rightChild.lowerBound > 2 * self.bestTour):rightChild = Noneself.num_prunedNodes += 1if (leftChild != None) and (rightChild == None):self.BranchAndBound(leftChild)elif (leftChild == None) and (rightChild != None):self.BranchAndBound(rightChild)elif (leftChild != None) and (rightChild != None):if leftChild.lowerBound <= rightChild.lowerBound:self.BranchAndBound(leftChild)self.BranchAndBound(rightChild)else:self.BranchAndBound(rightChild)self.BranchAndBound(leftChild)

在主程序中用如下的代码进行测试:

from Node import Node
from TSP import TSP
import numpy as npsize=14
costs=np.array([ [0,  141,  118,  171,  126,  69,   158, 79, 166, 208, 65, 67, 98, 97],[141,  0,  266,   34,  212,  208,  82, 114, 305, 324, 165, 75, 187, 118],[118, 266, 0, 232, 56, 107, 194, 109, 229, 122, 61, 151, 60, 201],[171, 34, 232, 0, 200, 233, 63, 128, 332, 347, 188, 104, 180, 146],[126, 212, 56, 200, 0, 105, 145, 84, 228, 170, 55, 123, 19, 181],[69, 208, 107, 233, 105, 0, 212, 113, 123, 144, 54, 133, 96, 163],[158, 82, 194, 63, 145, 212, 0, 104, 324, 323, 161, 93, 137, 161],[79, 114, 109, 128, 84, 113, 104, 0, 236, 225, 65, 51, 58, 133],[166, 305, 229, 332, 228, 123, 324, 236, 0, 187, 177, 230, 219, 239],[208, 324, 122, 347, 170, 144, 323, 225, 187, 0, 165, 249, 198, 298],[65, 165, 61, 188, 55, 54, 161, 65, 177, 165, 0, 91, 48, 140],[67, 75, 151, 104, 123, 133, 93, 51, 230, 249, 91, 0, 104, 73],[98, 187, 60, 180, 19, 96, 137, 58, 219, 198, 48, 104, 0, 161],[97, 118, 201, 146, 181, 163, 161, 133, 239, 298, 140, 73, 161, 0] ])tsp=TSP(size,costs)
tsp.findSolution()

计算结果:
在这里插入图片描述
同时可以用下面的代码来测试一下暴力搜索法的效果,下面的暴力搜索把顶点数减少到10个,但计算时间已经远大于分支界定算法

from Node import Node
from TSP import TSP
import numpy as np
import itertools
import math
import timesize=10
costs=np.array([ [0,  141,  118,  171,  126,  69,   158, 79, 166, 208, 65, 67, 98, 97],[141,  0,  266,   34,  212,  208,  82, 114, 305, 324, 165, 75, 187, 118],[118, 266, 0, 232, 56, 107, 194, 109, 229, 122, 61, 151, 60, 201],[171, 34, 232, 0, 200, 233, 63, 128, 332, 347, 188, 104, 180, 146],[126, 212, 56, 200, 0, 105, 145, 84, 228, 170, 55, 123, 19, 181],[69, 208, 107, 233, 105, 0, 212, 113, 123, 144, 54, 133, 96, 163],[158, 82, 194, 63, 145, 212, 0, 104, 324, 323, 161, 93, 137, 161],[79, 114, 109, 128, 84, 113, 104, 0, 236, 225, 65, 51, 58, 133],[166, 305, 229, 332, 228, 123, 324, 236, 0, 187, 177, 230, 219, 239],[208, 324, 122, 347, 170, 144, 323, 225, 187, 0, 165, 249, 198, 298],[65, 165, 61, 188, 55, 54, 161, 65, 177, 165, 0, 91, 48, 140],[67, 75, 151, 104, 123, 133, 93, 51, 230, 249, 91, 0, 104, 73],[98, 187, 60, 180, 19, 96, 137, 58, 219, 198, 48, 104, 0, 161],[97, 118, 201, 146, 181, 163, 161, 133, 239, 298, 140, 73, 161, 0] ])#tsp=TSP(size,costs)
#tsp.findSolution()minLength = math.inf
minTour = []
T1 = time.perf_counter()
for tour in itertools.permutations(list(range(1,size))):fr = 0length = 0count = 0while count < size-1:to = tour[count]length += costs[fr][to]fr = tocount += 1length += costs[fr][0]print('New tour :', tour)if length < minLength:minLength = lengthminTour = tour
minTour = (0,) + minTour + (0,)
T2 = time.perf_counter()
print('Shortest tour is:', minTour)
print('It has a length of:', minLength , 'km')
print('Found in',T2 - T1, 'seconds')

在这里插入图片描述

这篇关于分支界定算法实践(三) 精确求解TSP的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/184371

相关文章

SpringBoot项目中Maven剔除无用Jar引用的最佳实践

《SpringBoot项目中Maven剔除无用Jar引用的最佳实践》在SpringBoot项目开发中,Maven是最常用的构建工具之一,通过Maven,我们可以轻松地管理项目所需的依赖,而,... 目录1、引言2、Maven 依赖管理的基础概念2.1 什么是 Maven 依赖2.2 Maven 的依赖传递机

Oracle查询优化之高效实现仅查询前10条记录的方法与实践

《Oracle查询优化之高效实现仅查询前10条记录的方法与实践》:本文主要介绍Oracle查询优化之高效实现仅查询前10条记录的相关资料,包括使用ROWNUM、ROW_NUMBER()函数、FET... 目录1. 使用 ROWNUM 查询2. 使用 ROW_NUMBER() 函数3. 使用 FETCH FI

Git中恢复已删除分支的几种方法

《Git中恢复已删除分支的几种方法》:本文主要介绍在Git中恢复已删除分支的几种方法,包括查找提交记录、恢复分支、推送恢复的分支等步骤,文中通过代码介绍的非常详细,需要的朋友可以参考下... 目录1. 恢复本地删除的分支场景方法2. 恢复远程删除的分支场景方法3. 恢复未推送的本地删除分支场景方法4. 恢复

在C#中获取端口号与系统信息的高效实践

《在C#中获取端口号与系统信息的高效实践》在现代软件开发中,尤其是系统管理、运维、监控和性能优化等场景中,了解计算机硬件和网络的状态至关重要,C#作为一种广泛应用的编程语言,提供了丰富的API来帮助开... 目录引言1. 获取端口号信息1.1 获取活动的 TCP 和 UDP 连接说明:应用场景:2. 获取硬

Java内存泄漏问题的排查、优化与最佳实践

《Java内存泄漏问题的排查、优化与最佳实践》在Java开发中,内存泄漏是一个常见且令人头疼的问题,内存泄漏指的是程序在运行过程中,已经不再使用的对象没有被及时释放,从而导致内存占用不断增加,最终... 目录引言1. 什么是内存泄漏?常见的内存泄漏情况2. 如何排查 Java 中的内存泄漏?2.1 使用 J

numpy求解线性代数相关问题

《numpy求解线性代数相关问题》本文主要介绍了numpy求解线性代数相关问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 在numpy中有numpy.array类型和numpy.mat类型,前者是数组类型,后者是矩阵类型。数组

Python中的随机森林算法与实战

《Python中的随机森林算法与实战》本文详细介绍了随机森林算法,包括其原理、实现步骤、分类和回归案例,并讨论了其优点和缺点,通过面向对象编程实现了一个简单的随机森林模型,并应用于鸢尾花分类和波士顿房... 目录1、随机森林算法概述2、随机森林的原理3、实现步骤4、分类案例:使用随机森林预测鸢尾花品种4.1

Linux中Curl参数详解实践应用

《Linux中Curl参数详解实践应用》在现代网络开发和运维工作中,curl命令是一个不可或缺的工具,它是一个利用URL语法在命令行下工作的文件传输工具,支持多种协议,如HTTP、HTTPS、FTP等... 目录引言一、基础请求参数1. -X 或 --request2. -d 或 --data3. -H 或

Docker集成CI/CD的项目实践

《Docker集成CI/CD的项目实践》本文主要介绍了Docker集成CI/CD的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学... 目录一、引言1.1 什么是 CI/CD?1.2 docker 在 CI/CD 中的作用二、Docke

不懂推荐算法也能设计推荐系统

本文以商业化应用推荐为例,告诉我们不懂推荐算法的产品,也能从产品侧出发, 设计出一款不错的推荐系统。 相信很多新手产品,看到算法二字,多是懵圈的。 什么排序算法、最短路径等都是相对传统的算法(注:传统是指科班出身的产品都会接触过)。但对于推荐算法,多数产品对着网上搜到的资源,都会无从下手。特别当某些推荐算法 和 “AI”扯上关系后,更是加大了理解的难度。 但,不了解推荐算法,就无法做推荐系