变邻域搜索算法求解TSP问题(VNS)(自用部分非原创内容)

变邻域搜索算法求解TSP问题(VNS)(自用部分非原创内容)

变邻域搜索算法(Variable Neighborhood Search,简称VNS)是一种用于解决组合优化问题的启发式搜索算法。该算法由Mladenović和Hansen于1997年提出,旨在通过不断变化问题的邻域结构来寻找更好的解决方案。VNS通常用于NP困难问题,如旅行商问题(TSP)、车辆路径问题(VRP)等。

以下是变邻域搜索算法的基本思想和步骤:

  1. 初始解:算法从一个初始解开始,可以是随机生成的或基于其他启发式算法得到的解。
  2. 邻域结构:VNS使用一系列不同的邻域结构来探索潜在的解空间。每个邻域结构定义了一种改变当前解的方式。这些邻域结构可以是不同的,包括交换两个元素、插入一个元素、反转一部分元素等等。
  3. 邻域搜索:在每个迭代中,算法选择一个当前邻域结构,并在该结构下搜索一个新的解。搜索可以使用局部搜索方法,如模拟退火、爬山法等。如果找到了一个更好的解,则更新当前解为新的解。
  4. 邻域切换:如果在当前邻域结构下无法找到更好的解,算法切换到下一个邻域结构,并继续搜索。这个过程可能会在一系列不同的邻域结构之间进行多次迭代。
  5. 停止条件:算法可以根据一定的停止条件来终止,例如达到最大迭代次数、运行时间超过限制或者找到了一个满足要求的解。

VNS的关键思想是通过变化邻域结构来增加搜索的多样性,从而有更大的机会找到更优的解决方案。通过不断地切换邻域结构,VNS可以跳出局部最优解,进一步搜索解空间。然而,VNS的性能高度依赖于如何选择和切换邻域结构,以及在每个邻域中进行的局部搜索策略。

构造初始路线

初始解是算法开始搜索的起点,它可以通过多种方式构造,包括以下几种常见方法:

  1. 随机生成:可以随机生成一个初始解,这通常是一种简单而快速的方法。但是,随机生成的初始解可能会比较差,因此需要后续的搜索来改进。
  2. 启发式算法:可以使用其他启发式算法(如最近邻算法、插入算法、交换算法等)来生成一个初始解。这些算法通常会根据一些启发式规则来构建初始解,以期望得到相对较好的起点。
  3. 基于已知信息的构建:如果问题的特性允许,可以根据已知的信息来构建初始解。例如,对于VRP,可以基于顾客之间的距离或需求来构建初始路线。

初始解的质量对于变邻域搜索算法的性能至关重要。一个好的初始解可以加速算法的收敛,而一个差的初始解可能需要更多的搜索时间才能找到更优的解。因此,在实际应用中,通常会尝试不同的初始解构造方法,以找到最好的初始解来启动变邻域搜索算法的搜索过程。现在用贪婪算法来举例子

  1. 初始化一个空的解,表示还没有选择任何元素或路径。

  2. 从未选择的元素中选择一个元素,通常是基于某种启发式规则来做出选择。这个规则可能会考虑元素之间的成本、距离、权重等信息,以选择最有希望的元素。

  3. 将选定的元素添加到当前解中。

  4. 重复步骤2和步骤3,直到满足某个终止条件,例如所有元素都被选择完毕。

    function [init_route,init_len]=construct_route(dist)
    %% 贪婪算法构造TSP的初始解
    %输入dist:             距离矩阵   
    %输出init_route:      贪婪算法构造的初始路线
    %输出init_len:        init_route的总距离  
    N=size(dist,1);%城市数目
    for i = 1:N
        for j = 1:N
            if i == j
                dist(i,j)=inf;
            end
        end
    end
    univisited=1:N;%初始未被安排的城市集合
    visited=[];%初始已被安排的城市集合
    min_dist=min(min(dist));%找出距离矩阵中的最小值
    [row,col]=find(dist==min_dist);%在dist中找出min_dist所对应的行和列
    first=row(1);%将min_dist在dist中所对应行序号作为起点
    univisited(univisited==first)=[];%将first从unvisit中删除
    visited=[visited,first];%%把first添加到visit中
    pre_point=first;%%将fisrt赋值给pre_point
    while ~isempty(univisited)
        pre_dist=dist(pre_point,:); %pre_point与其它城市的距离
        pre_dist(visited)=inf;%将pre_point与已经添加进来的城市之间的距离设位无穷大
        [~,pre_point]=min(pre_dist); %找出pre_dist中的最小值
        univisited(univisited==pre_point)=[];%将pre_point从unvisit中删除
        %%第一个pre_point只是为了找到最小距离的起点序号,后面用于储存pre_dist
        %%第二个pre_point储存了pre_dist中的最小值然后并将其赋予[]值
        visited=[visited,pre_point]; %把pre_point添加到visit中
    end
    init_route=visited;
    init_len=route_length(init_route,dist); %计算init_route的总距离
    end
    

