你好,2017

By | 2017年1月1日

一、

每年的12月31日晚,世界的各个地区都有数以万计的人们在街头、广场,伴随着点亮夜空的烟火与迎接新年倒计时的摩登大厦,守候着新的一年的到来。

纽约时代广场会举行「降球仪式」——在23时59分整,一个大水晶球从广场顶部缓缓落下,00点00分整刚好到达地面,以此宣告整个美国新的一年的到来。在英国伦敦,人们会聚集在大本钟下,距离当日结束还有10秒之时,开始倒数,庆祝新的一年。台北的101大厦,进入跨年前夕几分钟左右,会将灯光完全熄灭,待到最后读秒的时刻,以8层楼为一个单位自底向上点亮灯光,时间归零的一刻会进行数分钟的烟火表演。

二、

奥地利的维也纳金色大厅,每年的第一天总会迎来一场音乐会。除去1939年第一届是在12月31日举办以及1945年因战争停办的一届,全世界有数十亿的人们,新年伊始,通过电视或者网络的转播,观看这场已经持续了76载的古典音乐盛会。

三、

学校的跨年晚会、路边张灯结彩的店面、寒冬中在街头相拥的情侣、饭店门口面带酒色相互搀扶的兄弟,都预示着即将来临一个「不同寻常的日子」。

四、

欢度新年的人群中,有很多人都停下了手边的工作、放下了眼前的烦恼,企盼新的一年能结交到好运。他们仿佛认为,旧年的最后一天一旦过去,便真的能时来运转,真的能将一切不如意给带走似的。但这其中更多的人,得到的只是暂时的麻木与短暂的忘却罢了。

五、

自研究生入学、尤其是研二以来,很多难题都无法妥善解决,节奏也被打乱太多,只是没到焦头烂额的境遇。终于,在最近一段时间感到有了一些好的转变。

高峰之后自然会遇到低谷,低谷之后必然会有所回升。

六、

泛黄的灯光洒在书桌上。

我回想着即将过去的一年的收获与付出,静候新的一天的到来。

睡前,打开音乐听着一些熟悉的旋律,我仿佛也置身于金色大厅,情绪随动听的音符共同跳动着。

「砰」!

零点时刻,烟花绽放在维多利亚港口,映红了我的面庞。

你好,2017.

冬游记·瞻园行

By | 2016年12月18日

一、

我深深地打了一个呵欠,曲折的血丝散布在眼白上格外明显。

「最近好累啊」,我和舍友唠叨说。

「是啊,我这段时间也是没精打采的」,舍友也表示这段时间体力消耗了太多,有点不堪重负了。

「以前还想着自己能实现很多,不过最近发现自己想法简单了很多,只想熬过研究生这个阶段,以后好好过日子。希望自己以后不是那种事业型的人,像我们实验室某些导师那样太辛苦了」,他又接着补充到。

「确实,我们实验室有些导师真的有点过了,也不知道他们的身体怎么那么好,感觉他们每天的睡眠时间太少了」,我表示了一下我对这些导师的观点。

「他们这都是在透支生命啊,这到了以后肯定会有隐患的」,室友也赞同。

「对了,最近你怎么也不出去浪了啊,以前你可是经常出去的啊」,我和室友打趣到。

「这还哪有功夫出门啊,我只希望能有个周末,只想睡个好觉」,他面带倦色的说。

自从开题那段时间以来,我们终日都毫不停歇地奔波在实验室和寝室之间。尤其是20天前开题的那周,更是劳累不堪。室友连续三天奋战到半夜,分别在3点半、5点、4点才睡。即使像我这样一个稍显懒散的人,心思也几乎整日都是在课题上,每晚都到了夜里两点多才休息。

之前也和九龙湖那边的室友交流,想和他们小聚一下,不过他们也说任务很多,再加上九龙湖校区距离四牌楼有20公里,所以这事也就搁置下来了。

最近不管怎么样,一定要出门一次,放松一下心情,不然身体肯定会受不了的。

二、

提到南京的景点,浮现在脑海中的必然是「中山陵」、「明孝陵」、「总统府」和「夫子庙」等等。既然它们有名,自然都是去逛过很多次了,反倒没有什么新鲜可言。甚至本科如果在南京就读,还可能招待外地前来游玩的同学去了很多回,那里的布局估计对他们来说肯定都是如数家珍了。所以目标自然是自己没去过的地儿。

