二叉树先序、中序、后序遍历的非递归实现

十一月 23rd, 2010 2 Comments »

在网上看了一些用非递归实现先序中序后序遍历二叉树的代码,都很混乱,while、if各种组合嵌套使用,逻辑十分不清晰,把我也搞懵了。想了大半天,写了大半天,突然开了窍,实际上二叉树的这三种遍历在逻辑上是十分清晰的,所以才可以用递归实现的那么简洁。既然逻辑如此清晰,那么用非递归实现也应该是清晰的。

自认为自己的代码比网上搜到的那些都清晰得多,好理解得多。

稍微解释一下:

先序遍历。将根节点入栈,考察当前节点(即栈顶节点),先访问当前节点,然后将其出栈(已经访问过,不再需要保留),然后先将其右孩子入栈,再将其左孩子入栈(这个顺序是为了让左孩子位于右孩子上面,以便左孩子的访问先于右孩子;当然如果某个孩子为空,就不用入栈了)。如果栈非空就重复上述过程直到栈空为止,结束算法。

中序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其左孩子未被访问过(有标记),则将其左孩子入栈,否则访问当前节点并将其出栈,再将右孩子入栈。如果栈非空就重复上述过程直到栈空为止,结束算法。

后序遍历。将根节点入栈,考察当前节点(即栈顶节点),如果其左孩子未被访问过,则将其左孩子入栈,否则如果其右孩子未被访问过,则将其右孩子入栈,如果都已经访问过,则访问其自身,并将其出栈。如果栈非空就重复上述过程直到栈空为止,结束算法。

其实,这只不过是保证了先序中序后序三种遍历的定义。对于先序,保证任意一个节点先于其左右孩子被访问,还要保证其左孩子先于右孩子被访问。对于中序,保证任意一个节点,其左孩子先于它被访问,右孩子晚于它被访问。对于后序,保证任意一个节点的左孩子右孩子都先于它被访问,其中左孩子先于右孩子被访问。如是而已。

代码里应该体现得比较清楚。这里不光给出了非递归版本,也给出了递归版本。
Continue reading »

威佐夫博弈、黄金分割、POJ 1067

四月 3rd, 2010 2 Comments »
取石子游戏
Time Limit: 1000MS Memory Limit: 10000K
Total Submissions: 17630 Accepted: 5338

Description

有两堆石子,数量任意,可以不同。游戏开始由两个人轮流取石子。游戏规定,每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者。现在给出初始的两堆石子的数目,如果轮到你先取,假设双方都采取最好的策略,问最后你是胜者还是败者。

Input

输入包含若干行,表示若干种石子的初始情况,其中每一行包含两个非负整数a和b,表示两堆石子的数目,a和b都不大于1,000,000,000。

Output

输出对应也有若干行,每行包含一个数字1或0,如果最后你是胜者,则为1,反之,则为0。

Sample Input

2 1
8 4
4 7

Sample Output

0
1
0

Source

这个游戏就是所谓的威佐夫博弈(Wythoff Game),很有意思。

昨天就在做这道题目,思考成果是:用一个二维数组来表示玩家将面临的局面,即a[i][j]表示两堆石头分别有i个和j个,如果a[i][j]是必败局面的话,那么——这个点的右侧所有点a[i][k](k>j)都是必胜点,因为可以通过拿走第二堆的k-j个石子令对方面临必败局面a[i][j];这个点的下侧所有点a[k][j](k>i)都是必胜点,因为可以通过拿走第二堆的k-i个石子令对方面临必败局面a[i][j];这个点的右下方45度所有点a[i+k][j+k](k>0)都是必胜点,因为可以通过同时拿走两堆的k个石子令对方面临必败局面a[i][j]。这样如果在二维数组a[i][j]处标记”X”表示必败的话,在上面提到的三类点的位置都可以标记”O”表示必胜了,做完这项工作后,再挑选如今距离原点最近的未被标记的点,它一定是下一个必败点——因为它无法通过游戏规则移动到一个必败点,并且规则规定的动作都是朝向原点移动的,而它是距离原点最近的未被标记的点,因此它只能移动到一个必胜点从而让对方获胜,所以该点一定是下一个必败点。有了新必败点后就可以重复上述工作,直到找出问题范围内的所有必败点。