交换操作

在这里插入图片描述

function route2 = swap(route1, i, j)
% 交换操作
% 比如说有6个城市,当前解为123456,随机选择两个位置,然后将这两个位置上的元素进行交换。
% 比如说,交换2和5两个位置上的元素,则交换后的解为153426。
% 输入 route1:初始路线
% 输入 i, j:两个交换点的位置索引
% 输出 route2:经过交换操作变换后的路线2

route2 = route1; % 复制初始路线到新路线

% 交换位置 i 和 j 上的元素
route2([i j]) = route1([j i]);

end

逆转操作

在这里插入图片描述

function route2=reversion(route1,i,j)
%% 逆转操作
%有6个城市,当前解为123456,我们随机选择两个位置,然后将这两个位置之间的元素进行逆序排列。
%比如说,逆转2和5之间的所有元素,则逆转后的解为154326。
%输入route1:          路线1
%输入i,j:             逆转点i,j
%输出route2:          经过逆转结构变换后的路线2
i1=min([i,j]);% 计算逆转起始位置的索引
i2=max([i,j]); % 计算逆转结束位置的索引
route2=route1;% 复制初始路线到新路线
% 使用逆序排列操作,将位置 i1 到 i2 之间的元素逆序排列
route2(i1:i2) = route1(i2:-1:i1);
end

插入操作

插入操作要分 i i i j j j的大小(如图已经很明了了)

在这里插入图片描述

function route2=insertion(route1,i,j)
%% 插入操作
%有6个城市,当前解为123456,我们随机选择两个位置,然后将这第一个位置上的元素插入到第二个元素后面。
%比如说,第一个选择5这个位置,第二个选择2这个位置,则插入后的解为125346。
%输入route1:          路线1
%输入i,j:             插入点i,j
%输出route2:          经过插入结构变换后的路线2
if i<j
     % 如果 i 小于 j,插入操作的位置在 i 和 j 之间
     %i左边的保留,i-j之间提前面去,i放j后面,j后面保留
     %i=2 j=5 123456→134526
    route2=route1([1:i-1,i+1:j,i,j+1:end]);
else
    % 如果 i 大于 j,插入操作的位置在 j 和 i 之间
    %j前面保留,i后面保留,j后面与i前面的整体的前面插入i
    %i=5 j=2 123456→125346
    route2 = route1([1:j, i, j+1:i-1, i+1:end]);
end
end

扰动操作

扰动操作用于生成一个局部最优解的邻域内的随机解,以探索可能的更优解。扰动操作是VNS算法中的一个重要步骤,用于引入随机性以打破当前局部最优解的限制,从而更广泛地搜索解空间。

扰动操作通常包括以下步骤:

  1. 选择一个邻域(neighborhood):首先,从算法定义的一组邻域中选择一个邻域,这个邻域可以是一种特定的局部搜索操作,例如交换操作、逆转操作、插入操作等。
  2. 选择一个解:从当前解中随机选择一个点或一组点,这些点将成为扰动操作的目标。选择的点数和方式取决于所选的邻域和问题的特性。
  3. 应用扰动:对所选择的解应用扰动操作,生成一个随机的解。扰动操作的具体实现方式取决于所选的邻域。例如,如果邻域是交换操作,那么可以随机选择两个点并交换它们的位置,从而生成一个新解。
  4. 返回扰动后的解:将生成的随机解作为扰动后的解返回,用于接下来的局部搜索或全局搜索。
function [route_shake, len_shake] = shaking(route, dist, k)
% 扰动,随机选择当前邻域中的一个解更新当前解
% 输入 route:一条路线
% 输入 dist:距离矩阵
% 输入 k:当前邻域序号
% 输出 route_shake:扰动操作后得到的路线
% 输出 len_shake:该条路线的距离

