算法学习记录-图——应用之关键路径(Critical Path)

 

之前我们介绍过,在一个工程中我们关心两个问题:

(1)工程是否顺利进行

(2)整个工程最短时间。

 

之前我们优先关心的是顶点(AOV),同样我们也可以优先关心边(同理有AOE)。(Activity On Edge Network)

看看百度百科上解释:

AOE网:Activity on edge network

若在带权的有向图中,以顶点表示事件,以有向边表示活动,边上的权值表示活动的开销(如该活动持续的时间),则此带权的有向图称为AOE网。

如果用AOE网来表示一项工程,那么,仅仅考虑各个子工程之间的优先关系还不够,更多的是关心整个工程完成的最短时间是多少;

哪些活动的延期将会影响整个工程的进度,而加速这些活动是否会提高整个工程的效率。

因此,通常在AOE网中列出完成预定工程计划所需要进行的活动,每个活动计划完成的时间,要发生哪些事件以及这些事件与活动之间的关系,

从而可以确定该项工程是否可行,估算工程完成的时间以及确定哪些活动是影响工程进度的关键。

很显然,顶点表示事件,表示活动,边的权则表示活动持续时间。

AOE一般用来估算工程的完成时间。

AOE表示工程的流程,把没有入边的称为始点或者源点,没有出边的顶点称为终点或者汇点。一般情况下,工程只有一个开始,一个结束,

所以正常情况下,AOE只有一个源点一个汇点。

 

AOV和AOE的区别:

1.AOV用顶点表示活动的网,描述活动之间的制约关系。

2.AOE用边表示活动的网,边上的权值表示活动持续的时间。

AOE 是建立在子过程之间的制约关系没有矛盾的基础之上,再来分析整个过程需要的时间。

 

AOE研究:

a.完成整个过程至少需要多长时间。

b.哪些活动影响工程的进度?


 

关键路径:从源点到汇点具有最大长度的路径。这个概念要清楚,一个工程不一定有一条关键路径,可能会有多条。

关键活动:关键路径上的活动(边)。

针对上面AOE所关心的问题,要想缩短工程时间,就缩短关键路径上的过程即可。(缩短后可能出现之前的关键路径变成了非关键路径)

 

由于AOE网上所有的活动是可以并行进行。这里举一个例子,组装一个变形金刚,需要头,左膀右臂,身体,左腿右腿。

我们可以有两种方法:1.先做好头,做左手臂,做右手臂,做身体,做左腿,做右腿,然后再组装。

          2.同时做头、手臂、身体、腿的部分,每有一个头、两个手臂、两个腿和一个身体的时候,就可以组装了。

方法1.如我们计算机中的串行运行。这样时间开销是累加的。

方法2.如我们计算机中的并行运行。这样时间开销可以立体应用。在此方法中,同时做各个部位时间不同,比如做头时间最长,那么整个一个

变形金刚所用的时间决定与做头的时间,如果做手臂的时间是做头时间的一半,那么就是说做手臂的时间点可以在头做了一半的时候。只要不超过这个

时间点,手臂部分是不会影响整个工程的进度的。

 

这里定义四个定义:前两个针对顶点,后两个针对边

事件最早开始时间:顶点Vi最早发生的时间。

事件最晚开始时间:顶点Vi最晚发生的时间,超出则会延误整个工期。

活动的最早开始时间:边Eg最早发生时间。

活动的最晚开始时间:边Eg最晚发生时间。不推迟工期的最晚开工时间。

 

下面这个例子说明一下:

说明:上图中J中为49,是最早开始时间。这里可以看到最早开始时间,就是完要成该顶点,前面的所有到该点的路径都要已经完成。所以取路径最大那一条。

补充说明:

事件最早开始时间:例子图中,F点,ACF(9) 和 ADF(19),到达F点时候,保证AC和AD都完成,这样 F才能开始,所以F点的最早开始时间取最大值,即19.

       可以看出,要求出到某一点的最早开始时间,则需要将汇于该点的所有路径的时间求出来,取最大值。

事件最迟开始时间:这里是反着推,比如H点最迟开始时间,H到J 与 H到I到J两条路径,39 和 44,所谓最迟开始时间,就是超过这个时间就会影响整个工程进度,

         而这个时间是时间点,是从源点工程开始计时的,所以对于H点,39和44是相对于源点,如果取44,则H-J这条路径就会拖延,最迟开始时间选择最小值。

 

