基于遗传算法的TSP算法(附代码)

该博客详细介绍了TSP问题的解决思路,通过遗传算法编码、种群初始化、适应度函数、选择、交叉、变异和进化逆转操作来寻找14城市间的最短路径。重点讨论了如何通过精英策略、进化逆转操作优化算法性能,以及优化结果的显著改进。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

TSP (traveling salesman problem,旅行商问题)是典型的NP完全问题,即其最坏情况下的时间复杂度随着问题规模的增大按指数方式增长,到目前为止还未找到一个多项式时间的有效算法。

一、问题描述

        本案例以14个城市为例,假定14个城市的位置坐标如表4 - 1所列。寻找出一条最短的遍历14个城市的路径。

二、解决思路及步骤

1、算法流程图

        

2、遗传算法实现及部分代码

(1)编码

        采用整数排列编码方法。对于 n 个城市的 TSP 问题,染色体分为 n 段,其中每一段为对应城市的编号,如对 10 个城市的 TSP 问题 {1,2,3,4,5,6,7,8,9,10},则 | 1 | 10 | 2 | 4 | 5 | 6 | 8 | 7 | 9 | 3 就是一个合法的染色体。

(2)种群初始化

        在完成染色体编码以后,必须产生一个初始种群作为起始解,所以首先需要决定初始化种群的数目。初始化种群的数目一般根据经验得到,一般情况下种群的数量视城市规模的大小而确定,其取值在 50~200 之间浮动。种群初始化函数 InitPop 代码为:

function Chrom = InitPop(NIND, N)
%% 初始化种群
% 输入:
% NIND: 种群大小
% N:    个体染色体长度(这里为城市的个数)
% 输出:
% 初始种群
Chrom = zeros(NIND, N);             %用于存储种群
for i = 1 : NIND
    Chrom(i, :) = randperm(N);      %随机生成初始种群
end

(3)适应度函数

 求种群个体的适应度函数 Fitness 的代码如下:

function FitnV = Fitness(len)
%% 适应度函数
% 输入:
% len:      个体的长度(TSP的距离)
% 输出:
% FitnV:    个体的适应度值
FitnV = 1 ./ len;

(4)选择操作

        选择操作即从旧群体中以一定概率选择个体到新群体中,个体被选中的概率跟适应度值有关,个体适应度值越大,被选中的概率越大。

选择操作函数 Select 的代码为:

function SelCh = Select(Chrom, FitnV, GGAP)
%% 选择操作
% 输入:
% Chrom:    种群
% FitnV:    适应度值
% GGAP:     选择概率
% 输出:
% SelCh:    被选择的个体
NIND = size(Chrom, 1);
NSel = max(floor(NIND * GGAP + .5), 2);
ChrIx = Sus(FitnV, NSel);
SelCh = Chrom(ChrIx, :);
end

其中,函数 Sus 的代码为:

