<Código compartido> Estructura de código de Python del algoritmo de rama y límite

Este artículo toma como ejemplo la resolución del modelo de programación de enteros y proporciona el marco de código de Python del algoritmo de ramificación y límite, buscando la perfección, la corrección y la comunicación.


estructura de archivos


estructura de archivos


código específico


problema.py


Defina el formato de la pregunta:

from typing import List


class Problem(object):
    """
    problem
    """
    def __init__(self, problem_type: str, 
                 num_var: int, mat_cons: List[List[int or float]], obj_coef: List[int or float]) -> None:
        """
        initialise
        
        :param problem_type:  problem type
        :param num_var:  number of variables
        :param mat_cons:  constraint coefficients matrix
        :param obj_coef:  objective coefficients list
        """

        self.problem_type = problem_type
        
        self.num_var = num_var
        self.mat_cons = mat_cons
        self.obj_coef = obj_coef


solver.py


Defina la clase base para los módulos de resolución de problemas:

from algorithm.problem import Problem


class Solver(object):
    """
    problem solver
    """
    def __init__(self, problem: Problem) -> None:
        """
        initialise
        
        :param problem:  problem to solve
        """
        
        self.num_var = problem.num_var
        self.mat_cons = problem.mat_cons
        self.obj_coef = problem.obj_coef

        # solution status
        self.status = 'empty'  # {'empty', 'invalid', 'optimal', 'feasible', 'infeasible', 'fail'}
        self.obj = None
        self.solution = []

    def _is_valid(self) -> bool:
        return True

    def solve(self):
        pass


solver_lp.py


Herede la clase base del módulo de resolución y escriba una subclase de módulo de resolución que llame al solucionador GLOP de OR-Tools para problemas de programación lineal:

from datetime import datetime

import rich
from ortools.linear_solver import pywraplp

from config import ACCURACY
from algorithm.solver import Solver
from algorithm.problem import Problem


class SolverLP(Solver):
    """
    problem solver, LP model
    """
    def __init__(self, problem: Problem) -> None:
        """
        initialise
        
        :param problem:  problem to solve
        """

        super().__init__(problem)

    def _is_valid(self) -> bool:
        """
        check model format

        :return:  if model valid
        """

        # constraint coefficient lists
        for i in range(len(self.mat_cons)):
            cons = self.mat_cons[i]

            # coefficients list length
            if len(cons) != self.num_var + 2:
                rich.print(f"Constraint {
      
      i} length {
      
      len(cons)} invalid, need to be {
      
      self.num_var} + 2")
                return False
            
        # objective coefficients list length
        if len(self.obj_coef) != self.num_var + 1:
            rich.print(
                f"Objective coefficients list length {
      
      len(self.obj_coef)} invalid, need to be {
      
      self.num_var} + 1")
            return False
        
        return True

    def solve(self):
        """
        solving
        """

        if not self._is_valid():
            self.status = 'invalid'
            return
        
        solver = pywraplp.Solver("lp", problem_type=pywraplp.Solver.GLOP_LINEAR_PROGRAMMING)
        
        x = [solver.NumVar(0, solver.infinity(), 'x_{}'.format(i)) for i in range(self.num_var)]

        # constraints
        for cons in self.mat_cons:
            exp = 0
            for j in range(len(cons[: -2])):
                exp += cons[j] * x[j]
            sign, b = cons[-2], cons[-1]
            
            # index -2 coefficient: negative for <=; 0 for ==; positive for >=
            if sign < 0:
                solver.Add(exp <= b)
            elif sign == 0:
                solver.Add(exp == b)
            else:
                solver.Add(exp >= b)

        # objective
        obj = 0
        for j in range(len(self.obj_coef[: -1])):
            obj += self.obj_coef[j] * x[j]
        # index -1 coefficient: positive for maximise; otherwise minimise
        solver.Minimize(obj) if self.obj_coef[-1] <= 0 else solver.Maximize(obj)

        # solve
        dts = datetime.now()
        status = solver.Solve()
        dte = datetime.now()
        tm = round((dte - dts).seconds + (dte - dts).microseconds / (10 ** 6), 3)
        rich.print(f"LP model solving time:  {
      
      tm} s")

        # result
        if status == pywraplp.Solver.OPTIMAL:
            self.status = 'optimal'
            self.obj = solver.Objective().Value()
            self.solution = [x[i].solution_value() for i in range(self.num_var)]
            rich.print(f"objective value:  {
      
      self.obj}")
            rich.print(f"solution:  {
      
      [round(x, ACCURACY) for x in self.solution]}")
        elif status == pywraplp.Solver.FEASIBLE:
            self.status = 'feasible'
            rich.print(f"Didn't get an optimal solution, but a feasible one.")
        elif status == pywraplp.Solver.INFEASIBLE:
            self.status = 'infeasible'
            rich.print(f"The problem does not have a feasible solution.")
        else:
            self.status = 'fail'
            rich.print(f"Failed to solving problem for an unknown reason.")


