最佳调度问题 回溯算法分析

合集下载

算法设计与分析——批处理作业调度(回溯法)

算法设计与分析——批处理作业调度(回溯法)

算法设计与分析——批处理作业调度(回溯法)之前讲过⼀个相似的问题流⽔作业调度问题,那⼀道题最开始⽤动态规划,推到最后得到了⼀个Johnson法则,变成了⼀个排序问题,有兴趣的可以看⼀下本篇博客主要参考⾃⼀、问题描述给定n个作业的集合{J1,J2,…,Jn}。

每个作业必须先由机器1处理,然后由机器2处理。

作业Ji需要机器j的处理时间为t ji。

对于⼀个确定的作业调度,设Fji是作业i在机器j上完成处理的时间。

所有作业在机器2上完成处理的时间和称为该作业调度的完成时间和。

批处理作业调度问题要求对于给定的n个作业,制定最佳作业调度⽅案,使其完成时间和达到最⼩。

例:设n=3,考虑以下实例:看到这⾥可能会对这些完成时间和是怎么计算出来的会有疑问,这⾥我拿123和312的⽅案来说明⼀下。

对于调度⽅案(1,2,3)作业1在机器1上完成的时间是2,在机器2上完成的时间是3作业2在机器1上完成的时间是5,在机器2上完成的时间是6作业3在机器1上完成的时间是7,在机器2上完成的时间是10所以,作业调度的完成时间和= 3 + 6 + 10这⾥我们可以思考⼀下作业i在机器2上完成的时间应该怎么去求?作业i在机器1上完成的时间是连续的,所以是直接累加就可以。

但对于机器2就会产⽣两种情况,这两种情况其实就是上图的两种情况,对于(1,2,3)的调度⽅案,在求作业2在机器2上完成的时间时,由于作业2在机器1上还没有完成,这就需要先等待机器1处理完;⽽对于(3,1,2)的调度⽅案,在求作业2在机器2上完成的时间时,作业2在机器1早已完成,⽆需等待,直接在作业1被机器1处理之后就能接着被处理。

综上,我们可以得到如下表达式if(F2[i-1] > F1[i])F2[i] = F2[i-1] + t[2][i]elseF2[i] = F1[i] + t[2][i]⼆、算法设计类Flowshop的数据成员记录解空间的结点信息,M输⼊作业时间,bestf记录当前最⼩完成时间和,数组bestx记录相应的当前最佳作业调度。

回溯算法详解

回溯算法详解

回溯算法详解
回溯算法是一种经典问题求解方法,通常被应用于在候选解的搜索空间中,通过深度优先搜索的方式找到所有可行解的问题。

回溯算法的本质是对一棵树的深度优先遍历,因此也被称为树形搜索算法。

回溯算法的基本思想是逐步构建候选解,并试图将其扩展为一个完整的解。

当无法继续扩展解时,则回溯到上一步并尝试其他的扩展,直到找到所有可行的解为止。

在回溯算法中,通常会维护一个状态向量,用于记录当前已经构建的解的情况。

通常情况下,状态向量的长度等于问题的规模。

在搜索过程中,我们尝试在状态向量中改变一个或多个元素,并检查修改后的状态是否合法。

如果合法,则继续搜索;如果不合法,则放弃当前修改并回溯到上一步。

在实际应用中,回溯算法通常用来解决以下类型的问题:
1. 组合问题:从n个元素中选取k个元素的所有组合;
2. 排列问题:从n个元素中选择k个元素,并按照一定顺序排列的所有可能;
3. 子集问题:从n个元素中选择所有可能的子集;
4. 棋盘问题:在一个给定的n x n棋盘上放置n个皇后,并满足彼此之间不会互相攻击的要求。

回溯算法的时间复杂度取决于候选解的规模以及搜索空间中的剪枝效果。

在最坏情况下,回溯算法的时间复杂度与候选解的数量成指数级增长,因此通常会使用剪枝算法来尽可能减少搜索空间的规模,从而提高算法的效率。

总之,回溯算法是一种非常有用的问题求解方法,在实际应用中被广泛使用。

同时,由于其时间复杂度较高,对于大规模的问题,需要慎重考虑是否使用回溯算法以及如何优化算法。