然而这样的模拟工作是不被允许的,因为数据量太大了,因此这道题要求的是给定两个数i,j,有办法立刻判断a[i][j]是不是必败点,在O(1)时间给出解答。

我试图找到必败点的规律,但是没成功,以上就是昨天的思考结果。因为这个问题够简洁,所以我坚信一定有简单规律的,因此在考察了很大数据量仍然没有发现规律后,我无比好奇这道题的解法。今天上网一搜才知道规律果然够简单够经典,竟然是“黄金分割”,以下内容来自对网上内容的总结:

前几个必败点如下:(0,0),(1,2),(3,5),(4,7),(6,10),(8,13)……可以发现,对于第k个必败点(m(k),n(k))来说,m(k)是前面没有出现过的最小自然数,n(k)=m(k)+k。一个必败点有如下性质:

1.所有自然数都会且仅会出现在一个必败点中;

证明:m(k)是前面没有出现过的最小自然数,自然与前k-1个必败点中的数字都不同;m(k)>m(k-1),否则违背m(k-1)的选择原则;n(k)=m(k)+k>m(k-1)+(k-1)=n(k-1)>m(k-1),因此n(k)比以往出现的任何数都大,即也没有出现过。又由于m(k)的选择原则,所有自然数都会出现在某个必败点中。性质1证毕。

2.规则允许的任意操作可将必败点移动到必胜点;

证明:以必败点(m(k),n(k))为例。若只改变两个数中的一个,由于性质1,则得到的点一定是必胜点;若同时增加两个数,由于不能改变两数之差,又有n(k)-m(k)=k,故得到的点也一定是必胜点。性质2证毕。

3.一定存在规则允许的某种操作可将必胜点移动到必败点;

证明:以某个必胜点(i,j)为例。因为所有自然数都会出现在某个必败点中,故要么i等于m(k),要么j等于n(k)。若i=m(k),j>n(k),可从j中取走j-n(k)个石子到达必败点;若i=m(k),j<n(k),可从两堆同时拿走m(k)-m(j-m(k)),从而到达必败点(m(j-m(k)),m(j-m(k))+j-m(k));若i>m(k),j=n(k),可从i中取走i-m(k)个石子到达必败点;若i<m(k),j=n(k),需要再分两种情况,因为i一定也出现在某个必败点中,若i=m(l),则从j中拿走j-n(l),若i=n(l),则从j中拿走j-m(l),从而到达必败点(m(l),n(l))。性质3证毕。

判断一个点是不是必败点的公式与黄金分割有关,为:

m(k) = k * (1 + sqrt(5))/2

n(k) = m(k) + k

至于为什么如此,我就不知道了,也没有查到,很好奇。

POJ1067就是这道题,代码如下:

#include 
#include 
 
using namespace std;
 
int main()
{
	int m,n;
	while(cin>>m>>n)
	{
		if (m > n)
		{
			int temp;
			temp = m;
			m = n;
			n =temp;
		}
		int k = n - m;
		int data = floor(k*(1.0+sqrt(5.0))/2.0);
		if (data == m)
			cout<<0<<endl;
        }
}
需要注意的是,由于数据量过大,float的精度不够,而只能用double,因此代码中的小数没有加“f”。

动态规划 POJ 1661

四月 2nd, 2010 No Comments »
Help Jimmy
Time Limit: 1000MS   Memory Limit: 10000K
Total Submissions: 5193   Accepted: 1582

Description

“Help Jimmy” 是在下图所示的场景上完成的游戏。

场景中包括多个长度和高度各不相同的平台。地面是最低的平台,高度为零,长度无限。

