此文档的目的是帮助更多初学《编程之美》的Programmers少走弯路,致力于顺藤摸瓜。笔者也是一名编程初学者。实际编写中时有重新发明轮子的行为,并已患上重度查询文档症。精巧的算法往往伴随生僻的知识点,究其根本,结合编程之美具体实现,在此汇总发布。因水平所限,如有缺漏以及不严谨之处,请各位多多指教。
博弈论 [1] 又被称为对策论(Game Theory)既是现代数学的一个新分支,也是运筹学的一个重要学科。
在这里,我们探讨中国象棋的将帅问题。如图,为了下面叙述方便,我们约定用A表示将,B表示帅。
A、B两子被限制在乙方3*3的格子里运动。每一步,A,B分别可以横向或纵向移动一格,但不能沿对角线移动。另外,A不能面对B,也就是说,A和B不能处于同一纵向直线上。请写出一个程序,输出A,B所有合法的位置,要求在代码中只能使用一个变量。
在这里,我们采用byte数据类型,用前面的4bit表示A的位置,后面的4bit表示B的位置。那么存在16种情况满足题意。
总体思路很简单,可以用两个嵌套的for循环实现:
遍历A的位置
遍历B的位置
判断
若,则
一.为什么使用byte类型?
我们用排除法,首先肯定不能用BOOL类型。因为它只有两个值。
事实上,对本题来说,每个子都只需要9个数字就可以表达Ta和TA的位置。(皮一下开心)
一个8位的byte类型能够表达2^8=256个值,足够了。
那问题在于,如何用bit级的运算将数据从这一byte变量的左边和右边分别存入和读出。
dim b为byte类型的通用实例。
我们对于任意一个b,可以把它赋为任何的两点坐标。进而,我们可以任意改变它的值。这个过程我们用LeftMask和RightMask实现,类似于计算机组成原理课上的并列与赋值方法。下面是做法:
对于byte b等于任何一个值,我们不妨假设为b=10100101。
声明LMask=11110000,RMask=00001111.
(L/RMask&b)&n即可
同样的,我们也可以获取b的任意坐标。
L/RMask & b即可
同样的,我们可以使用“<<” ,“>>”进行左右移。
二.如何在不声名其他变量约束的前提下创建一个for循环?
我们采用宏来抽象化代码。
for(LSET(b,1);LGET(b)<=GRIDW*GRIDW;LSET(b,(LGET(b)+1)))
反复利用1byte的循环单元,把它作为循环计数器并用前面提到的存取和读入方法进行操作。
这个宏的意思是:取b的左地址为1(0001),对于不大于9的b的左地址,/* Do:我们把每个地址完整遍历一次b的右地址。 */,之后b地址加一。
别忘了,b的左右地址分别代表将帅二子坐标:
1 2 3 1 2 3
4 5 6 4 5 6
7 8 9 7 8 9
那么,for循环也可以很容易的实现了。
三.解法
这里给出代码和我学习的一些理解:
请读者思考一个问题:
对于Line:18,为什么要对GRIDW进行一次“%”运算?直接比较不行吗?
代码练习一:
//为了更容易理解,尽可能将常量赋予定义,将功能打包为宏。
#include
#define HALF_BITS_LENGTH 4
#define FULLMASK 255
#define LMASK (FULLMASK << HALF_BITS_LENGTH)
#define RMASK (FULLMASK >> HALF_BITS_LENGTH)
#define RSET(b,n) (b=((LMASK&b) ^ n))// ^ 是按位异或
#define LSET(b,n) (b=((RMASK&b) ^ (n<>HALF_BITS_LENGTH)
#define GRIDW 3//棋盘的行范围为3
int main()
{
unsigned char b;
for (LSET(b, 1); LGET(b) <= GRIDW * GRIDW; LSET(b, (LGET(b) + 1)))//先对A遍历
for (RSET(b, 1); RGET(b) <= GRIDW * GRIDW; RSET(b, (RGET(b) + 1)))//再对B遍历
if (LGET(b) % GRIDW != RGET(b) % GRIDW)//如果不冲突
printf("A = %d , B = %d \n", LGET(b), RGET(b));//单项输出
return 0;
}
很简单,因为我们要对比的是对于 列不变的{1,2,3} 的公倍数,例如数字5,位于第二列。我们通过5%3=2得到列数.
或许你会得到如下的错误答案:
//错误的答案,没有进行有效的列比较
A = 1 , B = 3
A = 1 , B = 4
A = 1 , B = 5
A = 1 , B = 6
A = 1 , B = 7
A = 1 , B = 8
A = 1 , B = 9
A = 2 , B = 1
A = 2 , B = 3
A = 2 , B = 4
A = 2 , B = 5
A = 2 , B = 6
A = 2 , B = 7
A = 2 , B = 8
A = 2 , B = 9
A = 3 , B = 1
A = 3 , B = 2
A = 3 , B = 4
A = 3 , B = 5
A = 3 , B = 6
A = 3 , B = 7
A = 3 , B = 8
A = 3 , B = 9
A = 4 , B = 1
A = 4 , B = 2
A = 4 , B = 3
A = 4 , B = 5
A = 4 , B = 6
A = 4 , B = 7
A = 4 , B = 8
A = 4 , B = 9
A = 5 , B = 1
A = 5 , B = 2
A = 5 , B = 3
A = 5 , B = 4
A = 5 , B = 6
A = 5 , B = 7
A = 5 , B = 8
A = 5 , B = 9
A = 6 , B = 1
A = 6 , B = 2
A = 6 , B = 3
A = 6 , B = 4
A = 6 , B = 5
A = 6 , B = 7
A = 6 , B = 8
A = 6 , B = 9
A = 7 , B = 1
A = 7 , B = 2
A = 7 , B = 3
A = 7 , B = 4
A = 7 , B = 5
A = 7 , B = 6
A = 7 , B = 8
A = 7 , B = 9
A = 8 , B = 1
A = 8 , B = 2
A = 8 , B = 3
A = 8 , B = 4
A = 8 , B = 5
A = 8 , B = 6
A = 8 , B = 7
A = 8 , B = 9
A = 9 , B = 1
A = 9 , B = 2
A = 9 , B = 3
A = 9 , B = 4
A = 9 , B = 5
A = 9 , B = 6
A = 9 , B = 7
A = 9 , B = 8
但是MSRA里却有人说,下面的一小段代码也能达到同样的目的:
代码练习2:
#include "stdio.h"
struct {
unsigned char a : 4;//What’s ":"?
unsigned char b : 4;
}i;
void main(){
for (i.a = 1; i.a <= 9; i.a++)
for (i.b = 1; i.b <= 9; i.b++)
if (i.a % 3 != i.b % 3)
printf("A = %d, B = %d\n", i.a, i.b);
}
首先我们先搞懂什么是位域
位域是指信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。这就是位域。
把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
这里,域名为a,b .
写一个小例子帮助大家快速理解:
struct Bit_Field
{
int a:2;
int b:6;
}BF;
//这里BF为Bit_Field变量,其中位域a占2位,位域b占6位,共占一个字节。
一个位域必须存储在同一个字节中,不能跨两个字节。如一个字节所剩空间不够存放另一位域时,应从下一单元起存放该位域。因此位域的长度不能大于一个字节的长度,也就是说不能超过8位二进位。如果用作填充和调整位置,位域可以无位域名,无名的位域是不能使用的。
那么,那种方式是效率最高的呢?我们利用VS性能探测器进行对比:
与代码练习一对比,嗯。。笔者平台少占用约9KB内存。别的平台还没测,如果将范围放大为半个棋盘,那差距应该很可观。
书P18页尾,作者注的一句话很有意思。
这一题目来自微软亚洲研究院工程师Matt Scott,他在学习中国象棋的时候想出了这个题目,后来一位应聘者给出了比他的“正解”简明很多的答案,他们现在成了同事。