function NewChrIx = Sus(FitnV, Nsel)
%% Select(选择)调用
% 输入:
% FitnV:    个体的适应度值
% Nsel:     被选择个体的数目
% 输出:
% NewChrIx: 被选择个体的索引号
[Nind, ~] = size(FitnV);
cumfit = cumsum(FitnV);                 % cumsum:向量累计和
trials = cumfit(Nind) / Nsel * (rand + (0: Nsel - 1)');
Mf = cumfit(:, ones(1, Nsel));
Mt = trials(:, ones(1, Nind))';
[NewChrIx, ~] = find(Mt < Mf & [zeros(1, Nsel); Mf(1: Nind - 1, :)] <= Mt);
[~, shuf] = sort(rand(Nsel, 1));
NewChrIx = NewChrIx(shuf);
end

(5)交叉操作

        采用部分映射杂交,确定交叉操作的父代,将父代样本两两分组,每组重复以下过程(假定城市数为10):

        1.产生 [1,10] 区间内的随机整数 r1 和 r2 ,确定两个位置,对两位置的中间数据进行交叉

        2.交叉后,同一个个体中有重复的城市编号,不重复的数字保留,有冲突的数字采用部分映射的方法消除冲突,即利用中间段的对应关系进行映射。

交叉操作函数 Recombin 的代码为:

function SelCh = Recombin(SelCh, Pc)
%% 交叉操作
% 输入:
% SelCh:    被选择的个体
% Pc:       交叉概率
% 输出:
% SelCh:    交叉后的个体
NSel = size(SelCh, 1);
for i = 1 : 2 : NSel - mod(NSel, 2)
    if Pc >= rand
        [SelCh(i, :), SelCh(i+1, :)] = intercross(SelCh(i, :), SelCh(i+1, :));
    end
end

其中,函数 intercross 的代码为:

function [a, b] = intercross(a, b)
%% Recombin(交叉)调用
% 输入:
% a和b为两个待交叉的个体
% 输出:
% a和b为交叉后得到的两个个体
L = length(a);
r1 = randsrc(1, 1, [1: L]);             % 随机生成1到L的整数
r2 = randsrc(1, 1, [1: L]);
if r1 ~= r2
    a0 = a; b0 = b;
    s = min([r1, r2]);
    e = max([r1, r2]);
    for i = s: e
        a1 = a; b1 = b;
        a(i) = b0(i);                   % 交换r1到r2之间的数
        b(i) = a0(i);
        x = find(a == a(i));            % 查找交换后相同值的位置
        y = find(b == b(i));
        i1 = x(x ~= i);
        i2 = y(y ~= i);
        if ~ isempty(i1)                % x值与i值不相等
            a(i1) = a1(i);
        end
        if ~isempty(i2)
            b(i2) = b1(i);
        end
    end
end

(6)变异操作

        变异策略采取随机选取两个点,将其对换位置。产生两个 [1,10] 范围内的随机整数 r1 和 r2 ,确定两个位置,将其对换位置。

变异操作函数 Mutate 的代码如下:

function SelCh = Mutate(SelCh, Pm)
%% 变异操作
% 输入:
% SelCh:    被选择的个体
% Pm:       变异概率
% 输出:
% SelCh:    变异后的个体
[NSel, L] = size(SelCh);
for i = 1: NSel
    if Pm >= rand
        R = randperm(L);    % 生成1到L没有重复元素的随机整数行向量
        SelCh(i, R(1: 2)) = SelCh(i, R(2: -1: 1));
    end
end

(7)进化逆转操作

        为改善遗传算法的局部搜索能力,在选择、交叉、变异之后引进连续多次的进化逆转操作。这里的 “进化” 是指逆转算子的单方向性,即只有经逆转后,适应度值有提高的才接受下来,否则逆转无效。产生两个 [1,10] 区间内的随机整数 r1 和 r2,确定两个位置,将其对换位置。

        对每个个体进行交叉变异,然后代入适应度函数进行评估,x 选择出适应值大的个体进行下一代的交叉和变异以及进化逆转操作。循环操作:判断是否满足设定的最大遗传代数 MAXGEN,不满足则跳入适应度值的计算;否则,结束遗传操作

进行逆转函数 Reverse 的代码为:

function SelCh = Reverse(SelCh, D)
%% 进化逆转函数
% 输入:
% SelCh:        被选择的个体
% D:            各城市的距离矩阵
% 输出:
% SelCh:        进化逆转后的个体
[row, col] = size(SelCh);
ObjV = PathLength(D, SelCh);            % 计算路线长度
SelCh1 = SelCh;
for i = 1: row
    r1 = randsrc(1, 1, [1: col]);
    r2 = randsrc(1, 1, [1: col]);
    mininverse = min([r1, r2]);
    maxinverse = max([r1, r2]);
    SelCh1(i, mininverse: maxinverse) = SelCh1(i, maxinverse: -1: mininverse);
end
ObjV1 = PathLength(D, SelCh1);          % 计算路线长度
index = ObjV1 < ObjV;
SelCh(index, :) = SelCh1(index, :);

3、遗传算法主函数

clear; clc
close all
load('CityPosition1.mat')               % 各城市坐标
NIND = 100;                             % 种群大小
MAXGEN = 200;
Pc = 0.9;                               % 交叉概率
Pm = 0.05;                              % 变异概率
GGAP = 0.9;                             % 代沟
D = Distance(X);                        % 生成距离矩阵
N = size(D, 1);                         % (34 * 34)
%% 初始化种群
Chrom = InitPop(NIND, N);
%% 在二维图上画出所有坐标点
% figure
% plot(X(:, 1), X(:, 2), 'o');
%% 画出随机解的线路图
DrawPath(Chrom(1, :), X);
pause(0.0001)
%% 输出随机解的线路和总距离
disp('初始种群中的一个随机值:')
OutputPath(Chrom(1, :));
Rlength = PathLength(D, Chrom(1, :));
disp(['总距离:', num2str(Rlength)]);
disp('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
%% 优化
gen = 0;
figure;
hold on; box on
xlim([0, MAXGEN])
title('优化过程')
xlabel('代数')
ylabel('最优值')
ObjV = PathLength(D, Chrom);                % 计算路线长度
preObjV = min(ObjV);
while gen < MAXGEN
    %% 计算适应度
    ObjV = PathLength(D, Chrom);
    line([gen - 1, gen], [preObjV, min(ObjV)]); pause(0.0001)
    preObjV = min(ObjV);
    FitnV = Fitness(ObjV);
    %% 选择
    SelCh = Select(Chrom, FitnV, GGAP);
    %% 交叉操作
    SelCh = Recombin(SelCh, Pc);
    %% 变异
    SelCh = Mutate(SelCh, Pm);
    %% 逆转操作
    SelCh = Reverse(SelCh, D);
    %% 重插入子代的新种群
    Chrom = Reins(Chrom, SelCh, ObjV);
    %% 更新迭代次数
    gen = gen + 1;
end
%% 画出最优解的路线图
ObjV = PathLength(D, Chrom);
[minObjV, minInd] = min(ObjV);
DrawPath(Chrom(minInd(1), :), X)
%% 输出最优解的路线和总距离
disp('最优解:')
p = OutputPath(Chrom(minInd(1), :));
disp(['总距离:', num2str(ObjV(minInd(1)))]);
disp('-------------------------------------------------------------')

主函数里以下部分函数为自己编制的:

function DrawPath:输出路线函数

function PathLength:计算各个体的路径长度

function Distance:计算两两城市之间的距离

function OutputPath:输出路线函数

function Reins:重插入子代的新种群

function dsxy2figxy:坐标转换

三、结果分析与改进

1、结果分析 

        优化前的一个随机路线轨迹图

初始种群中的一个随机值:
6->3->11->7->14->8->5->1->2->4->13->9->10->12->6
总距离为:66.607

        优化后的路线图: 

最优解:
13->7->12->6->5->4->3->14->2->1->10->9->11->8->13
总距离:29.3405

        优化迭代图: 

由进化图可以看出,优化前后路径长度得到很大改进,80代以后路径长度已经保持不变了,可以认为已经是最优解了,总距离由优化前的 66.607 变为 29.3405,减为原来的44.05%。

 2、遗传算法的改进

(1)使用精英策略

        子代种群中的最优个体永远不会比父代最优的个体差,这样使得父代的好的个体不至于由于交叉或者变异操作而丢失

(2)使用进化逆转操作

        在本文的编码中,每一个染色体即对应一个 TSP 环游,如果染色体码串的顺序发生变化,则环游路径也随之改变。因此,TSP 问题解的关键地方就是码串的顺序。对照文中的交叉算子,可以发现,纵使两个亲代完全相同,通过交叉,仍然会产生不同于亲代的子代,且子代的码串排列顺序与亲代有较大的差异。交叉算子的这种变异效果所起的作用有两个方面,一方面它能起到维持群体内一定的多样性的作用,避免陷入局部最优解;但是另一方面,它却不利于子代继承亲代的较多信息,特别是当进化过程进入到后期,群体空间中充斥着大量的高适应度个体,交叉操作对亲代的较优基因破坏很大,使子代难以继承到亲代的优良基因,从而使交叉算子的搜索能力大大降低。

(3)算法的局限性

        当问题规模n比较小时,得到的一般都是最优解;当规模比较大时,一般只能得到近似解。这时可以通过增大种群大小和增加最大遗传代数使得优化值更接近最优解。

完整代码数据

链接:https://pan.baidu.com/s/11lGzWJ8JdX-o1HseVNj2NA 
提取码:4zc5 
--来自百度网盘超级会员V3的分享

<think>好的,我现在需要解决用户提出的关于TSP问题的几个算法实现以及思考题。首先,用户要求用贪婪算法、最邻近插入算法以及2-Opt算法求解29个城市的TSP问题,并且用Python实现。另外还有两个思考题需要解释子回路约束和使用神经网络或动态规划的方法。我得一步步来分析每个部分该如何处理。 首先,关于第一个问题,用贪婪算法求解TSP。贪婪算法通常指的是每次选择最近的未访问城市作为下一步,直到所有城市都被访问,然后回到起点。这种方法的缺点是容易陷入局部最优,但对于小规模问题可能有效。实现的话,需要读取城市坐标,计算距离矩阵,然后从某个起点开始,逐步选择最近的城市。需要注意的是,贪婪算法可能会有多个局部最优解,所以可能需要多次运行或者结合其他策略。 接下来是最邻近插入算法。这个算法可能指的是Nearest Neighbor Insertion,属于构造型算法的一种。基本步骤是:先构建一个初始子回路(比如三个城市),然后在剩下的城市中找到离当前回路中任一城市最近的城市,将其插入到回路中代价最小的位置。重复直到所有城市都被插入。实现时需要维护当前回路,并计算每次插入的位置,使得总距离增加最少。这里的关键是找到插入位置的最小增量,可能需要遍历所有可能的插入点。 然后是2-Opt算法对前两个算法的结果进行改进。2-Opt是一种局部搜索算法,通过交换路径中的两条边来消除交叉,从而减少总距离。具体来说,遍历所有可能的边对,反转它们之间的路径,如果总距离减少则保留改变。这个过程需要重复直到无法进一步改进。实现时需要编写一个函数,对现有路径进行2-Opt优化,可能需要多次迭代才能收敛。 关于思考题,消除子回路的约束在TSP模型中通常通过子回路消除约束来实现,比如在整数线性规划中使用DFJ(Dantzig-Fulkerson-Johnson)约束,或者MTZ(Miller-Tucker-Zemlin)约束。需要解释这些约束如何防止形成多个不连通的回路,确保整个路径是一个单一的环。 最后,用神经网络或动态规划来求解。动态规划对于29个城市来说,状态空间是O(n^2*2^n),29个城市的话,2^29大约是5亿,可能内存不够,但或许可以优化。而神经网络方面,可能指的是使用如神经网络的近似方法,或者最近的一些基于深度学习的TSP解法,比如指针网络(Pointer Networks)或者强化学习方法。不过这部分实现起来比较复杂,可能需要使用深度学习框架如TensorFlow或PyTorch。 现在,针对编程实现部分,需要先处理数据读取。用户给出的数据格式是“编号 横坐标 纵坐标”存储在文本文件中。Python中可以用numpy读取,存储为坐标列表。然后计算距离矩阵,这里需要注意使用欧氏距离,并可能四舍五入为整数,或者保留浮点数,视情况而定。 对于贪婪算法,步骤大致如下: 1. 选择起始城市(比如第一个城市)。 2. 从当前城市出发,选择最近的未访问城市作为下一个城市。 3. 重复直到所有城市都被访问,然后回到起点。 需要处理多个可能的起点吗?或者固定一个起点?这可能会影响结果,但用户没有特别说明,可能固定一个即可。 最邻近插入算法的实现步骤: 1. 初始化一个子回路,比如三个城市形成的三角形。 2. 找到剩余城市中距离当前子回路中任一城市最近的城市。 3. 在该子回路中找到插入该城市后总增加距离最小的位置,插入。 4. 重复直到所有城市都被插入。 这里需要注意,如何高效地找到插入位置的最小增量。可能需要遍历子回路的每一条可能的边,计算插入后的距离变化。 对于2-Opt优化,实现步骤: 1. 对于给定的路径,遍历所有可能的i和j(i < j)。 2. 反转路径中i到j的部分,计算新的总距离。 3. 如果新距离更短,则保留这个改变,并继续搜索。 4. 重复直到没有改进为止。 需要注意的是,2-Opt需要多次迭代,直到无法进一步优化。可能需要对每个边进行多次检查。 在Python实现中,路径可以用列表表示,距离计算可以预先计算好距离矩阵,以加快速度。对于动态规划,可能无法直接处理29个城市,但可以尝试状态压缩,比如使用bitmask表示访问过的城市集合。但29位的bitmask需要处理大约536,870,912种状态,每个状态需要存储n个可能的最后访问城市,所以总的状态数是n*2^n,即29*2^29≈15,5亿,这显然在内存上不可行。因此动态规划可能不适用于29个城市,除非进行空间优化或者使用迭代加深的方法。而用户可能期望的是实现动态规划,但需要指出其局限性。 关于神经网络的方法,可以尝试使用已有的库或框架,例如基于注意力机制的模型,或者使用已有的开源实现。但这可能需要较多的代码和训练时间,对于29个城市来说,可能不太实际,尤其是用户希望立即得到结果而不是训练模型。所以可能需要采用另一种方法,或者使用启发式算法代替。 现在,针对代码部分的结构: 1. 读取数据:读取文本文件,存储为城市坐标列表。 2. 计算距离矩阵:使用欧氏距离,存储为二维数组。 3. 实现贪婪算法:构建路径。 4. 实现最邻近插入算法:构建路径。 5. 对两种算法的结果应用2-Opt优化。 6. 绘制路径图。 在代码中,需要注意效率问题,尤其是在计算距离和进行2-Opt优化时。可能需要使用numpy来加速矩阵运算。 对于思考题中的消除子回路约束,需要解释在整数规划模型中,如何通过添加约束条件来防止子回路的形成。比如DFJ约束要求,对于每个子集S(非空且不等于全部城市),进入该子集的边数至少为1,这防止了形成独立的回路。而MTZ约束则是通过引入辅助变量u_i,使得对于每条边(i,j),如果i在路径中出现在j之前,则u_j >= u_i +1,这样避免了回路的形成。 对于动态规划的尝试,可以编写一个记忆化递归函数,但由于状态数太大,可能需要使用位运算和字典来存储状态,但这仍然可能无法在合理的时间内完成。所以可能需要指出动态规划不适用于n=29的情况,或者寻找其他优化方法。 综上所述,我需要分步骤编写Python代码,实现各个算法,并确保正确性。同时,在解释思考题时,要简明扼要地说明约束条件的作用和原理。在编程实现时,需要注意处理城市坐标、计算距离矩阵、实现各算法逻辑,并进行结果的可视化。</think>以下分步骤回答您的问题: 一、编程实现部分 (代码实现需配合城市坐标文件,此处给出核心代码结构) ```python import numpy as np import matplotlib.pyplot as plt # 数据读取 def read_coords(filename): cities = [] with open(filename) as f: for line in f: idx, x, y = map(float, line.strip().split()) cities.append((x, y)) return np.array(cities) # 距离矩阵计算 def calc_dist_matrix(cities): n = len(cities) dist = np.zeros((n, n)) for i in range(n): for j in range(n): dist[i,j] = np.linalg.norm(cities[i]-cities[j]) return dist # 贪心算法 def greedy_tsp(dist_matrix): n = dist_matrix.shape[0] unvisited = set(range(n)) path = [0] unvisited.remove(0) while unvisited: current = path[-1] nearest = min(unvisited, key=lambda x: dist_matrix[current][x]) path.append(nearest) unvisited.remove(nearest) return path + [0] # 返回起点形成闭环 # 最邻近插入算法 def nearest_insertion(dist_matrix): n = dist_matrix.shape[0] unvisited = set(range(n)) # 初始化三个城市形成子回路 path = [0, 1, 2, 0] unvisited -= {0,1,2} while unvisited: # 找最近城市 min_dist = float('inf') for city in unvisited: for node in path[:-1]: if dist_matrix[node][city] < min_dist: min_dist = dist_matrix[node][city] insert_city = city from_node = node # 找最佳插入位置 min_increase = float('inf') for i in range(len(path)-1): a, b = path[i], path[i+1] increase = dist_matrix[a][insert_city] + dist_matrix[insert_city][b] - dist_matrix[a][b] if increase < min_increase: min_increase = increase insert_pos = i+1 path.insert(insert_pos, insert_city) unvisited.remove(insert_city) return path # 2-Opt优化 def two_opt_improve(path, dist_matrix): improved = True n = len(path) best_path = path best_distance = sum(dist_matrix[best_path[i], best_path[i+1]] for i in range(n-1)) while improved: improved = False for i in range(1, n-2): for j in range(i+1, n-1): new_path = path[:i] + path[i:j+1][::-1] + path[j+1:] new_distance = sum(dist_matrix[new_path[k], new_path[k+1]] for k in range(len(new_path)-1)) if new_distance < best_distance: best_path = new_path best_distance = new_distance improved = True path = best_path return best_path # 结果可视化 def plot_route(cities, path, title): plt.figure(figsize=(10,6)) plt.scatter(cities[:,0], cities[:,1]) for i, (x, y) in enumerate(cities): plt.text(x, y, str(i)) route = cities[path] plt.plot(route[:,0], route[:,1], 'r-') plt.title(title) plt.show() # 主程序 if __name__ == "__main__": cities = read_coords("cities.txt") dist_matrix = calc_dist_matrix(cities) # 贪心算法 greedy_path = greedy_tsp(dist_matrix) print("Greedy path:", greedy_path) plot_route(cities, greedy_path, "Greedy Algorithm") # 最邻近插入 ni_path = nearest_insertion(dist_matrix) print("Nearest Insertion path:", ni_path) plot_route(cities, ni_path, "Nearest Insertion") # 2-Opt优化 opt_greedy = two_opt_improve(greedy_path, dist_matrix) opt_ni = two_opt_improve(ni_path, dist_matrix) plot_route(cities, opt_greedy, "2-Opt Improved Greedy") plot_route(cities, opt_ni, "2-Opt Improved NI") ``` 二、思考题解答 1. **子回路消除约束解释**: 在TSP的整数规划模型中,常用两种方式消除子回路: - **DFJ约束**:对于任意非空真子集$S \subset V$,要求进入子集的边数$\geq 1$ 数学表达式:$\sum_{i \in S} \sum_{j \notin S} x_{ij} \geq 1$ - **MTZ约束**:引入辅助变量$u_i$表示访问顺序,对每条边$(i,j)$有: $$u_j \geq u_i + 1 - n(1 - x_{ij}), \quad \forall i,j \in V \setminus \{0\}$$ 通过约束变量间的逻辑关系,确保路径连续性 2. **神经网络/动态规划实现建议**: - 动态规划实现(适用性警告:29城市需要约$29^2 \times 2^{29} \approx 1.5 \times 10^{10}$个状态,需使用状态压缩): ```python def dp_tsp(dist): n = len(dist) memo = {} def visit(state, pos): if state == (1 << n) - 1: return dist[pos][0] if (state, pos) in memo: return memo[(state, pos)] min_cost = float('inf') for city in range(n): if not (state & (1 << city)): new_state = state | (1 << city) cost = dist[pos][city] + visit(new_state, city) if cost < min_cost: min_cost = cost memo[(state, pos)] = min_cost return min_cost return visit(1, 0) # 从城市0出发 ``` - 神经网络实现建议:使用图神经网络(如GCN)或Transformer架构,采用强化学习策略梯度方法进行训练。 三、算法对比 $$ \text{典型算法复杂度对比} $$ | 算法 | 时间复杂度 | 适用规模 | |------|-----------|---------| | 贪心算法 | $O(n^2)$ | 中小规模 | | 最邻近插入 | $O(n^3)$ | 中等规模 | | 2-Opt | $O(n^2 \cdot k)$ | 局部优化 | | 动态规划 | $O(n^2 2^n)$ | n≤20 | | 神经网络 | 训练成本高 | 需大量数据 | 建议:实际应用时可组合使用构造算法(如贪心)与优化算法(如2-Opt)形成混合策略。
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hi,你好啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值