重读中国剩余定理

四月 10th, 2010 No Comments »

今天一个高中同学的校内更新了一个状态“屋内有多张桌子,若每3人一桌,多2人;每5人一桌,多4人;每7人一桌,多6人;每9人一桌,多8人;11人一桌刚刚好;Q:共有多少人?”。第一反应自然是“中国剩余定理”,但是细细想来,第一次看到中国剩余定理的时候我并没有仔细看透,而只是以为它是一种预处理手段,今天上网查了一下,原来是一个O(N)的解法(N为条件个数),非常高效,自己也算了算,原理很直观,很漂亮。当初低估你了。

就用上面的例子来说明吧。

我们要求一个数,这个数除3余2,除5余4,除7余6,除9余8,除11余0。首先我们应该保证这些数互素,否则条件是冗余的,容易发现“除9余8”包含“除3余2”,因此去掉第一个条件。下一步求出剩下这些互素的数5,7,9,11的最小公倍数3465,然后分别考察这四个互素的数:

1.用3465除以5,得到693,693是除了5以外三个数的最小公倍数,因此在我们要求的结果里加上任意倍的693不会影响对另外三个数余数的情况,而只会影响5的余数。我们的目标是令对5余4,容易发现693*3是符合该条件的最小数,记下它。接下来考察其它数的时候都按照这个方法,因为都是5的倍数,所以不会影响除5余4这个结果。

2.再举一步,考察7,用3465除以7,得到495,因此我们在所要求的结果上加上任意倍的495,都不会影响除了7以外的三个数的余数的情况,自然也不会影响刚刚考察过的5。而偶们的目标是除7余6,容易得出495*4符合条件。

3.考察9得到385*5。考察11得315*11。

4.将上述结果相加693*3+495*4+385*5+315*11=9449,则9449一定满足题目的条件,但是却不是最小的答案,因为从9449中加上和减去任意倍的3465都不影响对各个数的余数,因此对3465取模得到2519为最终答案。

威佐夫博弈、黄金分割、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;
	}
}

Mathematics Pearls

三月 24th, 2010 No Comments »

本篇不定时更新与数学有关的小point。

#1
(1+2+2^2+2^3+…+2^19)mod5=?

一共有20个数(2^0…2^19),每4个数分为一组:2^4n(1+2+4+8),0<=n<=4。1+2+4+8=15,15mod5=0,所以每一组模5都是0,故整体也是5的倍数,模5为0。

#2
哥德巴赫猜想及陈景润的工作

哥德巴赫猜想:任何一个偶数都能表示成两个素数的和。叙述简单,几百年来一直没有被成功证明过。因为要将一个偶数表示成一个素数加一个素数,因此称为“1+1”。

陈景润做的工作是证明了任何一个大偶数可以表示成两部分的和,一部分是一个素数,另一部分是两个素数之积,即所谓的“1+2”,距离“1+1”仅一步之遥。记得小学还是中学的某个老师讲过,陈景润最伟大的成就就是证明了1+1=2,当时我们都很纳闷,简单的1+1=2有什么可证明的,然后老师就故弄玄虚(应该是误弄玄虚)地说这里面其实学问可大了。所以这么多年来我一直认为1+1=2这个等式不是自然成立的,而它之所以成立的原因甚至可以称为数学的根基。今天想来,不免觉得好笑,呵呵。

#3
求最大公因子的欧几里德辗转相除法

记a,b(a>b)的最大公因子为gcd(a,b)——gcd for greatest common divisor。有a=bq+r,r为a除以b得到的余数,我们有gcd(a,b)=gcd(b,r)。因为任意a,b的公因子s,有a=ms,b=ns,r=a-bq=ms-qns=(m-qn)s,因此s也是b和r的公因子;反过来,对于任意b,r的公因子s’,有b=m’s',r=n’s',a=bq+r=qm’s'+n’s'=(qm’+n’)s’,因此s’也是a和b的公因子,综上,a,b公因子集合与b,r公因子集合相等,故gcd(a,b)=gcd(b,r)。这样一直进行求余数r的工作,就能一直将问题的规模缩小,直到r=0——gcd(b,0)=b。

#4
“2^n-1当且仅当n为偶数时是3的倍数”的证明

这个证明利用的是同余关系mod,首先同余有如下性质:a≡b,c≡d =>ac≡bd(证明:a≡b=>(a-b)=km,c≡d=>(c-d)=lm,ac-bd=(a-b)c+b(c-d)=(kc+bl)m,因此ac≡bd),由此性质得出a≡b=>a^n≡b^n,因为2≡-1(mod 3),令a=2,b=-1,则2^n≡(-1)^n,当n为偶数时,等式为2^n≡1(mod 3),则2^n-1=3k,证毕。

“等差数列(4n+3)中有无限多个素数”的证明