N = numel(route); % 城市数目

% 随机选择进行操作的两个点的序号,这两个点将被用于扰动操作
%在1-N中生成一个1×2的数组
select_no = randi([1, N], 1, 2);
i = select_no(1);
j = select_no(2);

if k == 1
    route_shake = swap(route, i, j); % 如果 k 等于 1,执行交换操作
elseif k == 2
    route_shake = reversion(route, i, j); % 如果 k 等于 2,执行逆转操作
else
    route_shake = insertion(route, i, j); % 否则,执行插入操作
end

len_shake = route_length(route_shake, dist); % 计算扰动后路线的总距离

end

路线距离计算

注意:计算时路线起点和终点需连接起来,也就是123456➡1234561,而下面的dist就是距离矩阵,在这之前已经用matlab自带的pdist函数计算了,再用squareform函数进行format distance matrix。

function len = route_length(route, dist)
% 计算一条路线总距离
% 输入 route:一条路线(城市编号的数组)
% 输入 dist:距离矩阵(城市之间的距离)
% 输出 len:该条路线总距离

n = numel(route); % 获取路线中城市的数量
route = [route route(1)]; % 将路线首尾连接,形成闭环

len = 0; % 初始化总距离为0

% 遍历路线上的每一对相邻城市,计算它们之间的距离并累加到总距离上
for k = 1:n
    i = route(k); % 当前城市的编号
    j = route(k+1); % 下一个城市的编号
    len = len + dist(i, j); % 累加距离
end

end

距离差值计算

距离差值计算需计算三种变换对应的结果,其实可以用route_length计算,但这样每次都会调用route_length函数,会增加计算时间,所以可以用下面这些函数来缩短计算时间

交换操作后的差值

交换的差值分7种情况(这里偷个懒 就不画这么多图了,哈哈)

在这里插入图片描述

%% 计算swap操作后与操作前路线route的总距离的差值
%输入route:            一条路线
%输入dist:             距离矩阵
%输入i,j:             交换点i,j
%输出delta1:           swap后路线的总距离-swap前路线的总距离
function delta1=cal_delta1(route,dist,i,j)
N=numel(route);         %城市数目
if (i==1)&&(j==N)
    delta1=-(dist(route(i),route(i+1))+dist(route(j-1),route(j)))+...
        (dist(route(j),route(i+1))+dist(route(j-1),route(i)));
elseif (i==1)&&(j==2)
    delta1=-(dist(route(N),route(i))+dist(route(j),route(j+1)))+...
        (dist(route(N),route(j))+dist(route(i),route(j+1)));
elseif i==1
    delta1=-(dist(route(N),route(i))+dist(route(i),route(i+1))+...
        dist(route(j-1),route(j))+dist(route(j),route(j+1)))+...
        (dist(route(N),route(j))+dist(route(j),route(i+1))+...
        dist(route(j-1),route(i))+dist(route(i),route(j+1)));
elseif (i==N-1)&&(j==N)
    delta1=-(dist(route(i-1),route(i))+dist(route(j),route(1)))+...
        (dist(route(i-1),route(j))+dist(route(i),route(1)));
elseif j==N
    delta1=-(dist(route(i-1),route(i))+dist(route(i),route(i+1))+...
        dist(route(j-1),route(j))+dist(route(j),route(1)))+...
        (dist(route(i-1),route(j))+dist(route(j),route(i+1))+...
        dist(route(j-1),route(i))+dist(route(i),route(1)));
elseif abs(i-j)==1
    delta1=-(dist(route(i-1),route(i))+dist(route(j),route(j+1)))+...
        (dist(route(i-1),route(j))+dist(route(i),route(j+1)));
else
    delta1=-(dist(route(i-1),route(i))+dist(route(i),route(i+1))+...
        dist(route(j-1),route(j))+dist(route(j),route(j+1)))+...
        (dist(route(i-1),route(j))+dist(route(j),route(i+1))+...
        dist(route(j-1),route(i))+dist(route(i),route(j+1)));
end
end

逆转操作后的差值

逆转的分情况讨论比较简单