nodo.py


Módulos de nodo para algoritmos de ramificación y límite:

from typing import List
import copy

from config import PROBLEM_TYPES, INSTANCE, ACCURACY
from algorithm.problem import Problem
from algorithm.solver_lp import SolverLP


class Node(object):
    """
    branch and bound node
    """
    def __init__(self, id: int, father: int or None, new_cons: List[int or float]) -> None:
        """
        initialise
        
        :param id:  node ID
        :param father:  father node ID, None for root
        :param id:  new constraits other than father node's constraints
        """

        self.problem: Problem = copy.deepcopy(INSTANCE)
        self.problem_type = self.problem.problem_type
        self.num_var, self.mat_cons, self.obj_coef = self.problem.num_var, self.problem.mat_cons, self.problem.obj_coef
        
        # check problem type
        if self.problem_type not in PROBLEM_TYPES:
            raise Exception(f"Problem type {
      
      self.problem_type} invalid!")
        
        self.id = id

        self.father = father
        self.new_cons = new_cons

        self.depth = None

        # solution
        self.status = 'empty'  # {'empty', 'feasible', 'relax', 'infeasible'}
        self.obj = None
        self.solution = []
        self.infeasible_list = []

    def add_ancestor_cons(self, ancestor_cons: List[List[int or float]]):
        """
        initialise
        
        :param ancestor_cons:  constraints from ancestors
        """

        self.mat_cons += ancestor_cons

    def solve(self):
        """
        solve problem
        """

        # add new constraint
        if self.new_cons:
            self.mat_cons.append(self.new_cons)

        solver = SolverLP(problem=self.problem)
        solver.solve()

        status, self.obj, self.solution = solver.status, solver.obj, solver.solution
        if status in {
    
    'optimal', 'feasible'}:
            self._check_solution()
            self.status = 'feasible' if not self.infeasible_list else 'relax'
        else:
            self.status = 'infeasible'

    def _check_solution(self):
        """
        check if solution valid
        """

        if self.problem_type == 'IP':
            self._check_integer()
        else:
            pass

    def _check_integer(self):
        """
        check if all solutions are integers
        """

        for i in range(len(self.solution)):
            x = self.solution[i]
            if round(x, ACCURACY) != round(x):
                self.infeasible_list.append((i, x))


algoritmo.py


El módulo de algoritmo del algoritmo de ramificación y límite, los puntos principales:

  • Cada vez que se obtenga una solución factible, actualizar la solución factible óptima (incumbente)
  • Cada vez que se completa una rama (se resuelven todos los nodos secundarios), actualice el límite global (límite)

import datetime
from typing import List, Dict
import math

import rich

from config import PROBLEM_TYPES, INSTANCE, ACCURACY
from algorithm.problem import Problem
from algorithm.node import Node


