对信息学竞赛中动态规划问题的认识与思考.docx

上传人:b****5 文档编号:8326023 上传时间:2023-01-30 格式:DOCX 页数:52 大小:96.51KB
下载 相关 举报
对信息学竞赛中动态规划问题的认识与思考.docx_第1页
第1页 / 共52页
对信息学竞赛中动态规划问题的认识与思考.docx_第2页
第2页 / 共52页
对信息学竞赛中动态规划问题的认识与思考.docx_第3页
第3页 / 共52页
对信息学竞赛中动态规划问题的认识与思考.docx_第4页
第4页 / 共52页
对信息学竞赛中动态规划问题的认识与思考.docx_第5页
第5页 / 共52页
点击查看更多>>
下载资源
资源描述

对信息学竞赛中动态规划问题的认识与思考.docx

《对信息学竞赛中动态规划问题的认识与思考.docx》由会员分享,可在线阅读,更多相关《对信息学竞赛中动态规划问题的认识与思考.docx(52页珍藏版)》请在冰豆网上搜索。

对信息学竞赛中动态规划问题的认识与思考.docx

对信息学竞赛中动态规划问题的认识与思考

【关键字】动态规划、状态

【摘要】本文讨论了一种解决问题十分有效的技术—动态规划。

它较高的解题效率一直受到很大的关注。

本文首先对动态规划的理论基础进行了讨论。

给出了一个用动态规划可以解决的问题的两个先决条件:

“最优子结构”与“无后效性”。

接着,讨论了在实际应用中的两个比较常见的问题:

动态规划中状态的选定与存储。

再通过以上问题的讨论,引出了动态规划的基本思维方法:

“不做已经做过的工作”以及“动态规划”技术在解决问题中速度惊人的原因—解决了查看中的冗余,达到了速度的极限。

最后,阐述了解决动态规划问题的一般步骤,即思考,计划,应用。

【正文】

一.引论

在信息学竞赛中,特别是最近几年,“动态规划”作为一种解题工具,经常被提及。

其应用范围愈来愈广,应用程度也愈来愈深。

那么,“动态规划”究竟与其它的算法有什么差别?

它有什么具体的应用价值呢?

本文将对此进行讨论。

我们先通过一个具体问题认识一下“动态规划”。

〖例1〗:

图1中给出了一个地图,地图中每个顶点代表一个城市,两个城市间的连线代表道路,连线上的数值代表道路的长度。

现在,我们想从城市A到达城市E,怎样走路程最短,最短路程的长度是多少?

假设:

Dis[X]为城市X到E的最短路线的长度;(X表示任意一个城市)

Map[I,J]表示I,J两个城市间的距离,若Map[I,J]=0,则两个城市不连通。

这个问题我们可以用搜索法来做,程序很容易写出来:

Var

Se:

未访问的城市集合;

FunctionLong(Who:

当前访问城市):

Integer;:

求当前访问城市与城市E的最短距离。

Begin

IfWho=EThenSearch:

=0

Else

Begin

Min:

=Maxint;

ForI取遍所有城市Do

If(Map[Who,I]>0)And(IInSe)Then

Begin

Se:

=Se-[I];

J:

=Map[Who,I]+Long(I);

Se:

=Se+[I];

IfJ

=J;

End;

Long:

=Min;

End;

End;

Begin

Se:

=除A外所有城市的集合;

Dis[A]:

=Long(A);

End.

这个程序的效率如何呢?

我们可以看到,每次除了已经访问过的城市外,其他城市都要访问,所以时间复杂度为

,这是一个“指数级”的算法,那么,还有没有更好的算法呢?

首先,我们来观察一下这个算法。

在求从B1到E的最短路径的时候,先求出从C2到E的最短路径;而在求从B2到E的最短路径的时候,又求了一遍从C2到E的最短路径。

也就是说,从C2到E的最短路径我们求了两遍。

同样可以发现,在求从C1、C2到E的最短路径的过程中,从D1到E的最短路径也被求了两遍。

而在整个程序中,从D1到E的最短路径被求了四遍,这是多么大的一个浪费啊!

如果在求解的过程中,同时将求得的最短路径的距离“记录在案”,随时调用,那会是多么的方便啊!

于是,一个新的思路诞生了,即:

由后往前依次推出每个Dis值,直到推出Dis[A]为止。