回溯算法原理和几个常用的算法实例

回溯算法原理和几个常用的算法实例

回溯算法原理和几个常用的算法实例回溯算法是一种基于深度优先的算法,用于解决在一组可能的解中找到满足特定条件的解的问题。

其核心思想是按照特定的顺序逐步构造解空间,并通过剪枝策略来避免不必要的。

回溯算法的实现通常通过递归函数来进行,每次递归都尝试一种可能的选择,并在达到目标条件或无法继续时进行回溯。

下面介绍几个常用的回溯算法实例:1.八皇后问题:八皇后问题是一个经典的回溯问题,要求在一个8×8的棋盘上放置8个皇后,使得每个皇后都不能相互攻击。

即每行、每列和对角线上都不能有两个皇后。

通过在每一列中逐行选择合适的位置,并进行剪枝,可以找到所有满足条件的解。

2.0-1背包问题:0-1背包问题是一个经典的组合优化问题,要求在一组物品中选择一些物品放入背包,使得其总重量不超过背包容量,同时价值最大化。

该问题可以通过回溯算法进行求解,每次选择放入或不放入当前物品,并根据剩余物品和背包容量进行递归。

3.数独问题:数独问题是一个经典的逻辑推理问题,要求在一个9×9的网格中填入数字1-9,使得每行、每列和每个3×3的子网格中都没有重复数字。

该问题可以通过回溯算法进行求解,每次选择一个空格,并依次尝试1-9的数字,然后递归地进行。

4.字符串的全排列:给定一个字符串,要求输出其所有可能的排列。

例如,对于字符串"abc",其所有可能的排列为"abc"、"acb"、"bac"、"bca"、"cab"和"cba"。

可以通过回溯算法进行求解,每次选择一个字符,并递归地求解剩余字符的全排列。

回溯算法的时间复杂度通常比较高,因为其需要遍历所有可能的解空间。

但是通过合理的剪枝策略,可以减少的次数,提高算法效率。

在实际应用中,可以根据具体问题的特点来设计合适的剪枝策略,从而降低算法的时间复杂度。

最佳调度问题(搜索回溯)

最佳调度问题(搜索回溯)

最佳调度问题(搜索回溯)最佳调度问题【问题描述】假设有n个任务由k个可并⾏⼯作的机器完成。

完成任务i需要的时间为ti。

试设计⼀个算法找出完成这n个任务的最佳调度,使得完成全部任务的时间最早。

【编程任务】对任意给定的整数n和k,以及完成任务i需要的时间为ti,i=1~n。

编程计算完成这n个任务的最佳调度。

【输⼊格式】由⽂件machine.in给出输⼊数据。

第⼀⾏有2 个正整数n和k。

第2 ⾏的n个正整数是完成n个任务需要的时间。

【输出格式】将计算出的完成全部任务的最早时间输出到⽂件machine.out。

【输⼊样例】7 32 14 4 16 6 5 3【输出样例】17/*搜索问题我会先想每个⼩问题⾯临的共同条件是什么,每加⼊⼀件任务,他加⼊的条件是什么?他要加⼊哪个机器?加⼊的条件是什么?时间怎样最短?然后我就蒙圈了、、、我智障的想法是search(1,1)把第⼀个加⼊第⼀个机器,然后直到search(7,1)把第七个加⼊第⼀个,然后再加⼀个机器search(1,2)直到search(7,2)后来想想真是愚蠢呐,上⽹搜了⼀下、、才明⽩...orz,search(int,int)⾥⾯的两个参数⼀个表⽰加⼊的第⼏个任务,另⼀个是现在的时间,所以search()⾥⾯是什么很重要,我发现这就⽐如前⾯的题⽬的效率问题,求最⾼的效率,search()⾥⾯也有⼀个参数代表效率,从0开始;还有着⼀个题注意剪枝,当这⼀种情况已经⽐之前找到的最⼩值⼤时这种情况就可以舍弃不⽤考虑了QWQ*/#include<iostream>#include<cstdio>#include<cstdlib>using namespace std;int a[30],s[30];int n,k,minn=0x7ffff;void search(int,int);int main() {scanf("%d%d",&n,&k);for(int i=1; i<=n; i++) //输⼊每个时间scanf("%d",&a[i]);search(1,0);//加⼊第⼀个时间,现在花费的时间是0;cout<<minn;return0;}void search(int x,int y) {if(y>=minn)return;//剪枝已经⽐最⼩的⼤了就不⽤再放了,舍弃这种⽅法if(x>n) { //放完了进⾏判断if(y<minn)minn=y;return;}for(int i=1; i<=k; i++) {if(s[i]+a[x]<=minn) { //剪枝s[i]+=a[x];search(x+1,max(y,s[i]));s[i]-=a[x];//回溯}}return;}。