关键路径的特点我们寻找关键路径——关键路径就是关键活动(顶点与顶点之间的边组成),就是我们怎么判断该顶点是否为关键活动(边)的顶点,即判断边是否为关键活动。

        前面定义过,关键路径就是图中从源点到汇点最长(权值最大)的路径。

        这条路径就决定了整个工程的工期,这说明一个什么问题?

        关键路径上的顶点与顶点之间的活动的应该最早开始和最迟开始时间是相等的,

        如果不等那么说明活动还有余额时间(在最早开始时间和最迟开始时间之间可以任选一个时间点开始),这说明还有其他活动是决定这个工程时间的,那就不是关键路径了。


 

算法思想:

要准备两个数组,a:最早开始时间数组etv,b:最迟开始时间数组。(针对顶点即事件而言)

1.从源点V0出发,令etv[0](源点)=0,按拓扑有序求其余各顶点的最早发生时间etv[i](1 ≤ i ≤ n-1)。同时按照上一章

拓扑排序的方法检测是否有环存在。

2.从汇点Vn出发,令ltv[n-1] = etv[n-1],按拓扑排序求各个其余各顶点的最迟发生时间ltv[i](n-2 ≥ i ≥ 2);

3.根据各顶点的etv和ltv数组的值,求出弧(活动)的最早开工时间最迟开工时间,求每条弧的最早开工时间最迟开工时间是否相等,若相等,则是关键活动。

注意:1,2 完成点(事件)的最早和最迟。3根据事件来计算活动最早和最迟,从而求的该弧(活动)是否为关键活动。

 

 


 

关键代码:

1.对图进行拓扑排序,存储了拓扑排序的顺序,作为关键路径的计算最迟开始时间的依据。

 

 1 int TopplogicalSort(GraphAdjList *g)
 2 {
 3     int count=0;
 4     eNode *e=NULL;
 5 
 6     StackType *stack=NULL;
 7     StackType top=0;
 8     stack = (StackType *)malloc((*g).numVextexs*sizeof(StackType));
 9     
10     int i;
11     
12     //初始化拓扑序列栈
13     g_topOfStk = 0;
14     //开辟拓扑序列栈对应的最早开始时间数组
15     g_etv = (int *)malloc((*g).numVextexs*sizeof(int));
16     //初始化数组
17     for (i=0;i<(*g).numVextexs;i++)
18     {
19         g_etv[i]=0;
20     }
21     //开辟拓扑序列的顶点数组栈
22     g_StkAfterTop = (int *)malloc(sizeof(int)*(*g).numVextexs);
23     
24     
25     
26 
27     for (i=0;i<(*g).numVextexs;i++)
28     {
29         if (!(*g).adjList[i].numIn)
30         {
31             stack[++top] = i;
32     //        printf("init no In is %c\n",g_init_vexs[i]);
33         }
34     }
35     
36 
37     while(top)
38     {
39         int geter = stack[top];
40         top--;
41 
42         //把拓扑序列保存到拓扑序列栈,为后面做准备
43         g_StkAfterTop[g_topOfStk++] = geter;
44         
45         printf("%c -> ",g_init_vexs[(*g).adjList[geter].idx]);
46         count++;
47 
48         //获取当前点出度的点,对出度的点的入度减一(当前点要出图)。
49         //获取当前顶点的出度点表
50         e = (*g).adjList[geter].fitstedge;
51         while(e)
52         {
53             int eIdx = e->idx;
54             //选取的出度点的入度减一
55             int crntIN = --(*g).adjList[eIdx].numIn;
56             if (crntIN == 0)
57             {
58                 //如果为0,则说明该顶点没有入度了,是下一轮的输出点。
59                 stack[++top] = eIdx;
60         //        printf("running the vex is %c\n",g_init_vexs[e->idx]);
61             }
62 
63             //求出关键路径
64             if ((g_etv[geter] + e->weigh) > g_etv[eIdx])
65             {
66                 g_etv[eIdx] = g_etv[geter] + e->weigh;
67             }
68 
69             e = e->next;
70         }
71     }
72     if (count < (*g).numVextexs)//如果图本身就是一个大环,或者图中含有环,这样有环的顶点不会进栈而被打印出来。
73     {
74         return false;
75     }
76     else
77     {
78         printf("finish\n");
79         return true;
80     }
81     
82 }

 

 2.关键路径代码:

 1 void CriticalPath(GraphAdjList g)
 2 {
 3     int i;
 4     int geter;
 5     eNode *e = NULL;
 6     g_topOfStk--;
 7     //1.初始化最迟开始时间数组(汇点的最早开始时间(初值))
 8     g_ltv = (int *)malloc(sizeof(int)*g.numVextexs);
 9     for (i=0;i<g.numVextexs;i++)
10     {
11         g_ltv[i] = g_etv[g.numVextexs-1];
12     }
13 
14     //2.求每个点的最迟开始时间,从汇点到源点推。
15     while (g_topOfStk)
16     {
17         //获取当前出栈(反序)的序号
18         geter = g_StkAfterTop[g_topOfStk--];
19         //对每个出度点
20         if (g.adjList[geter].fitstedge != NULL)
21         {
22             e = g.adjList[geter].fitstedge;
23             while(e != NULL)
24             {
25                 int eIdx = e->idx;
26                 if (g_ltv[eIdx] - e->weigh < g_ltv[geter])
27                 {
28                     g_ltv[geter] = g_ltv[eIdx] - e->weigh;
29                 }
30                 e = e->next;
31             }
32         }
33     }
34 
35     int ete,lte;//活动最早开始和最迟开始时间
36 
37     
38 
39     printf("start:->");
40     //3.求关键活动,即ltv和etv相等的
41     for (i=0;i<g.numVextexs;i++)
42     {
43         if (g.adjList[i].fitstedge)
44         {
45             e = g.adjList[i].fitstedge;
46             while(e)
47             {
48                 int eIdx = e->idx;
49                 //活动(i->eIdx)最早开始时间:事件(顶点) i最早开始时间
50                 ete = g_etv[i];
51                 //活动(i->eIdx)最迟开始时间:事件(顶点) eIdx 最迟开始时间 减去 活动持续时间
52                 lte = g_ltv[eIdx] - e->weigh; 
53                 if (ete == lte)
54                 {
55                     printf("(%c - %c)->",g_init_vexs[i],g_init_vexs[eIdx]);
56                 }
57                 e= e->next;
58             }
59         }
60     }
61     printf(" end\n");
62 }

 