这个思路的确很好,但等等,究竟什么是“由后往前”呢?

所谓“后”、“前”是我们自己为城市编的序号,当两个城市I,J的前后顺序定为I“前”J“后”时,必须满足这个条件:

或者I,J不连通,或者Dis[I]+Map[I,J]≥Dis[J]。

因为如果I,J连通且Dis[I]+Map[I,J]

那么,我们如何划分先后次序呢?

如图2所示。

我用不同颜色给城市分阶段,可以用阶段表示每个城市的次序,因为阶段的划分有如下性质:

1:

阶段I的取值只与阶段I+1有关,阶段I的取值只对阶段I-1的取值产生影响;

2:

每个阶段的顺序是确定的,不可以调换任两个阶段顺序;

通过这两个性质,可以推出阶段作为“前”、“后”顺序满足刚才提出的条件,所以我们可以用阶段作为每个城市的次序,然后从阶段3倒推至阶段1,再推出Dis[A]。

公式:

Dis[X]=Min{Dis[Y]:

Y是下一个阶段中与X相连通的城市}

注:

可以把E看成第4个阶段,A看成第0个阶段。

程序:

Dis[E]=0

ForX=阶段3的每个城市Downto阶段0的每个城市Do

Begin

Dis[X]:

=Maxint;

ForY=阶段X的下一个阶段中的每个城市Do

IfDis[Y]+Map[X,Y]

Dis[X]:

=Dis[Y]+Map[X,Y];

End。

这个程序的时间复杂为

,比上一个程序的复杂度

要小得多。

第二个算法就是“动态规划”算法。

二.动态规划的理论基础

通过上面的例子,我们对“动态规划”有了一个初步认识,它所处理的问题是一个“多阶段决策问题”。

我们现在对一些概念进行具体定义:

状态(State):

它表示事物的性质,是描述“动态规划”中的“单元”的量。

如例1中的城市A,B1,D2,E都为单个的状态。

阶段(Stage):

阶段是一些性质相近,可以同时处理的状态集合。

通常,一个问题可以由处理的先后次序划分为几个阶段。

如例1中的问题由三个阶段组成,其中阶段1包含状态B1,B2。

实际上,阶段只是标识那些处理方法相同、处理顺序无关的状态。

一个阶段既可以包含多个状态,也可以只包含一个状态。

其关系很类似分子与原子的关系。

状态转移方程:

是前一个阶段的状态转移到后一个的状态的演变规律,是关于两个相邻阶段状态的方程,是“动态规划”的中心。

决策(Decision):

每个阶段做出的某种选择性的行动。

它是我们程序所需要完成的选择。

动态规划所处理的问题是一个“多阶段决策问题”,一般由初始状态开始,通过对中间阶段决策的选择,达到结束状态。

这些决策形成了一个决策序列,同时确定了完成整个过程的一条活动路线(通常是求最优的活动路线)。

如图3所示。

总体上来说,一个“动态规划”算法应该包含上面提到的几种基本关系与属性。

那么,什么样的“决策问题”才可以划分阶段,来用“动态规划”求解呢?

我们说一个“决策问题”只有具有以下性质时,才可以考虑划分阶段,用“动态规划”算法:

一:

最优子结构

即一个问题的最优解只取决于其子问题的最优解,也就是说,非最优解对问题的求解没有影响。

在例1中,如果我们想求Dis[B1]的最小值,我们只需要知道C1,C2,C3的最小值,而不用考虑其他的路径,因为其他的非最优路径肯定对Dis[B1]的结果没有影响。

我们再来看一个问题:

〖例2〗有4个点,分别是A、B、C、D,如图4所示,相邻两点用两条连线C2k,C2k-1(1≤k≤3)表示两条通行的道路。

连线上方的数字表示道路的长度。

我们定义从A到D的所有路径中,长度除以4所得余数最小的路径为最优路径。

求一条最优路径。

在这个题目中,我们如果还按照刚才的方法来求解就会发生错误。

例如,按照例1的思维,A最优取值可以由B的最优取值来确定,而B的最优取值为0,所以A的最优值为2,而实际上,路径C1—C3—C5可得最优值为0,所以,B的最优路径并不是A最优路径的子路径,也就是说,A的最优取值不是由B的最优取值决定的,其不具有最优子结构。