class BranchAndBound(object):
    """
    branch and bound algorithm
    """
    def __init__(self, time_limit: int or float = 3600, gap_upper: float = 0) -> None:
        """
        initialise
        
        :param time_limit:  time limit of solving
        :param gap_upper:  upper bound of gap of solving
        """

        self.problem: Problem = INSTANCE
        self.problem_type = self.problem.problem_type
        self.num_var, self.mat_cons, self.obj_coef = self.problem.num_var, self.problem.mat_cons, self.problem.obj_coef
        
        # check problem type
        if self.problem_type not in PROBLEM_TYPES:
            raise Exception(f"Problem type {
      
      self.problem_type} invalid!")

        # optimising direction
        self.opt_dir = 'min' if self.obj_coef[-1] <= 0 else 'max'

        # solving params
        self.time_limit = time_limit
        self.gap_upper = gap_upper
        self.dt_start, self.dt_end = datetime.datetime.now(), None
        self.final_time_cost = None
        self.final_gap = None

        # nodes
        self.idx_acc = 0
        self.nodes: Dict[int, Node] = {
    
    }  # all nodes, key: ID
        self.leaf_nodes: List[int] = []  # node ID

        # solution status
        self.status = 'empty'  # {'empty', 'optimal', 'feasible', 'infeasible', 'fail'}
        self.solution = None
        self.incumbent, self.incumbent_node = None, None  # update once feasible solution found
        self.bound, self.bound_node = None, None  # update once leaf nodes updated

    @property
    def time_cost(self) -> float:
        """
        get time cost
        """

        dt_end = datetime.datetime.now()
        tm_delta: datetime.timedelta = dt_end - self.dt_start
        return round(tm_delta.seconds + tm_delta.microseconds / 1e6, 6)

    @property
    def reach_time_limit(self) -> bool:
        """
        check if reach time limit
        """

        return self.time_cost >= self.time_limit
    
    @property
    def gap(self) -> bool:
        """
        calculate current gap
        """

        # case 1: searched all nodes
        if self.bound_node not in self.leaf_nodes:
            return 0
        
        # case 2: catched both incumbent and bound
        elif self.bound is not None and self.incumbent is not None:
            return abs(self.bound - self.incumbent) / abs(self.incumbent)
        
        else:
            return None
    
    @property
    def reach_accuracy(self) -> bool:
        """
        check if gap reach accuracy
        """

        return True if self.gap is not None and self.gap <= self.gap_upper else False

    def run(self):
        """
        algorithm main process
        """

        self.dt_start = datetime.datetime.now()

        # root node and solve
        node_index = self.idx_acc
        self.idx_acc += 1
        node = Node(id=node_index, father=None, new_cons=[])
        node.depth = 0
        self.nodes[node_index] = node
        rich.print(f"Solving node {
      
      node_index}")
        node.solve()

        # special case 1: root node feasible
        if node.status == 'feasible':
            self.solution = node.solution
            self.status = 'optimal'
            return

        # special case 2: root node infeasible
        elif node.status == 'infeasible':
            self.status = 'infeasible'
            return
        
        # update leaf nodes and bound
        self.leaf_nodes.append(node.id)
        self._update_bound()
        rich.print()

        # main circulation
        node_index = self._select_node()
        while node_index is not None:  # attention: don't use "while node_index" because the index can be 0
            # branch and get new leaf nodes
            is_terminated = self._branch(node_index=node_index)

            # check termination
            if is_terminated:
                self.final_time_cost = self.time_cost
                self.final_gap = self.gap
                rich.print(f"final time cost:  {
      
      self.final_time_cost}")
                rich.print(f"final gap:  {
      
      self.final_gap}")
                return

            # select node to branch
            node_index = self._select_node()
            rich.print()

        # all nodes searched
        self.final_gap = self.gap
        if self.incumbent_node is not None:
            self.solution = self.nodes[self.incumbent_node].solution
            self.status = 'optimal' if self.final_gap == 0 else 'feasible'
        else:
            self.status = 'ineasible'
        self.final_time_cost = self.time_cost
        rich.print(f"final time cost:  {
      
      round(self.final_time_cost, 3)} s")
        rich.print(f"final gap:  {
      
      self.final_gap}")
        rich.print(f"total nodes:  {
      
      len(self.nodes)}")
        rich.print(f"maximal node depth:  {
      
      max(self.nodes[i].depth for i in self.nodes)}")
        rich.print()

    def _select_node(self) -> int or None:  # TODO: node selection strategy: bound node
        """
        node selection

        :return:  selected node index
        """

        # have updated bound before
        return self.bound_node if self.bound_node in self.leaf_nodes else None

    def _branch(self, node_index: int) -> bool:
        """
        branching
        
        :param node_index:  father node index

        :return:  if algorithm terminates
        """

        node = self.nodes[node_index]

        list_new_cons = self._get_new_constraints(node_index=node_index)
        for cons in list_new_cons:
            # new node
            node_index_new = self.idx_acc
            self.idx_acc += 1
            node_new = Node(id=node_index_new, father=node.id, new_cons=cons)
            node_new.depth = node.depth + 1
            self.nodes[node_index_new] = node_new

            # inherit constraints from ancestors
            ancestor_cons = []
            node_new_ = node_new
            while node_new_.father:
                ancestor_cons.append(self.nodes[node_new_.father].new_cons)
                node_new_ = self.nodes[node_new_.father]
            node_new.add_ancestor_cons(ancestor_cons=ancestor_cons)

            # solve node and check incumbent
            rich.print(f"Solving node {
      
      node_index_new}")
            rich.print(f"new constraint coefficients:  {
      
      cons}")
            node_new.solve()
            rich.print(f"node status:  {
      
      node_new.status}")
            if node_new.status == 'feasible':
                incumbent_updated = self._update_incumbent(feasible_node=node_index_new)
                
                # update incumbent and check accuracy
                if incumbent_updated:
                    if self.reach_accuracy:
                        rich.print(
                            f"Gap {
      
      self.gap} reach accuracy {
      
      self.gap_upper}, bound {
      
      round(self.bound, ACCURACY)}")

                        self.solution = self.nodes[self.incumbent_node].solution
                        self.status = 'optimal' if self.gap == 0 else 'feasible'
                        return True
                
            # check time limit
            if self.reach_time_limit:
                rich.print(f"Time limit reached!")

                if self.incumbent_node is not None:
                    self.solution = self.nodes[self.incumbent_node].solution
                    self.status = 'optimal' if self.gap == 0 else 'feasible'
                else:
                    rich.print(f"Didn't find a feasible solution within time limit!")
                    self.status = 'fail'

                return True
                
            # add new leaf node
            if node_new.status == 'relax':
                self.leaf_nodes.append(node_index_new)
            rich.print()
                
        # remove old leaf node
        self.leaf_nodes.remove(node_index)
        
        # update bound and check accuracy
        bound_updated = self._update_bound()
        if bound_updated:
            if self.reach_accuracy:
                rich.print(
                    f"Gap {
      
      self.gap} reach accuracy {
      
      self.gap_upper}, incumbent {
      
      round(self.incumbent, ACCURACY)}")
                rich.print()

                self.solution = self.nodes[self.incumbent_node].solution
                self.status = 'optimal' if self.gap == 0 else 'feasible'

                return True

        return False

    def _get_new_constraints(self, node_index: int) -> List[List[float or int]]:  # TODO: branching strategy: index-based
        """
        get new constraints for node branching
        
        :param node_index:  branching node

        :return:  new constraints list
        """

        node = self.nodes[node_index]

        if self.problem_type == 'IP':
            idx_infeasible, infeasible_solution = node.infeasible_list[0]
            new_cons_1 = [1 if i == idx_infeasible else 0 for i in range(self.num_var)] + [
                -1, math.floor(infeasible_solution)]
            new_cons_2 = [1 if i == idx_infeasible else 0 for i in range(self.num_var)] + [
                1, math.ceil(infeasible_solution)]

            return [new_cons_1, new_cons_2]

        else:
            pass

    def _update_incumbent(self, feasible_node: int) -> bool:
        """
        update global feasible solution
        
        :param feasible_node:  index of new feasible node

        :return:  if incumbent updated
        """

        node = self.nodes[feasible_node]

        # get a feasible solution firstly
        if self.incumbent is None:
            self.incumbent, self.incumbent_node = node.obj, feasible_node
            rich.print(f"Get new incumbent {
      
      round(self.incumbent, ACCURACY)} from node {
      
      self.incumbent_node}")
            return True
        
        # get a better feasible solution
        elif (self.opt_dir == 'min' and node.obj < self.incumbent) or (
            self.opt_dir == 'max' and node.obj > self.incumbent):
            self.incumbent, self.incumbent_node = node.obj, feasible_node
            rich.print(f"Get new incumbent {
      
      round(self.incumbent, ACCURACY)} from node {
      
      self.incumbent_node}")
            return True
        
        return False
    
    def _update_bound(self) -> bool:
        """
        update global bound

        :return:  if bound updated
        """

        bound, bound_node = None, None
        for node_idx in self.leaf_nodes:
            node = self.nodes[node_idx]
            if node.status == 'relax':
                if bound is None:
                    bound, bound_node = node.obj, node_idx
                elif (self.opt_dir == 'min' and node.obj < bound) or (
                    self.opt_dir == 'max' and node.obj > bound):
                    bound, bound_node = node.obj, node_idx

        if bound is not None:
            self.bound, self.bound_node = bound, bound_node
            rich.print(f"Get new bound {
      
      round(self.bound, ACCURACY)} from node {
      
      self.bound_node}")
            return True
        
        return False


