中国象棋的将帅问题

题目:下过中国象棋的朋友都知道,双方的“将”和“帅”相隔遥远,并且它们不能照面。在象棋残局中,许多高手能利用这一规则走出精妙的杀招。假设棋盘上只有“将”和“帅”二子(如图所示)(为了下面叙述方便,我们约定用A表示“将”,B表示“帅”):

AB二子被限制在己方3×3的格子里运动。例如,在如上的表格里,A被正方形{d10,f10, d8,f8}包围,而B被正方形{d3,f3, d1,f1}包围。每一步,AB分别可以横向或纵向移动一格,但不能沿对角线移动。另外,A不能面对B,也就是说,AB不能处于同一纵向直线上(比如Ad10的位置,那么B就不能在d1d2以及d3)。

请写出一个程序,输出AB所有合法位置。要求在代码中只能使用一个变量。

我们介绍三种方法并进行对比。

第一种解法:

题目要求只用一个变量,我们却要存储A和B两个棋子的位置信息,该怎么办呢?

先把已知变量类型列举一下,然后分析。

对于bool类型,估计没有办法做任何扩展了,他只能表示true和false两个值;而byte或int类型,能够表达的信息则更多。事实上,对本题来说,每个子都只需要9个数字就可以表示它的全部位置。

一个8位的byte类型能够表达256个值,用它来表示A和B 的位置信息绰绰有余,把这个字节的变量(设为b)分成两部分。用前面的4bit表示A的位置,用后面的4bit表示B的位置,而4bit可以表示16个数,这完全足够。现在问题在于:如何使用bit级的运算将数据从这一byte变量的左边和右边分别存入和读出。

下面是做法:

1.将byte b(10100101)的右边4bit(0101)设为n(0011):

首先清除b 右边的bits,同时保持左边的bits:

   11110000(LMASK)

& 10100101(b)          

   10100000

然后将上一步得到的结果与n做或运算

    10100000(LMASK &b)

|   00000011 (n)                 

    10100011 

 

2..将byte b(10100101)的左边4bit(1010)设为n(0011):

首先清除b 左边的bits,同时保持右边的bits:

   00001111(RMASK)

& 10100101(b)          

   00000101

现在把n移动到byte数据的左边

n<<4=00110000

然后对以上两步得到的结果做或运算

    00000101(RMASK &b)

|   00110000(n<<4)                 

    00110101

3.得到byte数据的右边4bits或左边4bits(e.g.10100101中的1010以及0101):

清除b左边的bits,同时保持右边的bits:

    00001111(RMASK)

& 10100101(b)          

   00000101

清除b右边的bits,同时保持左边的bits:

    11110000(LMASK)

& 10100101(b)          

   10100000

将结果右移4bits

10100000>>4=00001010

最后的挑战是如何在不声明其他变量的约束的前提下创建一个for循环。可以重复利用1byte的存储单元,把它作为循环计数器并用前面提到的存取和读入技术进行操作,还可以用宏来抽象化代码。

解法一代码如下:

ChessGeneral.h

<span style="color:#000000;">#include <stdio.h>
#include <iostream>
using namespace std;

#define  HALF_BITS_LENGTH 4                                            //这个值记忆存储单元长度的一半,在这里是4bit
#define FULLMASK  255                                                       //表示一个全部bits的mask,在二进制中为11111111
#define LMASK (FULLMASK<<HALF_BITS_LENGTH)     //表示左bits的mask,在二进制中为11110000
#define RMASK (FULLMASK>>HALF_BITS_LENGTH)     //表示右bits的mask,在二进制中为00001111
#define RSET(b,n)   (b=((LMASK & b) | (n)))                       //将b的右边置为n
#define LSET(b,n)    (b=((RMASK & b) |((n)<<HALF_BITS_LENGTH)))                      //将b的左边置为n
#define RGET(b)       (RMASK&b)                                       //得到b的右边的值
#define LGET(b) ((LMASK&b)>>HALF_BITS_LENGTH ) //得到b的左边的值
#define GRIDW   3                                                                 //将帅、将移动范围的行宽度
void simulation1();</span>
<span style="color:#000000;">main.cpp</span>
<span style="color:#000000;">#include "ChessGeneral.h"