由此可见,并不是所有的“决策问题”都可以用“动态规划”来解决。

所以,只有当一个问题呈现出最优子结构时,“动态规划”才可能是一个合适的侯选方法。

二:

无后效性

即一个问题被划分阶段后,阶段I中的状态只能由阶段I+1中的状态通过状态转移方程得来,与其他状态没有关系,特别是与未发生的状态没有关系,这就是无后效性。

实际上,如果我们把这个问题中的状态定义成图中的顶点,两个状态之间的转移定义为边,转移过程中的权值增量定义为边的权值,则这个问题实际上就是在一个“有向无环加权图”中寻找两个顶点路径的问题。

因为无后效性,所以没有环路(否则,无论如何划分阶段,都可以出现后效性)。

即这个图可以进行“拓扑排序”,那么,我们至少可以以他们拓扑排序的顺序划分阶段。

我们来看一个具体例子:

 

〖例3〗:

欧几里德货郎担问题是对平面给定的n个点确定一条连结各点的、闭合的游历路线问题。

图5(a)给出了七个点问题的解。

Bitonic旅行路线问题是欧几里德货郎担问题的简化,这种旅行路线先从最左边开始,严格地由左至右到最右边的点,然后再严格地由右至左到出发点,求路程最短的路径长度。

图5(b)给出了七个点问题的解。

这两个问题看起来很相似。

但实质上是不同的。

为了方便讨论,我将每个顶点标记了号码。

由于必然经过最右边的顶点7,所以一条路(P1—P2)可以看成两条路(P1—7)与(P2—7)的结合。

所以,这个题目的状态可以用两条道路结合的形式表示。

我们可以把这些状态中,两条路中起始顶点相同的状态归于一个阶段,设为阶段[P1,P2]。

那么,对于Bitonic旅行路线问题来说,阶段[P1,P2]如果可以由阶段[Q1,Q2]推出,则必须满足的条件就是:

P1

例如,阶段[3,4]中的道路可以由阶段[3,5]中的道路加一条边4—5得出,而阶段[3,5]的状态却无法由阶段[3,4]中的状态得出,因为Bitonic旅行路线要求必须严格地由左到右来旅行。

所以如果我们已经知道了阶段[3,4]中的状态,则阶段[3,5]中的状态必然已知。

因此我们可以说,Bitonic问题满足“无后效性原则”,可以用“动态规划”算法来解决,其程序可以参见Pro_3_1.Pas。

对于欧几里德货郎担问题,阶段与阶段之间没有什么必然的“顺序”。

如道路{3—2—5—7,4—6—7}属于阶段[3,4],可由属于阶段[2,4]的道路{2—5—7,4—6—7}推出;而道路{2—3—6—7,4—5—7}属于阶段[2,4],可由属于阶段[3,4]的道路{3—6—7,4—5—7}推出。

如果以顶点表示阶段,推出关系表示边,那么,阶段[3,4]与阶段[2,4]对应的关系就如图6所示。

我们可以很清晰地看出,这两个阶段的关系是“有后效性”的。

因为这个图中存在“环路”。

对于这个问题是不能像上一个问题那样来解决的,事实上,这个问题是一个NP完全问题,其解决的时间复杂度很可能是指数级的。

所以,对于一个问题能否用“动态规划”来解决的一个十分关键的判断条件就是“它是否有后效性”。

而我们在判断这个问题是否有“后效性”时,一个很有效的方法就是将这个问题的阶段作为顶点,阶段与阶段之间的关系看作有向边,判断这个有向图是否为“有向无环图”,亦即这个图是否可以进行“拓扑排序”。

通过上面的说明,我们可以总结出一些解决“动态规划”问题的基本方法与步骤:

1:

确定问题的研究对象,即确定状态。

2:

划分阶段,确定阶段之间的状态转移方程。

3:

考察此问题现在可否用“动态规划”来解决:

①:

考察此问题是否具有“最优子结构”。

②:

考察此问题是否为“无后效性”。

4:

如果发现此问题目前不能用“动态规划”来解决,则应该调整相应的定义与划分,以达到可以用“动态规划”来解决。

以上只是一般情况下的“动态规划”思维过程。

一些较为简单的问题可以“按部就班”来操作,但大多数的“动态规划”问题,特别是作为信息学竞赛中的“动态规划”问题,考察的知识是多方面的,应用的技巧是灵活多变的。