%% 将给定的route序列在i和j位置之间进行逆序排列,然后计算转换序列前和转换序列后的路径距离的差值
%输入route:            一条路线
%输入dist:             距离矩阵
%输入i,j:             逆转点i,j
%输出delta2:           reversion后路线的总距离-reversion前路线的总距离
function delta2=cal_delta2(route,dist,i,j)
N=numel(route);                 %城市个数
if i==1
    if j==N
        delta2=0;
    else
        delta2=-dist(route(j),route(j+1))-dist(route(N),route(i))+...
            dist(route(i),route(j+1))+dist(route(N),route(j));
    end
else
    if j==N
        delta2=-dist(route(i-1),route(i))-dist(route(1),route(j))+...
            dist(route(i-1),route(j))+dist(route(i),route(1));
    else
        delta2=-dist(route(i-1),route(i))-dist(route(j),route(j+1))+...
            dist(route(i-1),route(j))+dist(route(i),route(j+1));
    end
end
end

插入操作后的差值

插入操作后的差值非常复杂,建议自己画个图来进行理解,这样更快

%% 计算insertion操作后与操作前路线route的总距离的差值
%输入route:            一条路线
%输入dist:             距离矩阵
%输入i,j:             逆转点i,j
%输出delta1:           insertion后路线的总距离-insertion前路线的总距离
function delta3=cal_delta3(route,dist,i,j)
N=numel(route);         %城市数目
if i<j
    if (i==1) && (j==N)
        delta3=0;
    elseif (i==1) && (j==2)
        delta3=-(dist(route(N),route(i))+dist(route(j),route(j+1)))+...
            (dist(route(N),route(j))+dist(route(i),route(j+1)));
    elseif i==1
        delta3=-(dist(route(N),route(i))+dist(route(i),route(i+1))+dist(route(j),route(j+1)))+...
            (dist(route(N),route(i+1))+dist(route(j),route(i))+dist(route(i),route(j+1)));
    elseif (i==N-1)&&(j==N)
        delta3=-(dist(route(i-1),route(i))+dist(route(j),route(1)))+...
            (dist(route(i-1),route(j))+dist(route(i),route(1)));
    elseif j==N
        delta3=-(dist(route(i-1),route(i))+dist(route(i),route(i+1))+dist(route(j),route(1)))+...
            (dist(route(i-1),route(i+1))+dist(route(j),route(i))+dist(route(i),route(1)));
    elseif (j-i)==1
        delta3=-(dist(route(i-1),route(i))+dist(route(j),route(j+1)))+...
            (dist(route(i-1),route(j))+dist(route(i),route(j+1)));
    else
        delta3=-(dist(route(i-1),route(i))+dist(route(i),route(i+1))+dist(route(j),route(j+1)))+...
            (dist(route(i-1),route(i+1))+dist(route(j),route(i))+dist(route(i),route(j+1)));
    end
else
    if (i==N) && (j==1)
        delta3=-(dist(route(i-1),route(i))+dist(route(j),route(j+1)))+...
            (dist(route(i-1),route(j))+dist(route(i),route(j+1)));
    elseif (i-j)==1
        delta3=0;
    elseif i==N
        delta3=-(dist(route(i-1),route(i))+dist(route(i),route(1))+dist(route(j),route(j+1)))+...
            (dist(route(i-1),route(1))+dist(route(j),route(i))+dist(route(i),route(j+1)));
    else
        delta3=-(dist(route(i-1),route(i))+dist(route(i),route(i+1))+dist(route(j),route(j+1)))+...
            (dist(route(i-1),route(i+1))+dist(route(j),route(i))+dist(route(i),route(j+1)));
    end
end
end

更新距离差值函数

跟上面的cal_delata函数可以写在一起,但这样只是为了方便

function Delta1=Update1(route,dist,i,j)
%% swap操作后生成新的距离差矩阵Delta
%输入route:            一条路线
%输入dist:             距离矩阵
%输入i,j:             交换点i,j
%输出Delta1:           swap操作后的距离差值的矩阵
N=numel(route);                     %城市个数
route2=swap(route,i,j);             %交换route上i和j两个位置上的城市
Delta1=zeros(N,N);                  %N行N列的Delta初始化,每个位置上的元素是距离差值
for i = 1:N-1
    for j = i+1:N
        Delta1(i,j)=cal_delta1(route2,dist,i,j);
    end