void main()
{
	simulation1();	
}
void simulation1()
{
	int i=0;
	unsigned char b;
	cout<<"simulation1:"<<endl;
	for (LSET(b,1);LGET(b)<=GRIDW*GRIDW;LSET(b,(LGET(b)+1)))
	{
		for (RSET(b,1);RGET(b)<=GRIDW*GRIDW;RSET(b,(RGET(b)+1)))
		{		
			if (LGET(b)%GRIDW!=RGET(b)%GRIDW)
			{
				cout<<"A="<<LGET(b)<<","<<"B="<<RGET(b)<<"   ";
				i++;
				if (i%3==0)
				{
					cout<<endl;
				}
			}
		}
	}
}</span>
<span style="color:#000000;">运行结果如下:</span>
<span style="color:#000000;"><img src="https://img-blog.csdn.net/20141216114513875?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvc2luYXRfMjQ1MjA5MjU=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/Center" alt="" /></span>

第二种解法:

只能使用一个变量nNum ==> 只能使用一个循环,nNum只能用来表示A、B位置的组合,nNum最大为9×9-1=80;

 如何用nNum表示一个A、B位置的组合?

下图表示A(红色)、B(蓝色)所在位置:

 

 

678
345
012
678
345
012

以nNum%9表示A的位置,nNum/9表示B的位置,如nNum==15,A==6,B==1。

又因为:

81/9=9,80/9=9,79/9=9,78/9=9,77/9=9,76/9=9,75/9=9,74/9=9,73/9=9,72/9=8,...,

9%9=0,8%9=8,7%9=7,6%9=6,5%9=5,4%9=4,3%9=3,2%9=2,1%9=1,0%9=0,

如何确定A、B位置的合法性?

规则都指定了,合法性的确定也就很简单了:A%3 != B%3。

解法二代码:

<span style="color:#000000;">#define BYTE unsigned char 
void main()
{	
	simulation2();
}
void simulation2()
{
	BYTE i=81;
	int j=0;
	cout<<"simulation2:"<<endl;
	while(i--)
	{
		if (i/9%3==i%9%3)
		{
			continue;
		}
		cout<<"A="<<i/9+1<<","<<"B="<<i%9+1<<"   ";
		j++;
		if (j%3==0)
		{
			cout<<endl;
		}
	}
}</span>

运行结果:


第三种解法:

使用位域节省空间,位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。

使用占用一个字节的结构体变量

struct
 {
  unsigned char a:4;
  unsigned char b:4;
 }General;
unsigned char a:4;表示结构体变量a只使用其中的低4位;高4位可用作他用,在这里是给变量b使用了。

解法三代码如下:

<span style="color:#000000;">void main()
{
	simulation3();
}

void simulation3()
{
	int i=0;
	cout<<"simulation3:"<<endl;
	struct 
	{
		unsigned char a:4;
		unsigned char b:4;//表示结构体变量a只使用其中的低4位,高4位可用作他用,在这里是给变量b使用了。
	}General;
	for(General.a=1;General.a<=9;General.a++)
	{
		for (General.b=1;General.b<=9;General.b++)
		{
			if (General.a%3!=General.b%3)
			{
				cout<<"A="<<(int)General.a<<","<<"B="<<(int)General.b<<"   ";
				i++;
				if (i%3==0)
				{
					cout<<endl;
				}
			}
		}
	}
}</span>

运行结果为:


三种方法的对比:

不同点:

解法一使用#define定义的标识符不是变量,它只用作宏替换,因此不占有内存,解法一代码宏定义略复杂;

解法二代码简洁,逻辑关系不清楚,不易扩展;

解法三逻辑关系清楚明了。

相同点:

结果都正确,循环81次,计算量相当。

根据多次重复,统计可得三个算法运算时间相当,解法二较其他两个较快。

综上,解法二无论是空间上还是时间上性能都很好,解法一次之。

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值