下面,对“动态规划”在应用中的一些重点、难点进行讨论。

三.动态规划的实际应用

我们衡量一个算法的标准,无外乎时间、空间两项指标。

“动态规划”算法的时间大多数为“多项式级”的,比起同样解决这个问题的搜索算法“指数级”的时间来说,“动态规划”的时间需要是很少的,所以我们在实际应用中,很少考虑“动态规划”算法的时间问题,而最经常考虑的是空间问题:

即状态的选定与存储。

1:

状态的选定:

对于一个“动态规划”算法来说,阶段的划分显得不很重要,因为阶段只是一些可以等同处理的状态的集合,我们尽可以把单个的状态定义为一个阶段。

所以,状态的选定对整个问题的处理起了决定性的作用。

我们选定的状态必须满足如下两点:

1:

状态必须完全描述出事物的性质,两个不同事物的状态是不同的;

2:

必须存在状态与状态之间的“转移方程”。

以便我们可以由“初始问题”对应的状态逐渐转化为“终结问题”对应的状态。

状态是描述事物性质的量,所以我们应该以这个标准,根据题目中的具体要求来具体分析,我们来看下面一个例子:

〖例4〗有一个奶牛运输公司,需在7个农场A,B,C,D,E,F,G之间运输奶牛,从A出发,完成任务后要回到A。

运输车每次只能运送一头奶牛,每个运输任务是由一对字母给出的,分别表示奶牛从哪个农场被运向另外一个农场。

任务总数为N(1≦N≦12)。

已知这些农场之间的距离,求出一条完成所有任务的最短路线。

我们先分析这个问题的解决方法:

我们的目的是完成一些任务,由于运输车每次只能运送一头奶牛,所以我们只能一个接一个地处理这些任务。

由于题目求的是一条完成这些任务的最短路线,所以在完成任务的过程中,我们走的路线都是最短路线,也就是说,一旦我们确定了完成任务的顺序P1—P2—……—PN,那么这条路线的最短时间也就确定了。

我们的目的就是确定这N个任务的完成顺序。

所以,这个问题的研究对象就是某些任务的集合S。

那么,状态该如何定义呢?

既然研究对象是集合,所以很自然地就想起了描述集合中元素关系的方法。

我们可以把状态定义为一个数组(

),每一个

或者为0,表示任务i不在当前集合中;或者为1,表示任务i在当前集合中。

但如果这样定义状态,我们会发现,无法写出状态转移方程,因为涉及到前后两个任务的“接口”的最短路线长度。

所以我们还需要一个量x来限定当前这个任务集合中首先要执行任务,则一个状态定义为(

),其中

必为1。

那么,一个状态对应的权值就是完成这个状态描述的任务集合中任务的最短路线长度。

则状态转移方程为:

)=

其中,y是不等于x且

=1的值,若i≠x,则

假设Dis(x,y)为由任务x的终点城市到任务y的起点城市的最短距离。

这个转移方程的初始条件为:

)={任务x的终点城市到A的距离},其中

,其余的

均为0。

在实际处理中,用这样一个最大可能为12维的数组显然是不方便的,既然每一位只能是0或1,所以我就把(

)看作一个十进制数的二进制表示,于是,状态数组为一个二维数组,具体程序见附录中的Pro_4_1.Pas。

从这个问题中我们可以看出,一个“动态规划”问题的状态选定实质上就是选择描述这个问题中事物的最贴切,最简洁的方法。

状态的选定不是一蹴而就的,而是一个在思考过程中逐步调整,逐步完善的过程。

2:

状态的存储

当状态选定后,我们面对的问题就是如何存储状态了。

从理论上讲,每一个状态都应该存储两个值,一个是此状态的最优的权值,一个是决策标识值。

但实际中,我们面对的“动态规划”问题很多都是状态数目十分庞大,这就要求我们在存储上必须做一定的优化才可以实现这个算法的程序化。

主要的优化就是:

舍弃一切不必要的存储量。

在一些问题中,题目给出了只在某一范围内的关系,所以我们只需存储这一范围内的状态即可。

下面来看这个问题:

〖例5〗一个生物体的结构可以用“基元”的序列表示,一个“基元”用一些英文字符串表示。

对于一个基元集合P,可以将字符串S作为N个基元P1,p2…pN的依次连接而成。