0028算法笔记——【回溯法】批作业调度问题和符号三角形问题

0028算法笔记——【回溯法】批作业调度问题和符号三角形问题

1、批作业调度问题(1)问题描述给定n个作业的集合{J1,J2,…,Jn}。

每个作业必须先由机器1处理,然后由机器2处理。

作业Ji需要机器j的处理时间为tji。

对于一个确定的作业调度,设Fji是作业i在机器j上完成处理的时间。

所有作业在机器2上完成处理的时间和称为该作业调度的完成时间和。

批处理作业调度问题要求对于给定的n个作业,制定最佳作业调度方案,使其完成时间和达到最小。

例:设n=3,考虑以下实例:这3个作业的6种可能的调度方案是1,2,3;1,3,2;2,1,3;2,3,1;3,1,2;3,2,1;它们所相应的完成时间和分别是19,18,20,21,19,19。

易见,最佳调度方案是1,3,2,其完成时间和为18。

(2)算法设计批处理作业调度问题要从n个作业的所有排列中找出具有最小完成时间和的作业调度,所以如图,批处理作业调度问题的解空间是一颗排列树。

按照回溯法搜索排列树的算法框架,设开始时x=[1,2,……n]是所给的n个作业,则相应的排列树由x[1:n]的所有排列构成。

算法具体代码如下:[cpp]view plain copy1.#include "stdafx.h"2.#include <iostream>ing namespace std;4.5.class Flowshop6.{7.friend int Flow(int **M,int n,int bestx[]);8.private:9.void Backtrack(int i);10.11.int **M, // 各作业所需的处理时间12. *x, // 当前作业调度13. *bestx, // 当前最优作业调度14.15. *f2, // 机器2完成处理时间16. f1, // 机器1完成处理时间17. f, // 完成时间和18.19. bestf, // 当前最优值20. n; // 作业数21. };22.23.int Flow(int **M,int n,int bestx[]);24.25.template <class Type>26.inline void Swap(Type &a, Type &b);27.28.int main()29.{30.int n=3,bf;31.int M1[4][3]={{0,0,0},{0,2,1},{0,3,1},{0,2,3}};32.33.int **M=new int*[n+1];34.35.for(int i=0;i<=n;i++)36. {37. M[i]=new int[3];38. }39. cout<<"M(i,j)值如下:"<<endl;40.41.for(int i=0;i<=n;i++)42. {43.for(int j=0;j<3;j++)44. {45. M[i][j]=M1[i][j];46. }47. }48.49.int bestx[4];50.for(int i=1;i<=n;i++)51. {52. cout<<"(";53.for(int j=1;j<3;j++)54. cout<<M[i][j]<<" ";55. cout<<")";56. }57.58. bf=Flow(M,n,bestx);59.60.for(int i=0;i<=n;i++)61. {62.delete []M[i];63. }64.delete []M;65.66. M=0;67.68. cout<<endl<<"最优值是:"<<bf<<endl;69. cout<<"最优调度是:";70.71.for(int i=1;i<=n;i++)72. {73. cout<<bestx[i]<<" ";74. }75. cout<<endl;76.return 0;77.}78.79.void Flowshop::Backtrack(int i)80.{81.if (i>n)82. {83.for (int j=1; j<=n; j++)84. {85. bestx[j] = x[j];86. }87. bestf = f;88. }89.else90. {91.for (int j = i; j <= n; j++)92. {93. f1+=M[x[j]][1];94.//机器2执行的是机器1已完成的作业,所以是i-195. f2[i]=((f2[i-1]>f1)?f2[i-1]:f1)+M[x[j]][2];96.97. f+=f2[i];98.if (f < bestf)//剪枝99. {100. Swap(x[i], x[j]); 101. Backtrack(i+1); 102. Swap(x[i], x[j]); 103. }104. f1-=M[x[j]][1];105. f-=f2[i];106. }107. }108.}109.110.int Flow(int **M,int n,int bestx[]) 111.{112.int ub=30000;113.114. Flowshop X;115. X.n=n;116. X.x=new int[n+1];117. X.f2=new int[n+1];118.119. X.M=M;120. X.bestx=bestx;121. X.bestf=ub;122.123. X.f1=0;124. X.f=0;125.126.for(int i=0;i<=n;i++)127. {128. X.f2[i]=0,X.x[i]=i;129. }130. X.Backtrack(1);131.delete []X.x;132.delete []X.f2;133.return X.bestf;134.}135.136.template <class Type>137.inline void Swap(Type &a, Type &b) 138.{139. Type temp=a;140. a=b;141. b=temp;142.}由于算法Backtrack在每一个节点处耗费O(1)计算时间,故在最坏情况下,整个算法计算时间复杂性为O(n!)。