Jimmy老鼠在时刻0从高于所有平台的某处开始下落,它的下落速度始终为1米/秒。当Jimmy落到某个平台上时,游戏者选择让它向左还是向右跑,它跑动的速度也是1米/秒。当Jimmy跑到平台的边缘时,开始继续下落。Jimmy每次下落的高度不能超过MAX米,不然就会摔死,游戏也会结束。

设计一个程序,计算Jimmy到底地面时可能的最早时间。

Input

第一行是测试数据的组数t(0 <= t <= 20)。每组测试数据的第一行是四个整数N,X,Y,MAX,用空格分隔。N是平台的数目(不包括地面),X和Y是Jimmy开始下落的位置的横竖坐标,MAX是一次下落的最大高度。接下来的N行每行描述一个平台,包括三个整数,X1[i],X2[i]和H[i]。H[i]表示平台的高度,X1[i]和X2[i]表示平台左右端点的横坐标。1 <= N <= 1000,-20000 <= X, X1[i], X2[i] <= 20000,0 < H[i] < Y <= 20000(i = 1..N)。所有坐标的单位都是米。

Jimmy的大小和平台的厚度均忽略不计。如果Jimmy恰好落在某个平台的边缘,被视为落在平台上。所有的平台均不重叠或相连。测试数据保证问题一定有解。

Output

对输入的每组测试数据,输出一个整数,Jimmy到底地面时可能的最早时间。

Sample Input

1
3 8 17 20
0 10 8
0 10 13
4 14 3

Sample Output

23

Source

今天做了几道DP的题,觉得有点心得,这道题又有些可写之处,就来一篇吧。
此题为典型的动态规划,一开始我思考的复杂了,或者说思考的不够深入导致没有简化彻底,就着手写代码了。
所谓动态规划,就是递归(或者递推)+存储。动态规划能解决的问题具有最有子结构,因此很自然就可以得到递归解,但是由于重复计算的原因,递归的效率很低,如果将计算过的结果存储起来以保证每个子问题只计算一次,就得到了动态规划的基本思想。如果合理安排各个子问题的计算顺序,就能保证计算每一个子问题的时候,求解它所依赖的其它子问题都已经求解完毕。
动态规划的一般步骤是:a.划分子问题;b.找问题状态(能代表子问题的变量,可能是一维,也可能是一维以上);c.找状态转移关系(动态规划的灵魂就是状态转移方程);d.按合理顺序求解各子问题直至原问题得解。
这道题中,如果将所有平板按照高度进行排序(地板为0,最高为n),则可以用板子的编号i作为状态变量,每个状态代表两个子问题——从该板左端点到达地面所需最短时间,和从该板右端点到达地面所需最短时间,如果求出了这两个子问题,那么当Jimmy从上一个板子到达该板某处(!)时,再到达地面的最短时间就很容易求了,或者说Jimmy从上一个板子的左侧或者右侧(!)开始到达地面的最短时间就很容易求了。于是,我们令子问题为f[i].fromLeft和f[i].fromRight,分别代表从i号板左侧和右侧到达地面所需最短时间。
有了子问题划分和状态描述,状态转移方程就很容易看出来了:
if (板子i的左侧下方Max以内的距离有其它板子j)
	f[i].fromLeft=i高度-j高度+max(i的X走到j左侧的时间+f[j].fromLeft,i的X走到j右侧的时间+f[j].fromRight);
else
	f[i].fromLeft=正无穷;

这样本题的思路就很清晰了。
刚做此题的时候,没有思考透彻,以为从一个板子跳时,有很多个选择,而忽略了板子之间的互相遮挡,结果考虑得很复杂,其实从任何一个板子的某个端点起跳,最多只能落到一个板子上,因此,在每一个板子上只有两个选择——向左跳还是向右跳,而不存在向左跳到哪个板和向右跳到哪个板的问题。

代码如下:

#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
 
using namespace std;
 
#define INF 9999999
 
struct _platform 
{
	int x1;
	int x2;
	int height;
};
 