三月 24th, 2010 No Comments »

证明等差数列3,7,11,15,…中有无限多个素数

因为所有大于2的素数都是奇数,否则会被2整除,所以素数只有两种形式:4n+1、4n+3。
反证法。假设形为4n+3的素数是有限多的,不妨设它们为p1,p2,…,pn。考虑数A=4p1p2…pn-1=4(p1p2…pn-1)+3,要么它本身是素数,要么它有素数因子,而p1,p2,…,pn都不是它的素数因子,因为相除余-1,而p1,p2,…,pn是全部的4n+3形的素数,因此它的所有素数因子都只能是4n+1形的,但是我们又知道两个4n+1形的数相乘仍然是4n+1形——(4a+1)(4b+1)=4(4ab+a+b)+1,而A不是4n+1形的,因此产生矛盾,假设不成立,4n+3形的素数是无限多的。证毕。

“自然数中有无限多个素数”的证明,另几个相关定理的证明

三月 24th, 2010 1 Comment »

“自然数中有无限多个素数”的证明

欧几里德给出的反证法:

首先,任何一个正整数都能表示成素数的乘积。

假设自然数中只有有限多个素数,不妨设为n个,它们是q1,q2,q3,…,qn。现构造一个数A=(q1*q2*q3*…*qn)+1,A比q1,…qn中的任何一个都大,因此与它们都不同,按照假设A应该是合数。A不能被q1,…qn中的任何一个整除,因为会余1,所以q1,…qn都不是A的因子,即A不能表示成q1,…qn的乘积,这与我们假设的只有q1,…qn是素数是矛盾的,因此自然数中有无限多个素数,证毕。

由此得出的一个构造无穷多个素数的方法:

设已经构造出了n个素数,那么A=(q1*q2*…*qn)+1要么是新的素数,要么包含新的素数因子,如果是后者,通过不断试验也可以找到这个新素数因子,因此可以构造出q(n+1),如此反复,可构造出任意多个素数。

“任何比1大的整数N,在不考虑顺序的情况下,只能按唯一一种方式分解成素数的乘积”的证明

假设存在这样的大于1的、可以按照一种以上方式分解成素数乘积的整数,设满足此条件的最小整数为m,则两种分解方式可写作:

m=p1*p2*…*pr

m=q1*q2*…*qs

我们要求写成这样的形式,这是完全可以办到的:p1<=p2<=…<=pr;q1<=q2<=…<=qs。且所有因子都大于1,因为乘法里乘以1是可有可无的。肯定有p1!=q1,否则可以让m除以p1和q1,得到一个有两种不同素数分解形式的比m小的整数,这与对m的选择相矛盾。不妨设p1<q1,令m’=m-p1*q2*q3*…*qs,将m的两种表达式分别代入得到:

m’=p1*p2*…*pr-p1*q2*…*qs

     =p1*(p2*…*pr-q2*…*qs)                                 (1)

m’= q1*q2*…*qs-p1*q2*…*qs

     =(q1-p1)*q2*…*qs                                            (2)

因为q1>p1,所以由(2)知,m’是正整数,且m’<m,所以m’只能按一种方式写成素数的乘积,因此(1)中的第一项p1要么是(2)中(q1-p1)部分的素因子,要么是q2*…*qs部分的素因子,而后一部分中所有q都是素数、且都比p1大,因此不可能,故p1是(q1-p1)的素因子,即

(q1-p1)=h*p1

则q1=(h+1)p1,这与q1是素数是矛盾的,因此我们的假设不成立,不存在可以写成一种以上素数乘积形式的大于1的整数,证毕。

推论“如果一个素数p是a*b的因子,则要么它是a的因子,要么是b的因子”的证明:

反证法,如果p既不是a的因子也不是b的因子,将a和b的素数乘积形式分别写出,应该都找不到p,将两个乘积相乘得到a*b的乘积形式也找不到p,这样我们得到了将a*b进行素数分解的不包含p的形式。又因为p是a*b的因子,因此有a*b=p*t,将t写成素数分解的形式再乘以p,我们又得到了一个将a*b进行素数分解的包含p的形式,这与“ a*b的素数分解形式是唯一的”这一事实相矛盾,因此假设不成立,p要么是a的因子,要么是b的因子,证毕。

一个整数的所有因子(素因子与合因子)的个数

若a=p1^m1*p2^m2*…*pn^mn,p1,…,pn是a的所有素因子,每一个都有某次幂。则a的因子b应该有如下形式

b=p1^k1*p2*k2*…*pn^kn

其中,0<=k1<=m1,0<=k2<=m2,…0<=kn<=mn。不同的b就是从k1到kn的各种不同选择,因此a的因子b的个数为(m1+1)(m2+1)…(mn+1)。例如144=2^4*3^2,故它的因子个数为(4+1)(2+1)=15,分别是1,2,3,4,6,8,9,12,16,18,24,36,48,72,144。