《算法设计与分析》课程中“回溯法”教学探讨

《算法设计与分析》课程中“回溯法”教学探讨
时 , 让学 生在 充分理 解 的基础 上掌 握子 集树及 排列 树 的算法框 架 。子集树 问题算法 框架 如下 , 要 遍历 子 集树 需 0(“计 算时 间 。 2)
vi akrc it ) odb c t k(n a t
{ i t )otu( ) f( >n up tx ;
2 1 采 用传 统教 学方 法 , . 强化 习题 讨 论 实 验 的 目的是 让学 生 自己动 手完 成任 务 , 同于课 堂教 学学生 是 实验课 的主人 , 对学生 算法 中有 不 针
问题的部分 , 教师应及时加以纠正 , 并以此展开讨论 , 传统教学方法的优点在于条理清楚 , 思路清晰, 教 师在板书时学生可以思考 , 而并非教师用课件直接将算法或程序演示 给学生。例如在做教材 4 9 . 磁带
子树 , 续按 深度 优 先策 略搜 索 。 继
由于回溯法在许多知识领域有广泛 的应用背景 , 故从各级 中小学生信息学竞赛到 A M IP C /C C国际 大学生程序设计竞赛 中均能发现其踪迹 _ 。它可 以解决许多看上去无法处理的问题 , 5 J 另外一些可用类 似于动态规划解决的问题也可用其处理 , 因此掌握回溯法 的原理与编程技巧 , 是程序设计的基本功 。与 动态规划法的状态转移方程因问题而异相比, 回溯法显得相对简单 , 大多数学生经过一段时间的训练均
成 该题 。
对 一些 依 靠搜 索 而不是 直接 套用 子集 树及 排列 树 的 问题 , 加 以启 发诱 导 , 要 及 O I 1 木棍拼接问题。这两题不能完全按照前期所讲授 的子集树及排列树问题 的套路求 解, 启发 学 生这 两题 共 同点是 采用 深度 优先 搜 寻原则 , 回溯 中加 人适 当 的剪枝 函数 , 在 如买 鱼 问题 中鱼 互不冲突 , 木棍拼接问题 中拼接后剩余长度为当前长度 , 即可完成。当学生明白并非所有 的搜索均是照 以前的套路做时, 再让他们做书中 5 1 .5整数变换问题 , 52 世界名画陈列馆问题。为了让学生拓展 及 .5

最佳调度的回溯算法

最佳调度的回溯算法

实验目的:理解回溯法的原理,掌握调度问题的处理方法,实现最佳调度问题的回溯解决。

问题定义输入:1.任务数N2.机器数M3.随机序列长度t[i],其中t[i]=x表示第i个任务完成需要时间单位x,输出:1.开销时间besttime,表示最佳调度需要时间单位2.最佳调度序列bestx[],其中bestx[i]=x,表示将第i个任务分配给第x个机器执行。

实验思想解空间的表示:一个深度为N的M叉树。

基本思路:搜索从开始结点(根结点)出发,以DFS搜索整个解空间。

每搜索完一条路径则记录下besttime 和bestx[]序列开始结点就成为一个活结点,同时也成为当前的扩展结点。

在当前的扩展结点处向纵深方向移至一个新结点,并成为一个新的活结点,也成为当前扩展结点。

如果在当前的扩展结点处不能再向纵深方向扩展,则当前扩展结点就成为死结点。

此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展结点;直至找到一个解或全部解。

测试数据及结果本测试的硬件以及软件环境如下CPU:PM 1.5G; 内存:768M;操作系统:windows xp sp2;软件平台:JDK1.5;开发环境:eclipse如图1所示:即为求任务数为10机器数为5的最佳调度的算法结果。

图1实验结论以及算法分析通过测试证明算法正确有效。

性能分析的方法:使用JDK 1.5的System.nanoTime(),计算算法消耗的时间,以此来评价算法。

(该方法在JDK1.5以下的版本中不支持)为了不影响算法的准确度,在测试的过程我们注释掉了打印随机字符串的步骤。

由于没有使用限界函数进行优化,算法时间和空间复杂度呈指数级增长。

所以该算法不适合较大规模的计算。

图2图2 蓝线表示机器数一定M=3时,n增大时求解最佳调度对所消耗的时间,该趋势随着指数增加。

图3图3表示任务数N一定时随着M的增大的增长曲线。

图2和图3表明该程序的时间复杂度与理论分析相符合。

组合优化问题的算法与求解

组合优化问题的算法与求解

组合优化问题的算法与求解组合优化问题是一类需要在给定的约束条件下找到最优解的问题。

这些问题在现实生活中有着广泛的应用,比如物流配送问题、旅行商问题等等。

本文将介绍几种常见的组合优化问题的算法以及它们的求解方法。

一、贪婪算法贪婪算法是一种简单而高效的求解组合优化问题的方法。

它通过在每一步选择当前看起来最优的解决方案,逐步建立起最终的解。

贪婪算法通常具有快速的执行速度和较好的近似解质量。

例如,对于旅行商问题,贪婪算法可以从一个起点开始,每次选择离当前位置最近的未访问节点作为下一个访问节点,直到所有节点都被访问过。

这样,贪婪算法可以得到一个近似的最短路径。

二、回溯算法回溯算法是一种穷举搜索的方法,它通过逐个尝试所有可能的解决方案,并逐步剪枝以减少搜索空间。

回溯算法通常适用于组合优化问题的求解,尤其是在问题规模较小的情况下。

以0-1背包问题为例,回溯算法可以通过穷举所有可能的物品选择方式,计算其总价值,并在搜索过程中剪枝以提高效率。

回溯算法的优势在于能够找到最优解,但在问题规模较大时,耗时较长。

三、动态规划算法动态规划算法是一种将问题分解为子问题并记录子问题结果的方法。

它适用于能够将原问题分解为相互重叠的子问题,并利用子问题的解来推导原问题的解。

比如在背包问题中,动态规划算法可以通过定义状态转移方程来解决。

设dp[i][j]表示在前i个物品中选择总重量不超过j的情况下的最大价值,则有以下状态转移方程:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])通过填表计算,可以获得最终的最优解。