bool UDgreater(_platform p1,_platform p2)
{
	return p1.height < p2.height;
}
 
struct _time
{
	int fromLeft;
	int fromRight;
};
 
int N,X,Y,MAX;
vector<_platform> pvec;
_time fastest[1005];
 
void prepare()
{
	memset(fastest,INF,sizeof(fastest));
	pvec.clear();
	cin>>N>>X>>Y>>MAX;
 
	_platform platform;
	platform.x1 = -INF;
	platform.x2 = INF;
	platform.height = 0;
	pvec.push_back(platform);
 
	for (int i = 1;i <= N;i++)
	{
		cin>>platform.x1>>platform.x2>>platform.height;
		if (platform.height <= Y)
		{
			pvec.push_back(platform);
		}
	}
	platform.x1 = X;
	platform.x2 = X;
	platform.height = Y;
	pvec.push_back(platform);
	sort(pvec.begin(),pvec.end(),UDgreater);
}
 
bool CanLoad(int i,int j,int dir)
{	//whether Jimmy can load on j from i safely;0 for left,1 for right
	if (dir == 0)
	{
		if (pvec[i].height-pvec[j].height <= MAX&&\
			pvec[i].x1>=pvec[j].x1&&pvec[i].x1<=pvec[j].x2)
			return true;
	}
	else
	{
		if (pvec[i].height-pvec[j].height <= MAX&&\
			pvec[i].x2>=pvec[j].x1&&pvec[i].x2<=pvec[j].x2)
			return true;		
	}
	return false;
}
 
int main()
{
	int n;
	cin>>n;
	while (n--)
	{
		prepare();
		fastest[0].fromLeft = 0;
		fastest[0].fromRight = 0;
		int size = pvec.size();
		for (int i = 1;i < size;i++)
		{
			int leftMin = INF,rightMin = INF;
			int j = i-1;
			while(j>=0&&!CanLoad(i,j,0))
				j--;
			if (j > 0)
				fastest[i].fromLeft = min(fastest[j].fromLeft+\
				abs(pvec[i].x1-pvec[j].x1),fastest[j].fromRight+\
				abs(pvec[i].x1-pvec[j].x2))+abs(pvec[i].height-pvec[j].height);
			else if(j == 0)
				fastest[i].fromLeft = abs(pvec[i].height-pvec[j].height);
			j = i-1;
			while (j>=0&&!CanLoad(i,j,1))
				j--;
			if (j > 0)
				fastest[i].fromRight = min(fastest[j].fromLeft+\
				abs(pvec[i].x2-pvec[j].x1),fastest[j].fromRight+\
				abs(pvec[i].x2-pvec[j].x2))+abs(pvec[i].height-pvec[j].height);				
			else if(j == 0)
				fastest[i].fromRight = abs(pvec[i].height-pvec[j].height);
		}
		cout<<min(fastest[size-1].fromLeft,fastest[size-1].fromRight)<<endl;
	}
}

所谓哈希 POJ 3349

十二月 15th, 2009 No Comments »

数据结构者,“数据间关系+存储方式”也。

不同的数据结构在不同操作方面占有优势,这也是它们存在的价值。哈希表也如此,在某个操作方面提供其它数据结构所不能匹敌的优越性,这个操作就是“按值存取”。

其它普通数据结构如线性表、树、图,节点数据的值与存储位置之间的关系是随机的,要想实现按值存取,只能进行基于“比较”的查找,或多或少带有一定的盲目性(对于二分查找这样的方式,每次比较结果还是有一定的指导意义的)。而哈希表提供直接的按值存取,你询问一个值的数据在哪里,它能直接给出答案,是“定位”而不是“寻找”。

之所以能“按值取”,是因为当初的“按值存”。哈希表就是要在数据值和数据存储位置之间建立一个映射,即所谓的“哈希函数”。有此哈希函数作指导,则可实现“按值存、取”,因为在哈希函数的指导下,值本身就含着存储位置的信息,有点“加密解密”的意思。