我想了好久,冬季确实没有什么好去处。起先定下的景点是莫愁湖,网上有建议说可以去一赏冬季开放的一些花卉。可「山茶花」、「仙人指」、「茶梅」等等美艳的植株盛开之时几乎都是在晚冬,所以便放弃了这个想法。最后定下了南京历史最为久远的园林——瞻园——这个与苏州「拙政园」、「留园」以及无锡「寄畅园」并称「江南四大名园」的「金陵第一园」。


三、

来瞻园一定要细赏其山。瞻园中四处都有假山,其中以南、北两座假山群为主。北假山面积稍大,周围亭台楼榭俱全:西南方是「岁寒亭」,亭子附近种植松、竹、梅三种植物,遂以此名相称;东面是「观鱼亭」,驻足其间,可览假山前池中嬉戏的鱼群;东南不远便到了「一览阁」,顾名思义,这座楼是全园最高建筑且居正中,登上此阁可观瞻园全貌。南假山面积虽小,但令人回味依旧。相比与北假山,此座假山植被遍布、群峰跌宕,假山山洞幽深、钟乳倒悬,虽然没有北假山小亭、阁楼的相映成趣,但是池水、奇石与花木相衬,依然可称为美景。瞻园之景胜在假山。

来瞻园一定要细观其水。瞻园分为东、西两个部分。东瞻园主要是一些古代宅院,现在多处为相关的历史陈列展馆;西瞻园则是主要景区,假山等美景皆在西半园子。而在西瞻园中,几乎随处可见池水、清溪。假山被池水包围、亭榭筑在水面之上供游人歇息,曲折的长廊也是建立在池子之旁;不同景物辅以水域搭配,相辅相成、互为益彰。可见园子的设计者在水的搭配上必然是花了不少功夫。

来瞻园一定要品味其建筑。亭,是一种开敞型的小型建筑,供人停步休息;台,是高平的建筑,比如月台、窗台;楼,是两层及以上的房屋,在园林中常用作卧室书房;榭,筑在平台上的木屋。「百步一亭,五十步一榭」,在这「金陵第一园」中是真的亲眼目睹了。除了之前说的那些亭子、楼阁、长廊之外,其他的「轩」、「堂」以及「楼」还有很多很多。建筑密度虽大,但是伴随着假山、池水的相衬,尽不使人觉得乏味。其中有座楼名曰「逐月楼」,供赏月之用。当年乾隆南巡时驻跸此园,在此楼赏月之时想起欧阳修名句「瞻望玉堂,如在天上」,遂赐「瞻园」匾额,这也便是园子名称的由来了。

来瞻园一定要体会其历史。瞻园最早是明太祖朱元璋称帝前的吴王府,之后赐予开国功臣徐达做为府邸一部分。乾隆年间,皇帝南巡之时题「瞻园」之名,并且对此园念念不忘,回京后仿瞻园修建北京长春园。太平天国时期成为东王杨秀清的王府花园。甚至因为此园布局巧妙且保留了很多历史韵味,87版《红楼梦》以及92年赵雅芝版的《新白娘子传奇》皆在此取景。园内依托其历史修筑很多历史展馆:太平天国历史博物馆,明中山王徐达史料展等等,浏览一番,便可知晓园子的前世今生。

亭台楼阁、舞榭歌台,青山绿水、花红柳绿。错落有致的格局,典雅精致的设计。游走于其间,真的会感觉如入幻境一般。我想,倘若当年五柳先生居于此园,就写不出《桃花源记》和《归园田居》等等流传千古的词句了吧。

四、

漫步于瞻园,整个人都沉浸在其山水建筑的巧趣之中,使得我差点迷失了方向。实不相瞒,这座园子并不算大,长宽只有100来米,不过布局巧妙的山石让人赞赏从各个角度看皆为新奇,点缀其中众多的小亭让人总想驻足其中一赏究竟,且有曲折回环的长廊遍布连缀各处,为了不错过一处美景,让我这个路痴几次找寻错了方向。

赏园时遇到一位外国友人,他倒是步履匆匆,一时间便游览了多处景点。可能是确实园子不大再加上有长廊四处相连,我几次和他打了照面:在「太平天国历史史料成列馆」、在「明中山王徐达史料展馆」、在「碑廊」与「盆景园」,均见他走马观花。可能这样一座狭小的园子,只有西半边的假山和观赏亭才能让他稍稍停留打量一番吧。如果真的这样,乐趣自然少了太多。毕竟中国传统园林文化中的「天人合一」的境界会让外人看来少了很多乐趣。

