题目:下过中国象棋的朋友都知道,双方的“将”和“帅”相隔遥远,并且它们不能照面。在象棋残局中,许多高手能利用这一规则走出精妙的杀招。假设棋盘上只有“将”和“帅”二子(如图所示)(为了下面叙述方便,我们约定用A表示“将”,B表示“帅”):
A、B二子被限制在己方3×3的格子里运动。例如,在如上的表格里,A被正方形{d10,f10, d8,f8}包围,而B被正方形{d3,f3, d1,f1}包围。每一步,A、B分别可以横向或纵向移动一格,但不能沿对角线移动。另外,A不能面对B,也就是说,A和B不能处于同一纵向直线上(比如A在d10的位置,那么B就不能在d1、d2以及d3)。
请写出一个程序,输出A、B所有合法位置。要求在代码中只能使用一个变量。
我们介绍三种方法并进行对比。
第一种解法:
题目要求只用一个变量,我们却要存储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(蓝色)所在位置:
6 | 7 | 8 |
3 | 4 | 5 |
0 | 1 | 2 |
6 | 7 | 8 |
3 | 4 | 5 |
0 | 1 | 2 |
以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次,计算量相当。
根据多次重复,统计可得三个算法运算时间相当,解法二较其他两个较快。
综上,解法二无论是空间上还是时间上性能都很好,解法一次之。