问题是给定一个字符串S和一个基元集合P,使S的前缀可由P中的基元组成。

求这个前缀的最大长度。

基元的长度最大为20,字符串的长度最大为500,000。

例如基元集合为{a,AB,BbC,Ca,Ba},字符串为ABABACABAABCB,则最大长度为11,具体组成见图7。

图中不同的“基元”以不同的颜色标出。

这个问题的状态十分容易确定,字符串中的每一位的性质只有两种,即:

包含这一位字符的字符串前缀是否可以由基元序列构成。

那么,我们就可以设一个数组:

Could:

Array[1..Max]OfBoolean作为表示字符串中每一位的状态。

那么,状态转移方程为:

Could[K]:

=Could[K]Or(Could[K-W]And(S[K-W+1...K]=Ji[I]))

其中,Could[k]的初始值均为False,Ji[I]表示第I个基元对应的字符串,其长度为W,S[K-W+1...K]为题中给定的字符串从K-W+1位到K位的字符组成的字符串。

写到这里,这个问题似乎已经解决了,我们只需求出使Could[I]为True最大的I即可。

但只要回过头看一看问题就会发现,字符串最长为500,000,根本无法存储,该如何是好呢?

如果再仔细观察一遍转移方程,就会发现:

Could[I]只与Could[I-W]发生关系,而W为基元的长度,基元的长度最大为20,也就是说,我们如果想推出Could[I],最多只需要知道Could[I-20]至Could[I-1]的值即可,其它的Could值我们可以不去管它。

这样,我们需要记录的Could值一下子就由500000减少到20,这个突变是巨大的。

具体程序见Pro_5.Pas。

这个问题为什么只记录很少的状态就可以呢?

这是因为它特殊的转移方程与求解顺序。

求解顺序是按照字符串中位置的先后而定的,而转移方程中,当前状态只与比它先求解的那20个状态发生关系,所以这个问题可以如此解决。

但对于大多数问题来说,这样“美丽”的“结构”不一定具备,那么我们在解决这样的问题时,只有“处心积虑”地削减存储量。

我们来看一个例子。

〖例6〗某公司运进一批箱子,总数为N(1≤N≤1000)由“传送带”依次运入,然后在仓库内至多排成P(1≤P≤4)列,(如图8所示)。

现已知运来的箱子最多为M(1≤M≤20)种,想把同一种类的箱子尽量排在一起,以便美观。

“美观程度”T定义为:

T=Σ(每列依次看到的不同种类数);所谓“依次看到的不同种类数”即为:

如果某一列中第K个箱子与第K-1个箱子种类不同,则“美观程度”的值加1。

求一种调动安排,使各列的“美观程度”值的和最小。

这个问题的思路如下:

第K个箱子需要选择一列来放,如果它放在与其种类不同的某个箱子上,则它会使总的“美观程度”值加1。

也就是说,第K个箱子的摆放只与当前P列的顶端情况有关。

又如果想求这个箱子摆放后的最小值,自然需要这个前(K-1)个箱子在此顶端情况时的最小值。

这个多阶段决策问题同时满足最优化原理与无后效性原则,所以这个问题可以用“动态规划”的方法来解决。

则状态的选定很明显,为(k,P1,P2,P3,P4),K是当前即将放入的箱子的编号,P1—P4放入第K个箱子后各列的“顶端情况”,即放入第K个箱子后顶端是什么种类的箱子(假设当前有4列)。

则阶段划分也很明显,以箱子由传送带运来的先后顺序划分阶段。

那么,状态转移方程就是:

(K,P1,P2,P3,P4)=Min(K-1,Q1,Q2,Q3,Q4)+Dis}

其中,Q1—Q4为未放入第K个箱子前各列的情况,Dis为0或1,取决与第K个箱子与其列中上一个箱子的种类有无差别。

写到这里,“动态规划”的算法各个步骤已经清楚了,似乎可以直接写程序了。

但对于本题来说,存储大量的信息,是我们所不得不面对的一个问题。

Pascal语言所允许的最大可用空间为640KB,在保护模式下,最多也不过1000多KB。

一个“动态规划”程序所需存储的一般有两个值,一是决策信息,二是状态的价值。

对于本题,若按普通的方法,最多时为1000×20

×(1+2)字节,根本无法存储。