end
function Delta2=Update2(route,dist,i,j)
%% reversion操作后生成新的距离差矩阵Delta
%输入route:            一条路线
%输入dist:             距离矩阵
%输入i,j:             逆转点i,j
%输出Delta2:           reversion操作后的距离差值的矩阵
N=numel(route);                     %城市个数
route2=reversion(route,i,j);        %逆转route上i和j两个位置上的城市
Delta2=zeros(N,N);                  %N行N列的Delta初始化,每个位置上的元素是距离差值
for i=1:N-1
    for j=i+1:N
        Delta2(i,j)=cal_delta2(route2,dist,i,j);
    end
end
%% insertion操作后生成新的距离差矩阵Delta
%输入route:            一条路线
%输入dist:             距离矩阵
%输入i,j:             插入点i,j
%输出Delta1:           insertion操作后的距离差值的矩阵
function Delta3=Update3(route,dist,i,j)
N=numel(route);                     %城市个数
route2=insertion(route,i,j);        %插入route上i和j两个位置上的城市
Delta3=zeros(N,N);                  %N行N列的Delta初始化,每个位置上的元素是距离差值
for i=1:N
    for j=1:N
        if i~=j
            Delta3(i,j)=cal_delta3(route2,dist,i,j);
        end
    end
end

交换邻域搜索

在这里插入图片描述

function [reversion_route,reversion_len]=reversion_neighbor(route,dist,M)
%% 对route不断进行逆转操作后所得到的路线以及所对应的总距离
%输入route:           一条路线
%输入dist:            距离矩阵
%输入M:               最多进行M次邻域操作
%输出reversion_route: 对route不断进行逆转操作后所得到的路线
%输出reversion_len:   reversion_route的总距离
N=numel(route);         %城市数目
Delta2=zeros(N,N);      %逆转任意两个位置之间序列的元素所产的距离差的矩阵
for i=1:N-1
    for j=i+1:N
        Delta2(i,j)=cal_delta2(route,dist,i,j);
    end
end
cur_route=route;                                                    %初始化当前路线
m=1;                                                                %初始化计数器
while m<=M
    min_value=min(min(Delta2));                                     %找出距离差值矩阵中最小的距离差值
    %如果min_value小于0,才能更新当前路线和距离矩阵。否则,终止循环
    if min_value<0
        [min_row,min_col]=find(Delta2==min_value);                  %找出距离差值矩阵中最小的距离差值所对应的行和列
        Delta2=Update2(cur_route,dist,min_row(1),min_col(1));       %更新距离差值矩阵
        cur_route=reversion(cur_route,min_row(1),min_col(1));       %更新当前路线
    else
        break
    end
    m=m+1;
end
reversion_route=cur_route;                                          %将当前路线cur_route赋值给reversion_route
reversion_len=route_length(reversion_route,dist);                   %reversion_route的总距离
end
function [swap_route,swap_len]=swap_neighbor(route,dist,M)
%% 对route不断进行交换操作后所得到的路线以及所对应的总距离
%输入route:           一条路线
%输入dist:            距离矩阵
%输入M:               最多进行邻域操作的次数
%输出swap_route:      对route不断进行交换操作后所得到的路线
%输出swap_len:        swap_route的总距离
N=numel(route);         %城市数目
Delta1=zeros(N,N);      %交换任意两个位置之间序列的元素所产的距离差的矩阵
for i = 1:N-1
    for j = i+1:N
        Delta1(i,j)=cal_delta1(route,dist,i,j);
    end
end
cur_route=route;
m=1;
while m<=M
    min_value=min(min(Delta1));
    %如果min_value小于0,才能更新当前路线和距离矩阵。否则,终止循环
    if min_value<0
        [min_row,min_col]=find(Delta1==min_value); % 找出距离差值矩阵中最小的距离差值所对应的行和列
        Delta1=Update1(cur_route,dist,min_row(1),min_col(1));
        cur_route=swap(cur_route,min_row(1),min_col(1));
    else
        break
    end
    m=m+1;
