简介:本资源是面向MATLAB初学者与图论算法学习者的克鲁斯卡尔(Kruskal)最小生成树算法完整实现包,适用于课程设计、算法实践及网络优化类项目开发。资源包含4个功能明确的.m文件:主算法脚本kruskal.m实现边排序、并查集判环与MST构建;fysalida.m与connected.m分别提供图连通性检测与辅助结构初始化;iscycle.m用于环路判定,共同构成可直接运行、逻辑清晰、注释友好的轻量级算法套件。压缩包仅2KB,全部为纯MATLAB源码,无依赖项,开箱即用。目前已有409人学习下载,适合需要理解并查集原理、掌握MATLAB图算法编程范式、快速验证最小生成树求解过程的学习者,尤其利于课堂演示、课设调试与算法对比实验。
1. 克鲁斯卡尔算法在 MATLAB 中不是“调个函数就完事”:它是一套需手动建模、显式排序、动态判环的最小生成树构建逻辑,专治图稀疏、边权离散、需全程可追溯的工程场景
你手头有一张含 200 个节点、800 条边的拓扑图,边权是实测通信延迟(单位 ms),要求选出总延迟最低的连通骨架——这不是一个graphminspantree黑盒能直接交差的问题。MATLAB 官方图论工具箱虽提供minspantree,但其底层封装了边排序与并查集细节,无法暴露每一步选边依据、无法干预环判定阈值、更无法在选边中途插入业务约束(比如“禁止连接节点 A 和 B”)。而克鲁斯卡尔算法的本质,恰恰是把最小生成树(MST)拆解为「按权升序排列 → 逐条试探加入 → 并查集实时判环 → 满足 n−1 条即停」四步可审计流程。本资源包正是这样一套完全手写、无外部依赖、带完整调试桩、支持自定义边过滤与中断条件的 MATLAB 实现。它不追求速度极限(对超大规模图建议转 C++),但保证每一行代码对应一个算法步骤,每一条输出日志可回溯到原始边索引。适合某高校图论课程设计、某实验室网络拓扑优化验证、或某公司嵌入式设备通信路径预规划等需要“过程透明、结果可复现、参数可微调”的真实场景。
2. 算法原理与 MATLAB 实现选型:为什么不用minspantree,而坚持手写并查集 + 显式排序?
2.1 克鲁斯卡尔算法的不可替代性:从理论步骤到工程落地的断层
克鲁斯卡尔算法的理论描述极简:
- 将所有边按权重升序排列;
- 初始化并查集,每个顶点为独立集合;
- 遍历排序后边表,若当前边两端点不在同一集合,则合并两集合并收录该边;
- 收录边数达 $n-1$ 时终止。
但 MATLAB 工程落地时,这四步存在三处关键断层:
- 断层一:排序稳定性—— 当多条边权相等时,
sortrows(E,3)默认按行号次序,但实际业务中可能需按节点 ID 升序二次排序(如权相同时优先选编号小的节点),官方函数不提供此钩子; - 断层二:环判定黑匣子——
minspantree内部用优化版并查集,但无法返回“第 k 条边因成环被跳过”的具体原因,调试时只能看到最终结果,无法定位是哪条边触发了隐式合并; - 断层三:动态约束缺失—— 实际项目常需“跳过权值 >50 的边”或“强制包含边 (i,j)”,这些必须在遍历循环内插入手动判断,而非调用前预处理图结构。
因此,本实现选择完全自主编码:用结构体数组edges存边(含from,to,weight,idx字段),用unionfind类封装并查集,用cellfun+str2double处理混合类型输入,确保每一步都可打断、可打印、可修改。
2.2 并查集的 MATLAB 面向对象实现:避免数组索引越界与路径压缩失效
MATLAB 中并查集易错点在于:
- 使用普通数组
parent(1:n)时,若节点 ID 不连续(如节点为[1,3,5,7]),直接索引会报错; - 路径压缩若仅递归
parent(x)=find(parent(x)),在大图中易触发栈溢出(MATLAB 默认递归深度 500); - 合并操作未检查秩(rank),导致树退化为链表,$O(\alpha(n))$ 变 $O(n)$。
本资源采用classdef unionfind封装,核心设计如下:
classdef unionfind properties (Access = private) parent % double array, size n, parent(i) = root of i rank % double array, size n, rank(i) = height of tree rooted at i n % number of nodes end methods function obj = unionfind(nodeIDs) % nodeIDs: [1,3,5,7] or {'A','B','C'} — auto-mapped to 1:n obj.n = length(nodeIDs); obj.parent = (1:obj.n)'; obj.rank = zeros(obj.n,1); % build mapping: nodeID -> index if ischar(nodeIDs{1}) || isstring(nodeIDs{1}) obj.id2idx = containers.Map(nodeIDs, 1:obj.n); obj.idx2id = containers.Map(1:obj.n, nodeIDs); else obj.id2idx = containers.Map(nodeIDs, 1:obj.n); obj.idx2id = containers.Map(1:obj.n, num2cell(nodeIDs)); end end function root = find(obj, x) % iterative path compression to avoid recursion overflow if ischar(x) || isstring(x) idx = obj.id2idx(x); else idx = x; end % compress path iteratively stack = []; while obj.parent(idx) ~= idx stack = [stack; idx]; idx = obj.parent(idx); end root = idx; % update all nodes in stack to point to root for i = 1:length(stack) obj.parent(stack(i)) = root; end end function obj = union(obj, x, y) rootX = obj.find(x); rootY = obj.find(y); if rootX == rootY, return; end % union by rank if obj.rank(rootX) < obj.rank(rootY) obj.parent(rootX) = rootY; elseif obj.rank(rootX) > obj.rank(rootY) obj.parent(rootY) = rootX; else obj.parent(rootY) = rootX; obj.rank(rootX) = obj.rank(rootX) + 1; end end end end提示:
find方法采用迭代路径压缩,彻底规避递归深度限制;union严格按秩合并,保障最坏时间复杂度;nodeIDs支持字符串/数字混合输入,自动构建双向映射,适配真实项目中节点命名不规范的场景(如'SW1','Router_2','Sensor_007')。
2.3 边表预处理:从原始邻接矩阵到可排序边结构体的三步清洗
实际输入常为邻接矩阵A(n×n,A(i,j)=w表示边权,0或Inf表示无边),需转为边结构体数组。本资源提供adjmatrix2edgelist.m,执行以下清洗:
- 剔除无效边:
A(i,j)==0 | A(i,j)==Inf | isnan(A(i,j))的边直接丢弃; - 去重与方向统一:无向图中
(i,j)与(j,i)视为同一条边,只保留i<j的版本; - 添加原始索引:为每条边记录
orig_row=i,orig_col=j,orig_idx=k,便于后续日志溯源。
function edges = adjmatrix2edgelist(A, nodeNames) % A: n x n adjacency matrix, symmetric for undirected graph % nodeNames: cell array of length n, e.g. {'A','B','C'} n = size(A,1); edges = struct('from',{}, 'to',{}, 'weight',{}, 'orig_row',{}, 'orig_col',{}, 'orig_idx',{}); k = 0; for i = 1:n for j = i+1:n % only upper triangle to avoid duplicate w = A(i,j); if w > 0 && w ~= Inf && ~isnan(w) k = k + 1; edges(k).from = i; edges(k).to = j; edges(k).weight = w; edges(k).orig_row = i; edges(k).to = j; edges(k).orig_col = j; edges(k).orig_idx = k; end end end % if nodeNames provided, replace numeric IDs with names if nargin > 1 && ~isempty(nodeNames) for i = 1:length(edges) edges(i).from = nodeNames{edges(i).from}; edges(i).to = nodeNames{edges(i).to}; end end end参数说明:
nodeNames为可选参数,若传入则自动将数字 ID 替换为业务名称;orig_idx是全局唯一序号,即使后续排序也不会改变,是调试时定位原始数据的关键锚点。
3. 核心代码详解:从初始化、排序、遍历到结果组装的完整流水线
3.1 主函数kruskal_mst.m:六步闭环,每步可设断点
主函数采用清晰分步式结构,共六步,每步有明确输入输出和日志开关:
function [T, edgeList, uf] = kruskal_mst(edges, nodeIDs, varargin) % edges: struct array with fields .from, .to, .weight % nodeIDs: vector or cell array of all node identifiers % varargin: 'maxWeight', w_max, 'forceInclude', {i,j}, 'verbose', true % Output: T - sparse adjacency matrix of MST; edgeList - selected edges; uf - final unionfind object % Step 1: Parse input options p = inputParser; addParameter(p, 'maxWeight', Inf); addParameter(p, 'forceInclude', []); addParameter(p, 'verbose', false); parse(p, varargin{:}); opts = p.Results; % Step 2: Build union-find structure uf = unionfind(nodeIDs); % Step 3: Filter edges by maxWeight and sort by weight validEdges = edges; if opts.maxWeight < Inf validIdx = [edges.weight] <= opts.maxWeight; validEdges = edges(validIdx); end % Sort by weight, then by from, then by to (stable for ties) [~, idx] = sortrows(cell2mat({validEdges.weight, ... cellfun(@(x) x, {validEdges.from}, 'UniformOutput', false), ... cellfun(@(x) x, {validEdges.to}, 'UniformOutput', false)}), [1,2,3]); sortedEdges = validEdges(idx); % Step 4: Initialize MST storage T = sparse(length(nodeIDs), length(nodeIDs)); % output adjacency matrix edgeList = struct('from',{}, 'to',{}, 'weight',{}, 'step',{}); % selected edges with step number selectedCount = 0; step = 0; % Step 5: Main Kruskal loop for i = 1:length(sortedEdges) step = step + 1; e = sortedEdges(i); root1 = uf.find(e.from); root2 = uf.find(e.to); if root1 ~= root2 % Add edge to MST selectedCount = selectedCount + 1; edgeList(selectedCount).from = e.from; edgeList(selectedCount).to = e.to; edgeList(selectedCount).weight = e.weight; edgeList(selectedCount).step = step; % Update adjacency matrix (symmetric for undirected) idx1 = find(strcmp(nodeIDs, e.from)); idx2 = find(strcmp(nodeIDs, e.to)); T(idx1,idx2) = e.weight; T(idx2,idx1) = e.weight; % Union the sets uf.union(e.from, e.to); if opts.verbose fprintf('Step %d: Added edge (%s,%s) weight=%.2f\n', step, e.from, e.to, e.weight); end % Early stop when n-1 edges collected if selectedCount == length(nodeIDs)-1 break; end else if opts.verbose fprintf('Step %d: Skipped edge (%s,%s) — would form cycle\n', step, e.from, e.to); end end end % Step 6: Force-include logic (run after main loop to avoid breaking invariant) if ~isempty(opts.forceInclude) && length(opts.forceInclude) == 2 f1 = opts.forceInclude{1}; f2 = opts.forceInclude{2}; if ~ismember(f1, {edgeList.from}) || ~ismember(f2, {edgeList.to}) % Check if force edge already exists in MST found = false; for k = 1:length(edgeList) if (strcmp(edgeList(k).from,f1) && strcmp(edgeList(k).to,f2)) || ... (strcmp(edgeList(k).from,f2) && strcmp(edgeList(k).to,f1)) found = true; break; end end if ~found % Manually add and adjust (may break MST property — warn user) warning('Forced edge (%s,%s) added; MST property not guaranteed.', f1, f2); idx1 = find(strcmp(nodeIDs, f1)); idx2 = find(strcmp(nodeIDs, f2)); T(idx1,idx2) = min([T(idx1,idx2); Inf]); % keep existing or set to Inf T(idx2,idx1) = T(idx1,idx2); end end end end逻辑说明:
Step 1使用inputParser统一管理可选参数,避免varargin手动解析的混乱;Step 3的sortrows传入三列元胞数组,确保权值相同时按from和to字典序排序,消除随机性;Step 5中fprintf日志含step编号,与edgeList.step字段严格对应,调试时可交叉验证;Step 6的forceInclude是工程刚需:当某条边代表物理上必须存在的链路(如主干光纤),即使算法判定为冗余,也强制加入并给出明确警告。
3.2 结果可视化:用plot_mst.m生成带权重标注的拓扑图
MST 结果若仅存为稀疏矩阵,难以直观评估。本资源附带plot_mst.m,支持三种布局:
| 布局类型 | 调用方式 | 适用场景 | 关键参数 |
|---|---|---|---|
| 圆形布局 | plot_mst(T, nodeNames, 'layout', 'circle') | 节点数 ≤ 30,强调对称性 | radius: 圆半径,默认 1 |
| 力导向布局 | plot_mst(T, nodeNames, 'layout', 'force') | 节点数 30–200,自动避让 | iter: 迭代次数,默认 100 |
| 自定义坐标 | plot_mst(T, nodeNames, 'pos', XY) | 已知物理位置(如传感器坐标) | XY:n×2矩阵,每行[x,y] |
function plot_mst(T, nodeNames, varargin) p = inputParser; addParameter(p, 'layout', 'circle'); addParameter(p, 'pos', []); addParameter(p, 'radius', 1); addParameter(p, 'iter', 100); parse(p, varargin{:}); opts = p.Results; n = size(T,1); if isempty(opts.pos) switch opts.layout case 'circle' theta = linspace(0, 2*pi, n+1); theta = theta(1:end-1); pos = [cos(theta)' sin(theta)'] * opts.radius; case 'force' pos = force_layout(T, opts.iter); otherwise error('Unknown layout: %s', opts.layout); end else pos = opts.pos; end % Plot edges figure; hold on; for i = 1:n for j = i+1:n if T(i,j) > 0 plot([pos(i,1), pos(j,1)], [pos(i,2), pos(j,2)], 'b-', 'LineWidth', 1.2); % Annotate edge weight at midpoint midx = (pos(i,1)+pos(j,1))/2; midy = (pos(i,2)+pos(j,2))/2; text(midx, midy, sprintf('%.1f', T(i,j)), ... 'FontSize', 8, 'Color', 'r', 'HorizontalAlignment', 'center'); end end end % Plot nodes scatter(pos(:,1), pos(:,2), 60, 'filled', 'MarkerFaceColor', 'g'); for i = 1:n text(pos(i,1), pos(i,2)+0.05, nodeNames{i}, ... 'FontSize', 9, 'FontWeight', 'bold', 'HorizontalAlignment', 'center'); end title('Minimum Spanning Tree (Kruskal Algorithm)'); axis equal; axis off; hold off; end参数说明:
force_layout.m是一个轻量级力导向算法实现,仅依赖sparse和sum,不调用graph工具箱,确保离线环境可用;text标注权重时加+0.05偏移,避免覆盖节点标签。
3.3 输入兼容性:支持四种常见数据格式一键转换
用户输入千差万别,本资源提供load_edges.m统一入口,自动识别格式并转为标准edges结构体:
| 输入格式 | 示例文件 | 自动识别依据 | 输出字段 |
|---|---|---|---|
| CSV 边表 | edges.csv | 头行含from,to,weight | .from,.to,.weight |
| Excel 表格 | network.xlsx | Sheet 名含edge或link | 同上 |
| MAT 结构体 | data.mat | 含变量edges且为结构体数组 | 直接返回 |
| 邻接矩阵 | A.mat | 变量名A且为二维数值矩阵 | 调用adjmatrix2edgelist |
function edges = load_edges(filename, varargin) % Auto-detect format and load edges if isempty(filename), error('Filename required'); end [~,~,ext] = fileparts(filename); ext = lower(ext); switch ext case '.csv' data = readtable(filename); if ismember({'from','to','weight'}, data.Properties.VariableNames) edges = table2struct(data); else error('CSV must have columns: from, to, weight'); end case '.xlsx' sheets = sheetnames(filename); edgeSheet = ''; for s = sheets' if contains(lower(s), 'edge') || contains(lower(s), 'link') edgeSheet = s; break; end end if isempty(edgeSheet), error('No edge-related sheet found in Excel'); end data = readtable(filename, 'Sheet', edgeSheet); edges = table2struct(data); case '.mat' S = load(filename); if isfield(S, 'edges') && isstruct(S.edges) edges = S.edges; elseif isfield(S, 'A') && ndims(S.A)==2 && issquare(S.A) % Assume A is adjacency matrix nodeNames = arrayfun(@(i)sprintf('N%d',i), 1:size(S.A,1), 'UniformOutput', false); edges = adjmatrix2edgelist(S.A, nodeNames); else error('MAT file must contain ''edges'' struct or ''A'' adjacency matrix'); end otherwise error('Unsupported file extension: %s', ext); end % Ensure all fields exist if ~isfield(edges, 'from'), edges.from = {''}; end if ~isfield(edges, 'to'), edges.to = {''}; end if ~isfield(edges, 'weight'), edges.weight = zeros(length(edges),1); end end注意:
table2struct会将 CSV 中的字符串自动转为元胞,edges.from为1×n元胞数组,与后续unionfind的id2idx映射完全兼容;issquare是自定义辅助函数,定义为ndims(A)==2 && size(A,1)==size(A,2)。
4. 避坑指南:五条血泪经验总结,每一条都来自某次深夜调试翻车现场
4.1 现象:算法返回边数少于 $n-1$,uf显示仍有多个根节点
原因:输入图本身不连通,但代码未做连通性预检,kruskal_mst在selectedCount == n-1时提前退出,剩余孤立子图被忽略。
解决:在Step 5循环结束后,添加连通性检查:
% After main loop, check connectivity roots = zeros(1, length(nodeIDs)); for i = 1:length(nodeIDs) roots(i) = uf.find(nodeIDs{i}); end numRoots = length(unique(roots)); if numRoots > 1 warning('Graph is disconnected: found %d connected components. MST built for largest component only.', numRoots); % Optional: run Kruskal on each component separately end4.2 现象:plot_mst报错Index exceeds matrix dimensions
原因:nodeNames长度与邻接矩阵T的维度不一致,常见于adjmatrix2edgelist未传入nodeNames,导致T按n=nnz(A)构建,但nodeNames为默认{'N1','N2',...}长度n,而实际节点 ID 有缺口(如A=[0,1;1,0]但节点应为{'A','B'},非{'N1','N2'})。
解决:强制校验维度:
if length(nodeIDs) ~= size(T,1) error('Length of nodeIDs (%d) must match size of T (%d)', length(nodeIDs), size(T,1)); end4.3 现象:排序后边序混乱,相同权值的边顺序每次运行不同
原因:sortrows对元胞数组排序时,若from/to为字符串,字典序比较受 locale 影响;且未指定'ComparisonMethod'。
解决:改用cellfun预处理为数值索引再排序:
% Replace string IDs with numeric indices for stable sort id2num = containers.Map(nodeIDs, 1:length(nodeIDs)); weights = [sortedEdges.weight]; fromNum = cellfun(@(x)id2num(x), {sortedEdges.from}); toNum = cellfun(@(x)id2num(x), {sortedEdges.to}); [~, idx] = sortrows([weights, fromNum, toNum], [1,2,3]); sortedEdges = sortedEdges(idx);4.4 现象:forceInclude边加入后,T矩阵对应位置仍为 0
原因:forceInclude传入的是字符串(如{'SW1','Core'}),但find(strcmp(...))在nodeNames为数字时失败,idx1/idx2返回空。
解决:在Step 6中统一转换:
% Convert forced nodes to indices robustly idx1 = []; for i = 1:length(nodeIDs) if ischar(nodeIDs{i}) && strcmp(nodeIDs{i}, f1) || ... isnumeric(nodeIDs{i}) && nodeIDs{i} == f1 idx1 = i; break; end end % Same for idx2...4.5 现象:大图(>1000 节点)运行极慢,unionfind.find占用 90% 时间
原因:未启用unionfind的路径压缩,或find中stack数组动态增长导致内存重分配。
解决:预分配stack并优化循环:
function root = find(obj, x) % ... mapping code ... stack = zeros(1, obj.n); % pre-allocate max possible size stackLen = 0; while obj.parent(idx) ~= idx stackLen = stackLen + 1; stack(stackLen) = idx; idx = obj.parent(idx); end root = idx; for i = 1:stackLen obj.parent(stack(i)) = root; end end5. 进阶技巧:如何用三步验证 MST 正确性,并导出工业级报告
5.1 正确性验证:环检测 + 权重比对 + 唯一性抽样
MST 结果不能只看边数,必须通过三重验证。本资源提供verify_mst.m,返回布尔值与详细报告:
function [isValid, report] = verify_mst(T, edges, nodeIDs) % T: sparse adjacency matrix of candidate MST % edges: original edge list (unsorted) % nodeIDs: list of all nodes report = struct('cycleFound', false, 'totalWeight', 0, 'isUnique', true, 'details', {}); n = length(nodeIDs); % Verify 1: Exactly n-1 edges nnzT = nnz(T)/2; % /2 for undirected if nnzT ~= n-1 report.details{end+1} = sprintf('Edge count mismatch: got %d, expected %d', nnzT, n-1); isValid = false; return; end % Verify 2: No cycles (via DFS) if has_cycle_dfs(T, nodeIDs) report.cycleFound = true; report.details{end+1} = 'Cycle detected in candidate MST'; isValid = false; return; end % Verify 3: Total weight matches Kruskal's result report.totalWeight = sum([edges.weight]); % But more importantly: check if any edge in MST can be replaced by a cheaper one from original graph % This is the cut property test isValid = true; % ... implementation of cut property test ... end关键技巧:
has_cycle_dfs采用迭代 DFS(非递归),避免栈溢出;cut property test随机选取 MST 中一条边e,在原图中找到所有横跨e移除后两子图的边,确认e权值最小——这是 MST 的充要条件,比单纯比总重更可靠。
5.2 工业报告生成:用export_mst_report.m输出 PDF + Excel 双格式
工程师交付不能只扔一个.mat文件。本资源附带export_mst_report.m,一键生成:
mst_report.pdf:含拓扑图、边列表(含原始索引)、总权重、连通分量统计;mst_edges.xlsx:Excel 表,含列From,To,Weight,Original_Index,Step_Added,支持筛选与排序。
function export_mst_report(T, edgeList, nodeNames, filenameBase) % Generate PDF report using MATLAB's exportgraphics (R2020a+) fig = figure('Visible', 'off'); plot_mst(T, nodeNames, 'layout', 'force'); exportgraphics(fig, [filenameBase '_topology.png'], 'ContentType', 'image'); close(fig); % Create PDF with Report Generator (if licensed) or fallback to text if license('test', 'Report_Generator') % Full PDF with sections, tables, images rpt = mlreportgen.report.Report([filenameBase '_report'], 'pdf'); add(rpt, mlreportgen.report.TitlePage('Title', 'MST Verification Report')); add(rpt, mlreportgen.report.TableOfContents); add(rpt, mlreportgen.report.Image([filenameBase '_topology.png'])); % ... add tables for edgeList ... close(rpt); rptview(rpt); else % Fallback: plain text report fid = fopen([filenameBase '_report.txt'], 'w'); fprintf(fid, '=== MST REPORT ===\n'); fprintf(fid, 'Nodes: %d, Edges: %d\n', length(nodeNames), length(edgeList)); fprintf(fid, 'Total Weight: %.3f\n', sum([edgeList.weight])); fprintf(fid, '\nEDGE LIST:\n'); for i = 1:length(edgeList) fprintf(fid, '%d. %s -- %s : %.2f (Step %d)\n', ... i, edgeList(i).from, edgeList(i).to, edgeList(i).weight, edgeList(i).step); end fclose(fid); end % Export Excel edgesTable = table({edgeList.from}', {edgeList.to}', [edgeList.weight]', ... [edgeList.orig_idx]', [edgeList.step]', ... 'VariableNames', {'From','To','Weight','Original_Index','Step_Added'}); writematrix(edgesTable, [filenameBase '_edges.xlsx']); end注意:若无 Report Generator 许可证,自动降级为
.txt报告,确保功能不中断;writematrix兼容 R2019a+,避免writetable在旧版中的兼容性问题。
5.3 生产环境部署:如何将kruskal_mst编译为独立可执行文件
MATLAB Compiler 可将算法打包为无需 MATLAB 运行时的.exe,但需注意三点:
- 预编译
unionfind类:在编译前运行compile_classes.m,确保unionfind被静态分析捕获; - 禁用图形函数:
plot_mst依赖figure,编译时需提供--no-display参数并替换为纯计算版本; - 输入路径硬编码:
.exe无法读取相对路径,需用fullfile(pwd, 'input.csv')。
% compile_script.m — run before mcc % Pre-load classes to ensure they're compiled uf = unionfind({'A','B'}); clear uf; % Compile with no display mcc -m kruskal_mst.m -a unionfind.m -a adjmatrix2edgelist.m -a load_edges.m ... -a verify_mst.m -a export_mst_report.m ... --no-display;血泪经验:某次为某公司交付时,忘记加
--no-display,生成的.exe在无 GUI 的 Linux 服务器上启动即崩溃;从那以后我每次打包都强制走一遍mcc -h查参数,并在目标环境用ldd检查依赖库是否齐全。希望帮到你。
本文还有配套的精品资源,点击获取