四、遗传算法遗传算法是一种模拟自然选择和遗传机制的搜索算法。

它通过模拟生物种群的遗传、变异、选择等过程,逐步演化出最优解。

遗传算法在求解组合优化问题时,通过编码将解空间中的解表示成染色体,并利用交叉、变异等遗传操作来搜索更优的解。

通过不断迭代,遗传算法能够找到较好的解,但无法保证找到全局最优解。

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

《算法设计与分析》上机报告
姓名:张先荣学号:SA16225439 日期:2016/12/20
上机题
实验七:最佳调度问题(回溯算法) 目:
实验环境:
CPU:coreI7 ; 内存: 8G ;操作系统:Win10 ;软件平台Visual studio 2013 ;
一、算法设计与分析:
题目:
最佳调度问题(回溯算法)
设有n个任务由m个可并行工作的机器来完成,完成任务i需要时间为ti。

试设计一个算法找出完成这个任务的最佳调度,使完成全部任务的时间最早。

算法思想:
解空间的表示:
一个深度为N的M叉树。

int N;//任务数
int M;//机器数
int best;//最优值
int[] t;//每个任务所需要的时间序列
int[] len;//每台机器所需要的时间
int[] x;//当前路径
int[] bestx;//最优调度
基本思路:
1、搜索从开始结点(根结点)出发,以DFS搜索整个解空间。