由于数据域往往宽于地址域,所以通常哈希函数不能为简单的线性关系,这样就有可能造成不同值映射到同一个存储位置――所谓“冲突”,所以哈希还应该提供解决冲突的方式,比较好的一种方式是“链表法”,将冲突的数据都链在一个节点后面,而STL里的vector数组简直就像是为这种用链表解决冲突的哈希表量身定做的一样,使用起来十分方便。

有这样一类问题:在海量数据中查找是否有出现多于一次的数据?笨方法只能从头至尾逐一比对,复杂性为O(N*N)。聪明一点的方法,如果数据不是很分散,可以用做记号的方法,用一个位数组(可用bool实现)记录各个位所代表的数据是否出现过,如果读入一个已经出现过的数据则说明它出现多于一次,用int代替bool,还可以记录下每个数据出现的次数,在遍历一次便可得到出现最多次的数据,复杂度为O(N)。但是如果数据很分散,这种线性映射就不管用了,因为内存会严重浪费,如果数据过于夸张,会造成很大BUFFER的申请,但是这种映射记录的思想还是可取的,这时就需要一个从很大的数据域向相对很小的地址域映射的工具来辅助,自然就是“哈希”。

POJ3349就是利用哈希这个特点的一个典型应用。题目本身与存取无关,而只是要找出是否有数据出现过一次以上。就可以采取上述“标记”+“映射”的方法。
代码如下:

#include <iostream>
#include <vector>
#include <cstdio>
#define BIG_PRIMER 87719
 
using namespace std;
 
struct snowflake
{
	bool flag;	//0 for empty;
	unsigned long arms[6];
	snowflake()
	{
		flag = false;
	}
};
 
bool cmpTwoSnowflakes(const snowflake& s1,const snowflake& s2)
{
	int start[6] = {0},count = 0;
	for(int i  = 0;i < 6;i++)
	{
		if(s1.arms[0]==s2.arms[i])
		{
			start[count++] = i;
		}
	}
	if(count==0)
		return false;
	bool IsSame = true;
	for(int j = 0;j < count;j++)
	{
		for(int i = 1;i < 6;i++)
		{
			if(s1.arms[i]!=s2.arms[(start[j]+i)%6])
			{
				IsSame = false;
				break;
			}
		}
		if(IsSame == true)
			return true;
		IsSame = true;
		for(int i = 1;i < 6;i++)
		{
			if(s1.arms[i]!=s2.arms[(start[j]-i+6)%6])
			{
				IsSame = false;
				break;
			}
		}
		if(IsSame == true)
			return true;
		IsSame = true;
	}
	return false;
}
 
unsigned long Hash(const snowflake& sf)
{
	return (sf.arms[0]+sf.arms[1]+sf.arms[2]+sf.arms[3]+sf.arms[4]+sf.arms[5])%BIG_PRIMER;
}
 
int main()
{
	vector<snowflake> vs[BIG_PRIMER+3];
	int n;
	cin>>n;
	snowflake tempflake;
	int address;
	while(n--)
	{
		for(int i = 0;i < 6;i++)
		{
			scanf("%d",tempflake.arms+i);//cin>>tempflake.arms[i];
		}
		address = Hash(tempflake);
		vs[address].push_back(tempflake);
	}
	for(int i = 0;i < BIG_PRIMER;i++)
	{
		for(int j = 0;j < vs[i].size();j++)
		{
			for(int k = j+1;k < vs[i].size();k++)
			{
				if(cmpTwoSnowflakes(vs[i][j],vs[i][k])==true)
				{
					cout<<"Twin snowflakes found.\n";
					return 0;
				}
			}
		}
	}
	cout<<"No two snowflakes are alike.\n";
}

《编程珠玑》里的开篇故事里提到的电话号问题与刚才提到的标记思想相当契合。要对一个海量的数据进行排序,可以用一块连续的内存标记某号码是否出现过,然后从头开始扫描,若出现过则打印对应号码,以此完成排序。