Josephus问题的一个递推思路,复杂度O(n)

三月 23rd, 2010 No Comments »

本思路可用O(n)的复杂度解决最原始的Josephus问题,即只找出最后一个幸免者。关于约瑟夫(Josephus)问题:

N个人围成一圈,从第一个开始报数,第M个将被杀掉,最后剩下一个,其余人都将被杀掉。例如N=6,M=5,被杀掉的人的序号为5,4,6,2,3。最后剩下1号。

为了方便使用mod处理数据,将N个人编号0-N-1,每次报数从0开始,报到M-1的被杀掉。

第一个被杀掉的一定是编号(M-1)mod(N)的人,他被杀掉后,令k=(M)mod(N),则剩下的问题为:编号为:k,k+1,k+2,…,n-1,0,1,…,k-2的N-1个人,从k开始从0进行报数,报到M-1的人被杀掉。所以如果做一个i’ = (i-k)mod(N)的映射,就可以真正将问题转换成J(N-1,M),解出答案后再做逆映射i = (i’+k)mod(N)即可得到J(N,M)的解,即J(N,M) = (J(N-1,M)+k)mod(N)。又知道k = (M)mod(N),所以递推公式的形式是J(N,M) = (J(N-1,M)+M)mod(N)。递推起点是J(1,M)=0。(注意k,k+1,k+2,…n-1,0,1,…k-2的序列在考虑mod操作后并没有数据上的跳跃,因此做了映射后才仍然是问题的原型,抹去了杀掉一个人的影响。)

J(1,M) = 0

J(N,M) = (J(N-1,M)+M)mod(N)

这样,只要从1推到N就可以得出问题的答案,复杂度O(N)。

新的开始

三月 17th, 2010 No Comments »

新的域名,新的开始,新的生活,新的征程!

用MFC写了个贪吃蛇

二月 3rd, 2010 No Comments »

这几天看侯SIR的《深入浅出MFC》,看完了觉得如果就这么放下可能效果不好,就做了个贪吃蛇,本来打算做另一个东西呢,结果发现现在知识储备还不够,还得看看书,再说吧!

贪吃蛇实现得比较简单,SDI,单线程,没用位图,因为我没搞明白怎么编辑和使用位图,而且在网上看到一个也没用位图的贪吃蛇范例,很好看,就用简单地矩形填充做了。

做的过程中遇到的问题大概记录一下:

首先是定时器,贪吃蛇肯定要定时器,用以推进游戏进度。MFC中定时器的用法是:

SetTimer()用以设置定时器,指定定时间隔和消息处理函数,改变时间间隔可以改变游戏难度,如果不指定消息处理函数则由framework调用OnTimer(),这也是一般的做法;
OnTimer()用以响应,在此中完成该做的工作;
KillTimer()使用结束杀掉定时器。

需要注意的是,只有CWnd的派生类才能接受定时器消息,所以只能在CMyView和CMainFrm中使用。SetTimer()必须在窗口完全产生之后调用,KillTimer()必须在窗口销毁之前调用,因此要掌握好时机,否则会导致一个ASSERT FAILED。

有了定时器,蛇就可以走了,由于贪吃蛇每次前进时全身所有点都向前挪一位,就相当于尾部节点接到头结点前面而其它节点不动,而这样做移动的效率就很高,为了适应这种操作,选用了CList<CPoint>作为描述Snake的数据结构。

改变蛇的运动方向要靠响应键盘信息,响应ON_COMMAND的WM_KEY_DOWN,如果响应WM_KEY_UP的话灵敏度不太够。

对于设置背景颜色、改变窗口风格也都轻车熟路了,多亏了侯SIR的书。snake

由于刚看完《深入浅出MFC》,对MFC的框架比较熟悉,对Document/View的配合比较了解,所以写起来比上次写魔方顺手的多,有些改动也知其然并知其所以然了,感觉还是不错的。

不过写个破贪吃蛇真是没啥意思,本来我想把它写成多线程,当练手,不过又觉得太勉强了,就算了。

再好好看看书,写个复杂点的东西吧。

MFC的Serialization

一月 31st, 2010 No Comments »

所谓serialization,即序列化,说白了就是将程序用到的资料(Document)存储在文件中以及从文件中提取回来,目的是让对象有永久的生命力,不会因为程序的退出而丢失。比如做一个文字录入工具,肯定不希望程序退出后录入的文字就消失,而是希望将它保存起来,等再用程序打开文件时又能显示出来。
CArchive类是MFC负责serialization的类别。

由于Serialize()是CObject的虚函数,因此任何一个派生于此的类都有了该函数但也必须改写,而且所有CObject的直接派生类应该在改写的Serialize()中首先调用CObject::Serialize()。