不过每个人的兴趣不同,我钟爱的美景他人看来只是枯燥无奇,这倒也是随处可见。

畅游于此情此景的我,如同挣脱了牢笼之后的肖生克,近乎于漫无目的地穿梭在不同的庭前院落之间,久久不忍离去。

五、

回到寝室之后,看着自己相机中记录下的一些造访于神境之间的片段,依然回不过神来。细想一番,自己已经好久没有独自远离某地,完全融入一个不用刻意融入某种氛围的环境之中了。

看着看着,想起了刚搬到四牌楼这边时一个室友说的一句话。

「你没有发现,好友之间呆在一块,即使没有任何交流依然不会显得尴尬么?」

当时我刚从九龙湖搬到这边,室友几乎终日不语,我有时会闲聊几句以打破沉默,熟悉之后聊到了这些。

「我到没有在意过这些,但是我个人不管和谁在一起,不言不语总会感到有些别扭」,当时的我回答说。

现在他受到导师的要求,在工地干活。他和我说过,他就认为人应该干一行爱一行。他也是如此行事的。他对很多事情几乎没有半点怨言,这点我也格外敬佩。

比起沉稳干练的他,我倒是显得墨迹许多了。

或许,我早该意识到,人会随着年岁的增长,在自己的喜好上越走越远,很多美景只能够自己去探寻吧。

浅谈动态规划(三)——找到最优时的解

By | 2016年12月4日

前两次动态规划的博客中,介绍了动态规划最基础的两个特点以及列举了几个我在做lintcode中遇到的接触动态规划初期相对比较适合做的题目。这次结合几道题目来总结一下动态规划的另一个特点——找到最优时的解。

同样,还是以题目来说明。

1.Minimum Adjustment Cost

题目描述:

Given an integer array, adjust each integers so that the difference of every adjacent integers are not greater than a given number target.

If the array before adjustment is A, the array after adjustment is B, you should minimize the sum of |A[i]-B[i]|.

Example: Given [1,4,2,3] and target = 1, one of the solutions is [2,3,2,3], the adjustment cost is 2 and it’s minimal.

题目给出了一个数组A,并且给出了一个目标数字target。要求调整数组中每个位置的数字得到数组B,使得数组B中相邻元素的差值的绝对值小于等于target,且$$sum(|A[i] – B[i]|)$$最小。求得到最终结果时每个位置调整的和最小为多少。

题目只要求找出最小的调整开销的和,我这里再加一问:在调整和为最小值时,找出一个可能的数组B。

先来解决lintcode要求的问题。

Minimum Adjustment Cost题解:

我们应该能够想通一个问题,调整后的每个位置的数字都是在A数组中出现的最小值与最大值之间的数字。假设最小值是minVal,最大值是maxVal,所以数组B的每个位置上可选择的数字的个数为$$count = maxVal – minVal + 1$$。

构造一个二维数组$$cost[n+1][count]$$,n表示数组A中的个数。$$cost[loop][j]$$$$(0 <= loop <= n, minVal <= j <= maxVal)$$表示B数组中第loop个位置上(此时B的下标为loop-1)调整为数字j时的调整总和(包括loop之前的所有位置的调整的和)。那么当调整下一个位置loop+1时,调整为数字i的调整总和为$$cost[loop+1][i] = min(cost[loop][j] + | i – A[loop] |)$$,$$|i – j| <= target$$。这里j是上一个位置时调整的结果,因为有限制条件相邻的两个元素差的绝对值小于target,所以要用到上一个位置的数组;$$|i – A[i]|$$是此位置(数组B中第loop+1个数字,下标为loop)调整为i时调整的代价。最后的结果是$$cost[n][i]$$$$(minVal <= i <= maxVal)$$中最小的值。

可能我的表述不太清楚,加注释的代码可能表达的更清楚些:

int MinAdjustmentCost(vector<int> A, int target)
{
	int n = A.size();

	if (n <= 0 || n == 1)
		return 0;

	int minVal = INT_MAX; // 记录数组中最小值
	int maxVal = INT_MIN; // 记录数组中最大值
			
	int loop, i, j;
	
	// 找出数组A中出现的最小值和最大值
	for (i = 0; i < n; i++) { if (A[i] > maxVal)
			maxVal = A[i];
		if (A[i] < minVal)
			minVal = A[i];
	}
	
	int count = maxVal - minVal + 1; // 每次需要动态规划的个数
	// 辅助数组。记录每个位置上B调整为不同数字的最小开销。
	// 即,当B数组第loop个数字调整为i的时候的最小开销为cost[loop][i]
	vector<vector<int> > cost(n+1, vector<int>(count, INT_MAX)); 
	// 初始化
	for (i = 0; i < count; i++)
		cost[0][i] = 0; 

	// 动态规划步骤
	for (loop = 1; loop <= n; loop++)
	{
		// 找出数组B中,第loop个位置(下标为loop-1),选择数字i的最小调整代价
		// 即,找出cost[loop][i]。它记录了到loop位置时,调整为i数字的最小开销
		for (i = minVal; i <= maxVal; i++)
		{
			for (j = minVal; j <= maxVal; j++)
			{
				if (abs(i-j) <= target)
				{
					int temp = cost[loop-1][j-minVal] + abs(i-A[loop-1]); // 当第loop-1个数字选择j时,第loop个数字选择(调整到)i时的改变量
					if (temp < cost[loop][i-minVal]) // 找出最小的调整代价
						cost[loop][i-minVal] = temp;
				}
			}
		}
	}

	// 找出最后一个位置改变结束时最小的改变代价
	int minCost = INT_MAX;
	for (i = 0; i < count; i++)
		minCost = min(cost[n][i], minCost);

	return minCost;
}

我们发现,在每次比较找出最小开销的时候,可以在加一个辅助数组$$record[n+1][count]$$,记录每个位置时最小调整开销时选择的数字,最后得到了完整的cost列表之后,$$cost[n][i]$$$$(minVal <= i <= maxVal)$$中最小时i的值就是B[n-1]中的值,然后$$record[n][i]$$中记录的即使上一个位置对应的B中的值,以此类推,便能找到B数组一个可能的结果。同样,看代码可能更清楚点:

// 实现了记录每次选择数字的功能
void MinAdjustmentCost(vector<int> A, int target)
{
	int n = A.size();

	if (n <= 0 || n == 1)
		return;

	int minVal = INT_MAX; // 记录数组中最小值
	int maxVal = INT_MIN; // 记录数组中最大值
			
	int loop, i, j;
			
	for (i = 0; i < n; i++) { if (A[i] > maxVal)
			maxVal = A[i];
		if (A[i] < minVal)
			minVal = A[i];
	}
	
	int count = maxVal - minVal + 1; // 每次需要动态规划的个数
	vector<vector<int> > cost(n+1, vector<int>(count, INT_MAX)); // 辅助数组
	vector<vector<int> > record(n+1, vector<int>(count, 0));
	vector<int> B(n);
	// 初始化
	for (i = 0; i < count; i++)
		cost[0][i] = 0; 

	// 动态规划步骤
	for (loop = 1; loop <= n; loop++)
	{
		// 找出第loop个位置,选择数字i的最小调整代价
		for (i = minVal; i <= maxVal; i++)
		{
			for (j = minVal; j <= maxVal; j++)
			{
				if (abs(i-j) <= target)
				{
					int temp = cost[loop-1][j-minVal] + abs(i-A[loop-1]); // 当第loop-1个数字选择j时,第loop个数字选择(调整到)i时的改变量
					if (temp < cost[loop][i-minVal]) // 找出最小的调整代价
					{
						cost[loop][i-minVal] = temp;
						record[loop][i-minVal] = j; // record中记录的是第loop个数字取i最优时,上一个数字最优时对应的值
					}
				}
			}
		}
	}

	// 找出最后一个位置改变结束时最小的改变代价
	int minCost = INT_MAX;
	int backtrack; // 用于回溯之前的一个位置应该调整为哪个数值
	for (i = minVal; i <= maxVal; i++)
	{
		if (cost[n][i-minVal] < minCost)
		{
			minCost = cost[n][i-minVal];
			backtrack = i; // 找到最后一个调整好的数值对应的前一个数值
		}
	}

	cout << minCost << endl; // 输出最小的调整代价 int loc = n - 1; // 对应数组B每个下标 int sum = 0; // 用于核对回溯的每个位置的值是否正确,minCost == sum时正确 while (n > 0)
	{
		B[loc--] = backtrack;
		sum += abs(A[n-1] - backtrack);
		backtrack = record[n--][backtrack-minVal]; // 因为每次找到的调整好的数值,所以索引要调整
	}
	cout << sum << endl; // 比较sum是否等于minCost

	// 输出调整之后的数组B
	for (i = 0; i < A.size(); i++)
		cout << B[i] << ' ';
	cout << endl;
}