必须对存储做一定的优化,舍弃一切不必要的量:

1.只记录每步的“决策信息”,不记录每步的状态的价值,这样可省下

的空间;

2.对每步的状态来说,所对应的P列顶端不含有两个同一种的箱子;

3.除本次要选的列以外,其它(P-1)列顶端的排列对结果没影响,所以存储的状态应为另外(P-1)列的组合,而非排列。

这样,总的存储量可由24000KB减少到1200KB,缩小了约20倍,在Pascal保护模式下完全可以操作。

具体的程序见Pro_6.Pas。

通过以上两个问题,我们可以看出,“动态规划”在实际应用中,速度几乎“不成问题”,但存储状态的“环节”却必须思考再思考,优化再优化。

近几年的试题中,大多数“动态规划”问题的难点与重点就是“动态规划”中状态的选定与存储。

这也是“动态规划”在实际应用中的重中之重。

四.动态规划的深入思考

以上讨论了“动态规划”的理论基础与在实际应用中可能遇到的问题。

那么,“动态规划”究竟好在哪里?

我们为什么要用“动态规划”呢?

从上面的一些例子可以看出,“动态规划”这种方法最大优点就是节约了时间。

这个“巨大的优点”可以从例1的解决中看出。

在实际中,搜索算法与“动态规划”算法的时间差异是巨大的,表1是例1的两个算法对应程序的执行时间。

 

测试数据

算法

1

2

3

4

`

5

搜索算法

1.04s

1.42s

1.05s

7.42s

22.92s

动态规划算法

<0.01s

<0.01s

0.06s

0.02s

0.01s

(表1)

从这张执行时间表可以看出,“动态规划”算法与搜索算法在实际应用中的运行时间差异是巨大的。

从理论上讲,搜索算法的时间复杂度为

,“动态规划”算法的时间复杂度是

,这两个时间根本就不是一个数量级的。

指数级的时间增长是十分可怕的,远远大于多项式级的算法。

所以,我们经常称多项式级的算法为一个“较好”的算法,称指数级的算法是一个“较差”的算法。

“动态规划”程序实质上是一种“以空间代价来换取时间代价”的技术,它在实现的过程中,不得不存储产生过程中的各种状态。

所以,它的空间复杂度要大于其它的算法。

以Bitonic旅行路线问题为例,这个问题也可以用搜索算法来解决(具体见程序Pro_3_2.Pas)。

“动态规划”的时间复杂度为

,搜索算法的时间复杂度为

,但从空间复杂度来看,“动态规划”算法为

,而搜索算法为O(N)。

搜索算法反而优于“动态规划”算法。

那么,我们为何选择“动态规划”算法来解决例3呢?

因为“动态规划”算法在空间上可以承受(事实上是很可以承受),而搜索算法在时间上却十分巨大,所以我们“舍空间而取时间”。

这个问题在例4(即奶牛运输问题)中更加明显。

例4问题实质上是一个求最优的“哈密尔顿(Hamilton)回路”问题。

这个问题是一个NP问题,人们到目前为止还没有找到这个问题的“多项式级”解法。

所以,我们在进行“动态规划”算法时不得不记录每一种状态后再进行递推。

我们在进行递推时好像是在进行着一个“多项式级”的算法。

实质上,记录的状态数目已经是“指数级”的了。

我们用的“动态规划”算法从理论上讲,与普通搜索算法的时间复杂度差不多,而空间复杂度却远远高于普通的搜索算法(“动态规划”算法的空间复杂度为

,搜索算法的空间复杂度为O(N))。

那么,为什么用“动态规划”方法解决此问题要比搜索方法快呢?

(快多少可参见下边的表2)多用的那部分空间究竟干什么了?

这两个问题的答案就是我们用“动态规划”技术的根本性目的:

“解决冗余”。

我们在用搜索算法的过程中,做了一些已经做过的工作。

以例4为例:

假设目前待解决的任务集合为[1,2,3,4],我们如果选定头两个解决的任务为1,2,那么还需要从后两个任务3,4中选择解决顺序。

而如果选定前两个解决的任务为2,1(1,2与2,1不同,解决的次序不同),那么又得重新解决一遍工作“从后两个任务3,4中选择解决顺序”,这是多么大的一个浪费呀!

这句话在文章开头分

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 经管营销 > 公共行政管理

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1