任何派生自CObject的类都继承了虚函数Serialize(),该函数以一个CArchive对象为参数,并利用此对象进行序列化,即最终调用的是CArchive类的成员函数。

对serialization的管理应该呈层次化,即由上到下做好分内的事,然后让成员自己负责自己的序列化,即先用>>和<<将本层需负责的对象序列化好,再调用成员的Serialize()函数,在下一层中,成员仍然是利用<<和>>与CArchive对象交互。由于CArchive重载了<<和>>操作符,因此具体的序列化工作在CArchive的这两个操作符定义中完成。

《Serialization的写档动作》

当用户选择SaveAs时,程序执行的流程如下:
ID_FILE_SAVE_AS的消息在CDocument中被映射到CDocument::OnFileSaveAs(),该函数调用CDocument::DoSave(NULL),由于以NULL为参数,因此会调用CDocument::DoPromptFileName()从用户处接受输入的文件名,再调用CDocument::OnSaveDocument(),在此函数中,会定义一个CArchive对象,借之作为参数调用Serialize(),由于CMyDoc改写了该虚函数,因此调用的是CMyDoc::Serialize(),此CMyDoc::Serialize()就是程序员需要修改的一个主要函数。也就是Serialization层层分工的开始,从这开始,每一层做好自己的Serialization工作,将剩余工作交给下一层,具体可以调用CArchive类的各个成员函数和<<、>>操作符。

如果在某一层调用了CObList::Serialize()的话,有一些内容还值得研究,CObList::Serialize()的内容说复杂也并不复杂:无论存取,都是先针对count值,即该CObList对象所含的元素个数,然后再在一个循环里依次存取各个元素。

CArchive的<<操作符调用CArchive::WriteObject(),该函数是CArchive类实现序列化的根本所在,负责对一个对象进行序列化。CArchive的复杂动作(之所以能高效地实现序列化)可在此函数处找到玄机。首先调用CArchive::WriteClass()写入该对象的RuntimeClass信息,CArchive::WriteClass()会判别此类别是不是新类别,如果是则存储其版本号、类名长度、类名,如果不是则进行相应标记。继而CArchive::WriteObject()会调用所序列化对象自己的Serialize(),由它完成具体的该对象序列化任务,该函数可能进行分层部署,如果此对象有其它类别成员的话。

《Serialization的读档动作》
读档动作是从资料到对象,所以涉及到RTTI和动态生成。简要回顾一下RTTI和动态生成:
想要支持RTTI和动态生成的每个类在声明和给出定义时要加上如下宏:
DECLARE_DYNAMIC()/IMPLEMENT_DYNAMIC() for RTTI
DECLARE_DYNCREATE()/IMPLEMENT_DYNCREATE() for Dynamic Creation
这两个宏负责将类在动态识别或生成时需要的信息(类名、类对象大小、生成类对象所需函数的指针等――都是CRuntimeClass对象的成员)加入一个CRuntimeClass对象的链表中。在读档时读出要生成的对象的类名,然后到CRuntimeClass链表中比对从而得到相应信息进行对象的动态生成,此为Serialization和Dynamic Creation的结合之处。

当用户点击File/Open时,ID_FILE_OPEN消息会被CWinApp::OnFileOpen()拦截,该函数又调用CDocManager::OnFileOpen(),此函数通过DoPromptFileName()取得用户输入的文件名,然后以之为参数调用CWinApp::OpenDocumentFile(),中间有很绕的调用关系,简单说来就是创建Document\Window\View三件组,然后进行serialization,Serialization的过程和写档就很相似了,又会调用到ReadObject()、ReadClass()等函数,还会调用CreateObject()用于动态生成来承接读档的结果。

Serialization由“<<”和“>>”操作符调用CArchive::ReadObject()和CArchive::WriteObject(),这也不是最底层,这两个函数先利用类似RTTI的技术取得所序列化的类别信息,也就得到了其Serialize()函数指针,再调用该函数进行具体的读写。而这个Serialize()函数,正是需要程序员具体实现的,即对于特定的类还是只有编写它程序员知道具体的读写方式,才能制定具体的读写方法。但是这个CMyObject::Serialize()有可能还会调用CArchive的“>>”和“<<”操作符,如果该对象还有其它类型对象作为其成员的话,这样又开始上述过程,整个过程呈递归螺旋上升状,这个递归过程直到所有的“<<”和“>>”操作都转换为CArchive所重载的那些针对C++基础类别和MFC基础类别的“<<”和“>>”为止,进行递归返回,直到最外层的CMyObject::Serialize()调用结束。

关于一个程序相关文件的内容如何解释,必须由程序员负责,所谓负责就是程序员在CMyObject::Serialize()中定义将对象按何种格式写入文件,并按照相同格式读取之回到对象。