2.钢条切割问题

接下来我们再来看一下《算法导论》上给的一道经典题目,钢条切割问题。

题目描述:

一家公司有一根固定长度的钢条,要切割为短的钢条出售。切割工序本身没有成本。公司希望知道最佳的切割方案(即如何切割能够获得价格的最大值)。假设钢条的长度为n(此处假设n <= 10),不同长度i的钢条的价格如下表。

长度j

1

2

3

4

5

6

7

8

9

10

价格P

1

5

8

9

10

17

17

20

24

30

应用动态规划的思想,我们可以这样考虑问题:假设长度为i时,最佳切割方案时的价格总和为$$r[i]$$$$(0 <= i<= n)$$,那么当长度为i+1时,最佳的方案是$$r[i+1] = max(p[j] + r[i+1-j]), 1<= j <= i+1$$。所以状态转移方程即是:
$$!r[i+1] = max(p[j] + r[i+1-j]),1<= j<= i+1$$
同样,看代码可能思路更清晰:

// 自底向上实现
// 数组p记录不同长度的价格,n是所求最大的长度
int cutRodBottomUp(vector<int> const &p, int n)
{
	vector<int> r(n+1, INT_MIN); // 辅助数组,记录每个长度不同切割方法的最大值。以整型最小值初始化
	r[0] = 0; 

	// 动态规划每个长度不同切割方法的最大值
	for (int i = 1; i <= n; i++)
	{
		int q = INT_MIN;
		for (int j = 1; j <= i; j++)
		{
			q = max(q, p[j-1] + r[i-j]);  // 找到每个长度切割的最大值。上述解释中的j是长度,j-1对应其下标
		}
		r[i] = q;
	}

	return r[n];
}

这时函数返回的是最大的长度,再加一个辅助数组,在每次找出最大价值的时候记录切割位置,就可以找出切割方案了。代码如下:

// 带有输出分割情况功能的版本
void cutRodExtended(vector<int> const &p, int n)
{
	vector<int> r(n+1, INT_MIN); // 辅助数组,记录每个长度不同切割方法的最大值
	r[0] = 0;
	vector<int> s(n+1, 0); // 记录每个长度最大收益时的切割位置
	
	int i, j;

	// 动态规划步骤
	for (i = 1; i <= n; i++)
	{
		int q = INT_MIN;
		for (j = 1; j <= i; j++)
		{
			if (q < p[j-1] + r[i-j])
			{
				q = p[j-1] + r[i-j];
				s[i] = j; // 记录每个长度最大收益时的切割位置
			}
			r[i] = q;
		}
	}

	cout << r[n] << endl; // 输出结果 while (n > 0)
	{
		cout << s[n] << '\t';
		n = n - s[n];
	}
	cout << endl;
}

这样,就找到最佳切割方案了。

3.最长公共子序列

最后,再来看另一道《算法导论》里面的经典问题——最长公共子序列,这到题目也非常适合用辅助数组来记录最优解。

题目描述:

给定两个序列X = {$$x_1$$, $$x_2$$, $$x_3$$, …, $$x_m$$ }, 和Y = {$$y_1$$, $$y_2$$, …, $$y_n$$},求X和Y长度最长的公共子序列。公共子序列的定义为,给定两个序列X,Y,如果Z即是X的子序列,也是Y的子序列,我们称它是X和Y的公共子序列。

例如:X = {A, B, C, D, A, B},Y = {B, D, C, B, A},那么序列{B, C, A}就是X和Y的公共子序列,但不是最长公共子序列。X和Y的最长公共子序列是{B, C, A, B},其长度为4。

解题思路:

我们假设Z = {$$z_1$$, …, $$z_k$$}是X和Y的一个最长公共子序列。1)如果$$x_m$$ == $$y_n$$,那么有$$z_k$$ == $$x_m$$ == $$y_n$$,所以$$Z\_{k-1}$$是$$X\_{m-1}$$和$$Y\_{n-1}$$的一个最长公共子序列;2)如果$$x_m$$ != $$y_n$$且$$x_m$$ != $$z_k$$,那么Z是$$X_{m-1}$$和Y的一个最长公共子序列;3)如果$$x_m$$ != $$y_n$$且$$z_k != y_n$$,那么Z是X和$$Y_{n-1}$$的一个最长公共子序列。

我们令$$c[i][j]$$表示$$X_i$$和$$Y_j$$的最长公共子序列的长度,那么可以得到以下关系:

情况1: $$i==0$$ 或者$$j== 0$$时,$$c[i][j] = 0$$;
情况2: $$i, j > 0$$且$$x_i == y_j$$时, $$c[i][j] = c[i-1][j-1] + 1$$;
情况3: $$i,j > 0$$且$$x_i != y_j$$时, $$c[i][j] = max(c[i][j-1], c[i-1][j])$$;

这也就是状态转移方程了。

同样,给出带有注释的代码可能更好理解,这里第一个版本返回的是最长公共子序列的长度。

// 自底向上动态规划
// 自底向上动态规划
int LCS(vector<char> &X, vector<char> &Y)
{
	int len1 = X.size();
	int len2 = Y.size();
	int i, j;

	vector<vector<int> > c(len1+1, vector<int>(len2+1)); // 辅助二维数组,记录$X_i$, $Y_j$的最长公共子序列
	
	// 初始化
	for (i = 0; i <= len1; i++)
		c[i][0] = 0;
	for (j = 0; j <= len2; j++)
		c[0][j] = 0;

	// 动态规划过程
	for (i = 1; i <= len1; i++)
	{
		for (j = 1; j <= len2; j++) { if (X[i-1] == Y[j-1]) // 对应状态转移方程中情况2 c[i][j] = c[i-1][j-1] + 1; else if (c[i-1][j] > c[i][j-1]) // 对应状态转移方程中情况3
				c[i][j] = c[i-1][j];
			else // 对应情况3
				c[i][j] = c[i][j-1];
		}
	}

	return c[len1][len2];
}

在情况2和情况3的判断中,可以加入判断结果,将结果记录到一个辅助数组,这样就能回溯结果了,代码如下:

// 自底向上动态规划。path二维数组记录对应X_i, Y_j的最长公共子序列的选择情况。
// 自底向上动态规划。path二维数组记录对应X_i, Y_j的最长公共子序列的选择情况。
int LCS2(vector<char> &X, vector<char> &Y,vector<vector<string> > &path)
{
	int len1 = X.size();
	int len2 = Y.size();
	int i, j;

	vector<vector<int> > c(len1+1, vector<int>(len2+1)); // 记录对应位置的结果
	
	// 初始化
	path.resize(len1+1);
	for (i = 0; i <= len1; i++)
		path[i].resize(len2+1);

	for (i = 0; i <= len1; i++)
		c[i][0] = 0;
	for (j = 0; j <= len2; j++)
		c[0][j] = 0;

	// 动态规划过程
	for (i = 1; i <= len1; i++)
	{
		for (j = 1; j <= len2; j++) { if (X[i-1] == Y[j-1]) { c[i][j] = c[i-1][j-1] + 1; path[i][j] = "left-top"; // 上一个位置在左上位置 } else if (c[i-1][j] > c[i][j-1])
			{
				c[i][j] = c[i-1][j];
				path[i][j] = "up"; // 上一个位置在上方
			}
			else
			{
				c[i][j] = c[i][j-1];
				path[i][j] = "left"; // 上一个位置在左边
			}
		}
	}

	return c[len1][len2];
}

这里配合下图更好理解。

这里的根据路径输出的函数为:

void Print_LCS(vector<char> &X, vector<vector<string> > &path, int i, int j)
{
	if (i == 0 || j == 0)
		return;

	if (path[i][j] == "left-top") // 左上位置
	{
		Print_LCS_test(X, path, i-1, j-1);
		cout << X[i-1];
	}
	else if (path[i][j] == "up") // 上方
		Print_LCS_test(X, path, i-1, j);
	else if (path[i][j] == "left") // 左边
		Print_LCS_test(X, path, i, j-1);
}

参考文献:
1.lintcode Minimum Adjustment Cost
2.《算法导论(第三版)》,15章,动态规划内容。