end
swap_route=cur_route;
swap_len=route_length(swap_route,dist);
end
function [insertion_route,insertion_len]=insertion_neighbor(route,dist,M)
%% 对route不断进行插入操作后所得到的路线以及所对应的总距离
%输入route:           一条路线
%输入dist:            距离矩阵
%输入M:               最多进行M次邻域操作
%输出insertion_route: 对route不断进行插入操作后所得到的路线
%输出insertion_len:   insertion_route的总距离
N=numel(route);         %城市数目
Delta3=zeros(N,N);      %插入任意两个位置之间序列的元素所产的距离差的矩阵
for i=1:N-1
    for j=i+1:N
        Delta3(i,j)=cal_delta3(route,dist,i,j);
    end
end
cur_route=route;                                                    %初始化当前路线
m=1;                                                                %初始化计数器
while m<=M
    min_value=min(min(Delta3));                                     %找出距离差值矩阵中最小的距离差值
    %如果min_value小于0,才能更新当前路线和距离矩阵。否则,终止循环
    if min_value<0
        [min_row,min_col]=find(Delta3==min_value);                  %找出距离差值矩阵中最小的距离差值所对应的行和列
        Delta3=Update3(cur_route,dist,min_row(1),min_col(1));       %更新距离差值矩阵
        cur_route=insertion(cur_route,min_row(1),min_col(1));       %更新当前路线
    else
        break
    end
    m=m+1;
end
insertion_route=cur_route;                                          %将当前路线cur_route赋值给insertion_route
insertion_len=route_length(insertion_route,dist);                   %insertion_route的总距离
end

主函数

流程图后面补,今天脑子累

tic
clear
clc
close all
%% 输入数据
dataset=importdata('input.txt');                %数据中,每一列的含义分别为[序号,x坐标,y坐标]
x=dataset(:,2);                                 %x坐标
y=dataset(:,3);                                 %y坐标
vertexes=dataset(:,2:3);                        %提取各个城市的xy坐标
N=size(dataset,1);                              %城市数目
h=pdist(vertexes);                              %计算各个城市之间的距离,一共有1+2+......+(n-1)=n*(n-1)/2个
dist=squareform(h);                             %将各个城市之间的距离转换为n行n列的距离矩阵
%% 参数初始化
MAXGEN=1000;                                      %外层最大迭代次数
M=60;                                           %最多进行M次邻域操作
n=7;                                            %邻域数目
%% 构造初始解
[init_route,init_len]=construct_route(dist);    %贪婪构造初始解
disp(['初始路线总距离为',num2str(init_len)]);
cur_route=init_route;
best_route=cur_route;
best_len=route_length(cur_route,dist);
BestL=zeros(MAXGEN,1);                          %记录每次迭代过程中全局最优个体的总距离
%% 主循环
gen=1;                                          %外层计数器
while gen<=MAXGEN
    k=1;
    while(1)
        switch k
            case 1
                cur_route=shaking(cur_route,dist,k);
                [swap_route,swap_len]=swap_neighbor(cur_route,dist,M);
                cur_len=swap_len;
                if cur_len<best_len
                    cur_route=swap_route;
                    best_len=cur_len;
                    best_route=swap_route;
                    k=0;
                end
            case 2
                cur_route=shaking(cur_route,dist,k);
                [reversion_route,reversion_len]=reversion_neighbor(cur_route,dist,M);
                cur_len=reversion_len;
                if cur_len<best_len
                    cur_route=reversion_route;
                    best_len=cur_len;
                    best_route=reversion_route;
                    k=0;
                end
            case 3
                cur_route=shaking(cur_route,dist,k);
                [insertion_route,insertion_len]=insertion_neighbor(cur_route,dist,M);
                cur_len=insertion_len;
                if cur_len<best_len
                    cur_route=insertion_route;
                    best_len=cur_len;
                    best_route=insertion_route;
                    k=0;
                end
            otherwise
                break;
        end
        k=k+1;
    end
    disp(['第',num2str(gen),'代最优路线总距离为',num2str(best_len)]);
    BestL(gen,1)=best_len;
    %% 计数器加1
    gen=gen+1;
end
%% 画出优化过程图
figure;
plot(BestL,'LineWidth',1);
title('优化过程')
xlabel('迭代次数');
ylabel('总距离');
%% 画出全局最优路线图
plot_route(best_route,x,y);

toc

图我用figurebest修了下,有意者可以关注下图通道这个公众号,非常好用

在这里插入图片描述

在这里插入图片描述
`

图我用figurebest修了下,有意者可以关注下图通道这个公众号,非常好用

  • 17
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值