2、每搜索完一条路径则记录下t和best序列,开始结点就成为一个活结点,
同时也成为当前的扩展结点。

在当前的扩展结点处向纵深方向移至一个新结点,
并成为一个新的活结点,也成为当前扩展结点。

3、如果在当前的扩展结点处不能再向纵深方向扩展,则当前扩展结点就成为死结点。

此时,应往回移动(回溯)至最近的一个活结点处,并使这个活结点成为当前的扩展
结点;直至找到一个解或全部解。

二、核心代码:
1.如图所示,即为回溯算法,以深度优先遍历方式遍历解空间树,每次向下遍历一层,意味着记录一次任务的完成序列。

Len加上任务序列的时间T,如果不是最优的,回溯,len减去任务的序列时间T
2.计算任务完成的时间。

某个机器时间最大,则他就是任务结束的时间。

3.解空间如图。

在这个图中能清晰地说明问题。

3叉树,机器数为3.深度为4,总共4层,则任务数为4,用3台机器去调度4个任务,把这棵树深度遍历,最后选出最优值。

三、结果与分析:
题目一:
如图所示,当任务数为10,机器数为7,总共耗时34.0782ms,最有时间为95单位时间,每个任务所花费时间就是随机数生成的时间序列是:67,45,80,32,59,95,37,46,28,20.机器调度顺序是1,2,3,2,4,5,6,6,1,4
如图所示,当任务数为20,机器数为2,总共耗时18.2999ms,最有时间为474单位时间,每个任务所花费时间就是随机数生成的时间序列是:73,31,66,7,50,9,11,53,66,90,99,12,37,31,31,38,9,82,60,93.
如图所示,当任务数为20,机器数为4,总共耗时57835.5837ms,
总结:
当任务数大于20,机器数大于5时,系统崩溃,所以回溯算法的时间复杂度较大。

附录(源代
码)算法源代码(C#描述)
public class BestSchedule
{
int N;//任务数
int M;//机器数
int best;//最优值
int[] t;//每个任务所需要的时间序列
int[] len;//每台机器所需要的时间
int[] x;//当前路径
int[] bestx;//最优调度
public void showTest()
{
N = 20;
Random r = new Random();
t=new int[N];//每个任务的时间
for (int i = 0; i < N; i++)
{
t[i] = r.Next(1,100);//随机数生成每个任务所需要的时间 }
len=new int[M];//记录每台机器已安排的时间
best = 9999999;
bestx = new int[N];
x = new int[N];
Stopwatch sw = new Stopwatch();
sw.Start();
backtrack(0);
sw.Stop();
TimeSpan ts2 = sw.Elapsed;
Console.WriteLine("程序运行总共花费{0}ms.",
ts2.TotalMilliseconds);
Console.WriteLine("最优时间是:");
Console.Write(best+" ");
Console.WriteLine("每个任务所花费的时间是:");
for (int i = 0; i < N; i++)
{
Console.Write(t[i] + " ");
}
Console.WriteLine();
Console.WriteLine("最佳调度序列是:");
for (int i = 0; i < N; i++)
{
Console.Write(bestx[i] + " ");
}
Console.ReadKey();
}
void backtrack(int dep)
{
if (dep == N)
{
int tmp = comp();
if (tmp < best)
{
best = tmp;
for (int i = 0; i < N; i++)
bestx[i] = x[i];
}
}
return;
}
for (int i = 0; i < M; i++)
{
len[i] += t[dep];
x[dep] = i + 1;
if (len[i] < best)
{
backtrack(dep + 1);
}
len[i] -= t[dep];
}
}
//计算完成任务的时间
int comp()
{
int temp = 0;
for (int i = 0; i < M; i++)
{
if (len[i] > temp)
{
temp = len[i];
}
}
return temp;
}
}
class Program
{
static void Main(string[] args)
{
BestSchedule bs = new BestSchedule(); bs.showTest();
}
}。

相关文档
最新文档