编程所用的图:

拓扑排序结果:

 

过程:

1.从J开始,无后继,不做任何事情;

2.G,G的ltv为27,ltv-weight = 27-2 < 27,所以G的ltv为25;

3.I,I的ltv为27,ltv-weight = 27 -3 < 27,所以I的ltv为24;

4.H,H的ltv为24(I的ltv),24-5 < 24,所以H 的ltv为19;

依次类推。。。

 

完成top排序和关键路径后:

全局存放各个顶点的最早开始和最迟开始时间:

完整代码:

  1 // grp-top.cpp : 定义控制台应用程序的入口点。
  2 //
  3 // grp-top.cpp : 定义控制台应用程序的入口点。
  4 //
  5 
  6 #include "stdafx.h"
  7 #include <stdlib.h>
  8 
  9 
 10 #define MAXVEX 100
 11 #define IFY 65535
 12 
 13 
 14 typedef char VertexType;
 15 typedef int  EdgeType;
 16 typedef int  IdxType;
 17 typedef int QueueType;
 18 typedef int StackType;
 19 
 20 
 21 //-------
 22 int *g_etv = NULL;
 23 int *g_ltv = NULL;
 24 int *g_StkAfterTop;
 25 int g_topOfStk;
 26 
 27 
 28 ///---------------------------------------
 29 //边节点
 30 typedef struct EdgeNode{
 31     IdxType idx;
 32     int weigh;
 33     struct EdgeNode* next;
 34 }eNode;
 35 
 36 //顶点节点
 37 typedef struct VexNode{
 38     int numIn;        //入度数量
 39     IdxType idx;
 40     eNode *fitstedge;
 41 }vNode;
 42 
 43 //图的集合:包含了一个顶点数组
 44 typedef struct {
 45     vNode adjList[MAXVEX];
 46     int numVextexs,numEdges;
 47 }GraphAdjList;
 48 
 49 ///-----------------------------------
 50 /*VertexType g_init_vexs[MAXVEX] = {'A','B','C','D','E','F','G','H','I','J','K','L'};
 51 
 52 char *g_input[] = {
 53     "A->B->C->D",
 54     "B->E",
 55     "C->F->I->J",
 56     "D->E->I->J",
 57     "E",
 58     "F->K",
 59     "G->F->H->K",
 60     "H->I",
 61     "I->J->L",
 62     "J->E->K",
 63     "K->L",
 64     "L"
 65 };*/
 66 
 67 ///-----------------------------------
 68 VertexType g_init_vexs[MAXVEX] = {'A','B','C','D','E','F','G','H','I','J'};
 69 
 70 char *g_input[] = {
 71     "A->B->C",
 72     "B->D->E",
 73     "C->D->F",
 74     "D->E",
 75     "E->G->H",
 76     "F->H",
 77     "G->J",
 78     "H->I",
 79     "I->J",
 80     "J",
 81     NULL
 82 };
 83 
 84 char *g_input_weigh[] = {
 85     "3,4",//A
 86     "5,6",//B
 87     "8,7",//C
 88     "3",//D
 89     "9,4",//E
 90     "6",//F
 91     "2",//G
 92     "5",//H
 93     "3",//I
 94     " ",//J
 95     NULL
 96 };
 97 //===============================================================
 98 //队列
 99 