casos.py


Según el formato del problema definido anteriormente, se proporcionan dos ejemplos de programación de enteros:

Un ejemplo sencillo:
ejemplo sencillo

Un ejemplo de NP:
ejemplo de NP del algoritmo de ramificación y límite

from algorithm.problem import Problem


CASES = {
    
    }


"""
IP: simple
"""

num_var = 2
mat_cons = [
    [1, 1, -1, 5], 
    [10, 6, -1, 45]
]
var_range = [
    [1, 0, 1, 0], 
    [0, 1, 1, 0]
]
mat_cons += var_range
obj_coef = [5, 4, 1]

CASES['IP-simple'] = Problem(problem_type='IP', num_var=num_var, mat_cons=mat_cons, obj_coef=obj_coef)


"""
IP: NP-hard
"""

num_var = 16
mat_cons = [[2 for _ in range(num_var - 1)] + [1] + [0] + [15]]
var_range = [
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0] + [1] + [0], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + [1] + [0], 
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0] + [-1] + [1], 
    [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1] + [-1] + [1],
]
mat_cons += var_range
obj_coef = [0 for _ in range(num_var - 1)] + [1] + [-1]

CASES['IP-NP-hard'] = Problem(problem_type='IP', num_var=num_var, mat_cons=mat_cons, obj_coef=obj_coef)