100 //队列节点
101 typedef struct Node {
102     QueueType data;
103     struct Node *next;
104 }QNode,*qQNode;
105 
106 //队列指示
107 typedef struct {
108     int length;
109     qQNode frnt,rear;    
110 }spQueue;
111 
112 void init_Queue(spQueue *Q)
113 {
114     (*Q).frnt = NULL;
115     (*Q).rear = NULL;
116     (*Q).length = 0;
117 }
118 bool isEmptyQueue(spQueue Q)
119 {
120     if (Q.length == 0)
121     {
122         return true;
123     }
124     return false;
125 }
126 //进队
127 void unshiftQueue(spQueue *Q,QueueType elem)
128 {
129     //队列空
130     if (isEmptyQueue(*Q))
131     {
132         qQNode n = (qQNode)malloc(sizeof(QNode));
133         n->data = elem;
134         n->next = NULL;
135 
136         (*Q).frnt = n;
137         (*Q).rear = n;
138         (*Q).length = 1;
139     }
140     else
141     {
142         qQNode n = (qQNode)malloc(sizeof(QNode));
143         n->data = elem;
144         n->next = NULL;
145 
146         (*Q).rear->next = n;
147 
148         (*Q).rear = n;
149         (*Q).length++;
150     }
151 }
152 
153 //出队
154 QueueType shiftQueue(spQueue *Q)
155 {
156     if (isEmptyQueue(*Q))
157     {
158         printf("Warning:Queue is empty!!!\n");
159         return NULL;
160     }
161     if ((*Q).length == 1)
162     {
163         QueueType sh = (*Q).frnt->data;
164         (*Q).frnt = NULL;
165         (*Q).rear = NULL;
166         (*Q).length = 0;
167         return sh;
168     }
169     QueueType sh = (*Q).frnt->data;
170     (*Q).frnt = (*Q).frnt->next;
171     (*Q).length--;
172 
173     return sh;
174 }
175 
176 //打印队列
177 void prt_que(spQueue que)
178 {
179     if (isEmptyQueue(que))
180     {
181         return ;
182     }
183     qQNode pos = que.frnt;
184     while(que.rear->next != pos && pos != NULL)
185     {
186         printf(" %d ",pos->data);
187         pos = pos->next;
188     }
189     printf("\n");
190 }
191 //===============================================================
192 
193 ///-------
194 //由节点找节点的序号
195 IdxType strFindIdx(char ch)
196 {
197     int i=0;
198     VertexType *p = g_init_vexs;
199     while(p != NULL)
200     {
201         if(*p == ch)
202         {
203             return i;
204         }
205         p++;
206         i++;
207     }
208     return i;
209 }
210 
211 //由序号找节点
212 VertexType idxFindStr(IdxType i)
213 {
214     return g_init_vexs[i];
215 }
216 
217 void prt_strings(char *p)
218 {
219     char *pos = p;
220     while (NULL != *pos)
221     {
222         printf("%c",*pos);
223         pos++;
224     }
225     printf("\n");
226 }
227 
228 void prt_strArrays(char *p[],int num)
229 {
230     char **pos = p; 
231     int i=0;
232     while( *pos != NULL && i < num)
233     {
234         prt_strings(*pos);
235         pos++;
236         i++;
237     }
238 }
239 
240 //自己规定:顶点只能是大写。
241 bool isVexter(char p)
242 {
243     if (p>='A' && p<='Z')
244     {
245         return true;
246     }
247     return false;
248 }
249 
250 bool isNumeric(char p)
251 {
252     if (p >= '0' && p <= '9')
253     {
254         return true;
255     }
256     return false;
257 }
258 
259 void init_GrapAdjList(GraphAdjList *g,char **str,char **wstr)
260 {
261     char **pos = str;
262     
263     int cnt=0;
264     int vcnt = 0;
265     char **wpos = wstr;//weight value
266 
267     //入度清零
268     int i;
269     for (i=0;i<MAXVEX;i++)
270     {
271         (*g).adjList[i].numIn = 0;
272     }
273 
274     while (*pos != NULL) //g_input的每行的首指针
275     {
276         int i=0;
277         while(**pos != NULL) //g_input的每行字母
278         {
279             if(isVexter(**pos)) //判断是否为顶点(我规定‘A’-‘Z’之间为顶点标志)
280             {
281                 if (i == 0) //建立顶点的节点
282                 {
283                     (*g).adjList[cnt].idx = strFindIdx(**pos);
284                     (*g).adjList[cnt].fitstedge = NULL;
285                     
286                     i=1;
287                 }
288                 else if(i == 1) //建立第一个边的节点
289                 {
290                     eNode* n = (eNode*)malloc(sizeof(eNode));
291                     n->idx = strFindIdx(**pos);
292                     n->next = NULL;
293 
294                     //weight
295                     while (!isNumeric(**wpos))
296                     {
297                         (*wpos)++;
298                     }
299                     n->weigh = **wpos-'0';
300                     (*wpos)++;
301 
302                     (*g).adjList[cnt].fitstedge = n;
303                     i=2;
304 
305                     //添加入度
306                     int iidx = strFindIdx(**pos);
307                     (*g).adjList[iidx].numIn++;
308                 }
309                 else //边节点连接到前一个边节点上
310                 {    
311                     eNode* n = (eNode*)malloc(sizeof(eNode));
312                     n->idx = strFindIdx(**pos);
313                     n->next = NULL;
314 
315                     //weight
316                     while (!isNumeric(**wpos))
317                     {
318                         (*wpos)++;
319                     }
320                     n->weigh = **wpos-'0';
321                     (*wpos)++;
322 
323                     //first splist
324                     eNode *r = (*g).adjList[cnt].fitstedge;
325                     while (r->next != NULL)
326                     {
327                         r = r->next;
328                     }
329                     r->next = n;
330 
331                     //添加入度
332                     int iidx = strFindIdx(**pos);
333                     (*g).adjList[iidx].numIn++;
334                 }
335             }
336             (*pos)++; 
337         }
338 
339         wpos++;
340         cnt++;
341         pos++;
342         
343     }
344     (*g).numVextexs = cnt;
345 }
346 
347 int TopplogicalSort(GraphAdjList *g)
348 {
349     int count=0;
350     eNode *e=NULL;
351 
352     StackType *stack=NULL;
353     StackType top=0;
354     stack = (StackType *)malloc((*g).numVextexs*sizeof(StackType));
355     
356     int i;
357     
358     //初始化拓扑序列栈
359     g_topOfStk = 0;
360     //开辟拓扑序列栈对应的最早开始时间数组
361     g_etv = (int *)malloc((*g).numVextexs*sizeof(int));
362     //初始化数组
363     for (i=0;i<(*g).numVextexs;i++)
364     {
365         g_etv[i]=0;
366     }
367     //开辟拓扑序列的顶点数组栈
368     g_StkAfterTop = (int *)malloc(sizeof(int)*(*g).numVextexs);
369     
370     
371     
372 
373     for (i=0;i<(*g).numVextexs;i++)
374     {
375         if (!(*g).adjList[i].numIn)
376         {
377             stack[++top] = i;
378     //        printf("init no In is %c\n",g_init_vexs[i]);
379         }
380     }
381     
382 
383     while(top)
384     {
385         int geter = stack[top];
386         top--;
387 
388         //把拓扑序列保存到拓扑序列栈,为后面做准备
389         g_StkAfterTop[g_topOfStk++] = geter;
390         
391         printf("%c -> ",g_init_vexs[(*g).adjList[geter].idx]);
392         count++;
393 
394         //获取当前点出度的点,对出度的点的入度减一(当前点要出图)。
395         //获取当前顶点的出度点表
396         e = (*g).adjList[geter].fitstedge;
397         while(e)
398         {
399             int eIdx = e->idx;
400             //选取的出度点的入度减一
401             int crntIN = --(*g).adjList[eIdx].numIn;
402             if (crntIN == 0)
403             {
404                 //如果为0,则说明该顶点没有入度了,是下一轮的输出点。
405                 stack[++top] = eIdx;
406         //        printf("running the vex is %c\n",g_init_vexs[e->idx]);
407             }
408 
409             //求出关键路径
410             if ((g_etv[geter] + e->weigh) > g_etv[eIdx])
411             {
412                 g_etv[eIdx] = g_etv[geter] + e->weigh;
413             }
414 
415             e = e->next;
416         }
417     }
418     if (count < (*g).numVextexs)//如果图本身就是一个大环,或者图中含有环,这样有环的顶点不会进栈而被打印出来。
419     {
420         return false;
421     }
422     else
423     {
424         printf("finish\n");
425         return true;
426     }
427     
428 }
429 void CriticalPath(GraphAdjList g)
430 {
431     int i;
432     int geter;
433     eNode *e = NULL;
434     g_topOfStk--;
435     //1.初始化最迟开始时间数组(汇点的最早开始时间(初值))
436     g_ltv = (int *)malloc(sizeof(int)*g.numVextexs);
437     for (i=0;i<g.numVextexs;i++)
438     {
439         g_ltv[i] = g_etv[g.numVextexs-1];
440     }
441 
442     //2.求每个点的最迟开始时间,从汇点到源点推。
443     while (g_topOfStk)
444     {
445         //获取当前出栈(反序)的序号
446         geter = g_StkAfterTop[g_topOfStk--];
447         //对每个出度点
448         if (g.adjList[geter].fitstedge != NULL)
449         {
450             e = g.adjList[geter].fitstedge;
451             while(e != NULL)
452             {
453                 int eIdx = e->idx;
454                 if (g_ltv[eIdx] - e->weigh < g_ltv[geter])
455                 {
456                     g_ltv[geter] = g_ltv[eIdx] - e->weigh;
457                 }
458                 e = e->next;
459             }
460         }
461     }
462 
463     int ete,lte;//活动最早开始和最迟开始时间
464 
465     
466 
467     printf("start:->");
468     //3.求关键活动,即ltv和etv相等的
469     for (i=0;i<g.numVextexs;i++)
470     {
471         if (g.adjList[i].fitstedge)
472         {
473             e = g.adjList[i].fitstedge;
474             while(e)
475             {
476                 int eIdx = e->idx;
477                 //活动(i->eIdx)最早开始时间:事件(顶点) i最早开始时间
478                 ete = g_etv[i];
479                 //活动(i->eIdx)最迟开始时间:事件(顶点) eIdx 最迟开始时间 减去 活动持续时间
480                 lte = g_ltv[eIdx] - e->weigh; 
481                 if (ete == lte)
482                 {
483                     printf("(%c - %c)->",g_init_vexs[i],g_init_vexs[eIdx]);
484                 }
485                 e= e->next;
486             }
487         }
488     }
489     printf(" end\n");
490 }
491 
492 
493 int _tmain(int argc, _TCHAR* argv[])
494 {
495     GraphAdjList grp;
496     printf("print Matix: of Vextexs:\n");
497     prt_strArrays(g_input,10);
498     printf("print Matix: of Weigh:\n");
499     prt_strArrays(g_input_weigh,10);
500 
501     init_GrapAdjList(&grp,g_input,g_input_weigh);
502     printf("Top sort:\n");
503     if (!TopplogicalSort(&grp))
504     {
505         printf("grp wrong!\n");
506     }
507     
508     CriticalPath(grp);
509 
510     getchar();
511     return 0;
512 }


测试结果:

 

 

 

 

posted @ 2013-12-02 17:29 sjdang 阅读(...) 评论(...) 编辑 收藏