config.py


El archivo de configuración en el directorio raíz, incluidas las variables globales, como la selección de ejemplos de cálculo y la precisión del algoritmo:

from algorithm.cases import CASES


PROBLEM_TYPES = {
    
    'IP'}  # TODO: only support IP model so far

# INSTANCE = CASES['IP-simple']
INSTANCE = CASES['IP-NP-hard']

ACCURACY = 3  # 0.001


principal.py


Programa principal:

import rich

from config import ACCURACY
from algorithm.algorithm import BranchAndBound


rich.print()
branch_and_bound = BranchAndBound(time_limit=300, gap_upper=0)
branch_and_bound.run()

status = branch_and_bound.status
solution = [round(x, ACCURACY) for x in branch_and_bound.solution]
optimal_obj = branch_and_bound.incumbent
rich.print(f"algorithm final status:  {
      
      status}")
rich.print(f"algorithm final solution:  {
      
      solution}")
rich.print(f"algorithm final objective value:  {
      
      optimal_obj}")
rich.print()



Efecto de ejecución de cálculo


ejemplo sencillo


efecto de ejemplo simple

Diagrama de flujo de un estudio simple

Ejemplo de cálculo de NP


efecto estudio NP


panorama


Este artículo se dedicó originalmente a escribir un marco de algoritmo simple de ramificación y acotación adecuado para varios problemas. De hecho, es solo por diversión. Sin embargo, debido a la energía limitada, solo completé la parte de resolver problemas de programación entera, y la parte La estrategia específica dentro del algoritmo es relativamente simple. Por lo tanto, tengo pocas posibilidades de realizar más funciones, esperando mejoras, correcciones y comunicación:

  • Estrategia de bifurcación más rica: el código de este artículo selecciona el número de secuencia más pequeño entre todas las restricciones inviables para bifurcar
  • Estrategia de selección de nodos más rica: el código de este artículo selecciona el nodo enlazado (enlazado) en el nodo hoja para la siguiente búsqueda profunda
  • Tipos de problemas más ricos: como el uso del algoritmo de ramificación y límite para resolver TSP, CBS (búsqueda basada en conflictos), ramificación y precio y otros problemas. Por supuesto, el marco actual puede no ser factible o razonable, y esperamos mejores métodos de implementación
  • Presentación de corte efectivo: mejora del rendimiento del algoritmo

Supongo que te gusta

Origin blog.csdn.net/Zhang_0702_China/article/details/130565934
Recomendado
Clasificación