[读书笔记]编程之美(一)
不得不说编程之美是一本很有意思的书,里面的各式各样新奇的问题,总是可以通过课上讲的简单的问题来解决,对于训练自己的思维的确有很大的好处。一般解决复杂的问题,我们总是可以通过:1、画图:链表、二叉树,2、举例,3、分解:分治法、动态规划来解决。
游戏 程序也是游戏的一种
1.1让CPU占用率曲线听你指挥
- 题目:用户决定CPU的占有率
- 思路:(举例)首先我们要明确什么是CPU的占有率,在任务管理器的一个刷新周期内,CPU忙(执行应用程序)的时间和刷新周期总时间的比率,就是CPU的占有率。也就是说调节CPU的忙闲比。
- 核心代码:
for(;;)
{
for(int i = 0; i < 9600000; i++)
;
Sleep(10);
}
1.2中国象棋将帅问题
题目:只剩A、B两个将帅,输出A、B所有合法位置。要求在代码中只能使用一个字节存储变量。
思路:(举例)
遍历A的位置
遍历B的位置
判断A、B的位置组合是否满足要求。如果满足,则输出。
- 核心代码:
BYTE i = 81;
while(i--)
{
if(i/9 % 3 == i % 9 % 3)
continue;
printf("A = %d, B = %d\n", i/9+1, i%9+1);
}
1.3一摞烙饼的排序
- 问题:假设有n块大小不一的烙饼,那最少要翻几次,才能达到大小有序的结果呢?
- 思路:(画图)我们先把最大的烙饼翻到最上面,然后再翻到最下面,然后对n-1、n-2个饼重复这个过程,至多需要2(n-1)次翻转。如果有几个相对有序就可以略过。
- 核心代码:
for(int i = 1;i < m_nCakeCnt; i++)
{ Reverse(0,i); m_ReverseCakeArraySwap[step] = i; Search(step + 1); Reverse(0,i); }
1.4买书问题
- 题目:在一份订单中根据购买的卷数及本数可能对应不同的折扣情况,但是一本书只会应用一个折扣规则。2本5%,3本10%,4本20%,5本25%。
- 思路:举例发现贪心不行,所以只能使用动态规划。
- 状态转移方程:空间O(Y1 * Y2 * Y3 * Y4 * Y5),时间O(Y1 * Y2 * Y3 * Y4 * Y5)
F(Y1,Y2,Y3,Y4,Y5)
= 0 if(Y1=Y2=Y3=Y4=Y5=0)
= min{
5 * 8 * (1-25%) + F(Y1-1,Y2-1,Y3-1,Y4-1,Y5-1), if(Y5 >= 1)
...
8 + F(Y1-1,Y2,Y3,Y4,Y5)
}
1.5快速找出故障机器
- 题目:(1)有一组数ID,每个都出现了两次,只有一个出现了一次求这个ID
(2)如果有两个呢? - 思路:因为一个数亦或自己等于0,所以我们只需要一次对数组所有数的遍历就能得到结果。
(2)可以先亦或遍历一波,然后根据结果的某一位1对数组分为两组,再遍历亦或。
1.6饮料供货
- 题目:STC提供n种饮料每种饮料都是2的方幂,用(S、V、C、H、B)表示饮料名字、容量、可能的最大数量、满意度、实际购买量,来表示第i种饮料。
- 思路:这就是一个变种的完全背包问题,用动态规划是肯定有解的。空间O(n)。
这里也可以根据容量相同,满意度从高到低排序,V%(2的i次方)非零,那么我们就可以贪心,购买一瓶满意度最高的V%(2的i次方)的饮料,现在的容量减去后,再算。每次升级单位,还应该考虑上一个单位的倍数的满意度。
1.7光影切割问题
- 问题:如果我们需要快速计算某个时刻,在X坐标[A,B]区间的地板上被光影划分成多少块。
- 思路:(举例)如果有N条直线,M个交点,那么区域的数目就是N+M+1。
思路一:将N条直线逐一投影到坐标区间上,[A,B]被划分的块数1+N+|交点|,只要求出所有直线两两相交的交点,然后再查找哪些交点再[A,B]之间,进而就可以求出平面被划分的块数。
思路二:直线a和直线b与左边界的交点(a,b),右边界的交点为(b,a),逆序对为1,即为交点。问题就转换成了求一个N个元素的数组的逆序数。
可以使用分治法,时间复杂度O(NlgN)
1.8小飞的电梯调度算法
- 问题:电梯从一楼上电梯,到达某层楼后,电梯停下来,乘客从那一层爬楼梯。电梯停在哪一层楼,能够保证这次乘坐电梯的所有乘客爬楼梯的层数之和最少。
- 思路:(举例)直接枚举0(n2)这是不可能的。假设在i层,乘客要爬楼梯Y层。如果有N1个乘客目的楼层在第i层楼以下,有N2个乘客在第i层楼,还有N3个乘客在以上。如果N1 + N2 < N3 时,电梯在i+1层停更好,否则第i层好
- 核心代码:
//得到第一层的
for(N1 = 0; N2 = nPerson[1], N3 = 0, i = 2; i <= N; i++)
{
N3 += nPerson[i];
nMinFloor += nPerson[i] * (i - 1);
}
for(i = 2; i <= N; i++)
{
if(N1 + N2 < N3)//慢慢往上找
{
nTargetFloor = i;
nMinFloor += (N1 + N2 - N3);
N1 += N2;
N2 = nPerson[i];
N3 -= nPerson[i];
}
else
break;
}
1.9高效率地安排见面会
- 问题:已知有n位学生,他们对m个研究组中若干感兴趣。每个见面会的时间为t。
- 思路:(画图)我们把每个小组看成是一些散布的点。如果有一位同学同时对两个小组感兴趣,两个小组对应的两个点加上一条边。
解法一:对顶点1分配颜色1,然后对剩下的N-1个顶点枚举其所有的颜色可能,再一一验证是否可以满足我们的着色要求,枚举的复杂度是O((n-1)n次方),验证一种颜色配置是否满足要求的时间复杂度是O(n2)。所以总的时间复杂度是O((n-1)n * n2)。
解法二:XJBS(瞎JB搜算法),又称启发式算法,可以得到一个近似解,而不是最优解。但复杂度远小于解法一。
1.10双线程高效下载
(待续)
1.11NIM(1)一排石头的游戏
- 问题:N块石头排成一行,每块石头有各自固定的位置。两个玩家依次取石头,每个玩家每次可以取其中任意一块石头,或者相邻的两块石头,石头在游戏过程中不能移位(编号不能改变),最后能将剩下的石头一次取光的玩家获胜。
- 思路:(举例)N=1,2,3,4,>4。N为奇数,取中间一个,N为偶数,取中间两个,保证左右两边数量一样,然后在后取者所取石头位置对称的地方取的数目相同的石头,就能必胜。
1.12NIM(2)“拈”游戏分析
- 问题:有N块石头和两个玩家A和B,玩家A先将石头分成若干堆,然后按照BABA…的顺序不断轮取石头,能将剩下的石头一次取光的玩家获胜。每次取石头时,每个玩家只能从若干堆石头中任选一堆,取这一对石头中任意数目(大于0)个石头。问:玩家A要怎么分配和取石头才能保证自己有把握取胜?
- 思路:(举例)得到结论,当摆放方法为(1,1,…,1)的时候,如果1的个数是奇数个,则先拿者赢;如果1的个数是偶数个,则先拿者必输。当摆放方法为(1,1,…,1,X)的时候先拿者必赢。
所以,策略:M为偶数时,把M分成相同的两份。M为奇数时,无论怎么分总是先动手的人赢。
1.13NUM(3)两堆石头的游戏
- 问题:假设有两堆石头,,每人每次可以从两堆石头中各取出数量相等的石头,或者仅从一堆石头中取出任意数量的石头,最后把剩下的石头依次拿光的人获胜。
- 思路:(举例)首先定义:先取者有必胜策略的局面为“安全局面“,否则为”不安全局面“。
找规律:所有不安全局面({<1,2>,<3,5>,<4,7>,<6,10>,…})的两个数合起来就是所有正整数的集合。而且之差也是正整数的集合。
an = [a * n], bn = [b * n],
a = (1 + sqrt[5]) / 2,
b = (1 + sqrt[5]) / 2
可以判断x(x <= y)是否等于[a] * (y – x)来判断< x,y >是否为一个不安全局面。
1.14连连看游戏设计
问题:
– 1.怎样用简单的计算机模型来描述这个问题?
– 2.怎么判断两个图形能否相消?
– 3.怎样求出相同图形之间的最短路径(转弯数最少,路径经过的格子数目最小)?
– 4.怎样确定死锁状态,如何设计算法来解除死锁?
思路:
– 1.自动机模型
生成游戏初始局面
Grid preClick = NULL, curClick = NULL;
while(游戏没有结束)
{
监听用户动作
if(用户点击格子(x,y),且格子(x,y)为非空格子)
{
preClick = curClick;
curClick.Pos = (x,y);
}
if(preClick != NULL && curClick != NULL
&& preClick.PIC == curClick.PIC
&& FindPath(preClick, curClick) != NULL)
{
显示两个格子之间的消去路径
消去格子preClick, curClick;
preClick = curClick = NULL;
}
}
- 2.充分必要条件是这两个图形相同,且它们之间存在转弯数目小于3的路径。广度优先搜索是解决经典最短路问题的一个思路。
- 3.广度优先搜索,就是每次优先扩展状态值最少的格子。
if((MinCrossing(X) + 1 < MinCrossing(Y)
|| ((MinDistance(X) + 1 == MinCrossing(Y))
&& (MinDistance(X) + Dist(X,Y) < MinDistance(Y))))
{
MinCrossing(Y) = MinCrossing(X) + 1;
MinDistance(Y) = MinDistance(X) + Dist(X,Y);
}
- 4.”死锁“问题本质上还是判断两个格子是否可以消去的问题。最直接的方法就是,对于游戏中尚未消去的格子两两计算一下它们是否可以消去。
我们可以打乱局面,打破死锁。
1.15构造数独
- 问题:程序的大致框架是什么?用什么样的数据结构存储数独游戏中的各种元素?如何生成一个初始局面?
- 思路:我们可以把每一个格子抽象为一个对象,把整体看成9 * 9的格子对象。
(1)用经典的深度优先搜索来生成一个可行解,然后再随机删去一些格子中的数值。
(2)有一个3 * 3的矩阵是排列好了的,然后我们可以置换行、列生成一个9 * 9的数独。
1.16 24点游戏
- 问题:给玩家4张牌,每张牌的面值在1 ~ 13之间,允许其中有数值相同的牌。采用加、减、乘、除四则运算,允许中间运算存在小数,并且可以使用括号,但每张牌只能使用一次,尝试构造一个表达式,使其运算结果为24.
- 思路:
(1)穷举法:
if(Array.Length < 2)
{
if(得到的最终结果为24) 输出表达式
else 输出无法构造符合要求的表达式
}
foreach(从数组中任取两个数的组合)
{
foreach(运算符(+、-、*、/))
{
1.计算该组合在此运算符下的结果
2.将该组合中的两个数从原数组中移除,并将步骤1的计算结果放入数组
3.对新数组递归调用f。如果找到一个表达式则返回
4.将步骤1的计算结果移除,并将该组合中的两个数重新放回数组中对应的位置
}
}
(2)分治法,去冗余,有点像动态规划。
1.17俄罗斯方块
问题:
- 1.如果你是设计者,如何设计各种数据结构来表示这个游戏的各种元素,如每一个可活动的积木块、在底层堆积的积木等。
- 2.现在已经知道底层积木的状态,然后在游戏区域上方出现了一个新的积木块,你如何运用刚才设计的数据结构来判断新的积木块要如何移位或旋转,才能最优效率地消除底部累计的积木?
- 3.如果有一个预览窗口。再怎么办。
思路:
- 1.用一个二维数组area[M][N]表示M * N的游戏区域。其中,数组中值为0表示空,1表示有方块。统一尺寸的数组来容纳所有可能的积木块,4 * 4的数组可以满足要求。rotateBlock = BlockSet[n][m%4],其中n是特定的方块序号,m是旋转的次数。
判断积木块是否和游戏区域中已有的方块重叠:
while(OffsetY < N - maxRow)
OffsetY++;
Flag = 0
For i = 0 To 3
For j = 0 To 3
IF (y > min(di - maxRowi)) //计算每一列触底高度的最小值
Flag = 1
1.18挖雷游戏
- 问题:表示剩余所有为标识的方块是否有地雷的概率。
- 思路:可以先标识所有肯定有地雷的方块(广度优先搜索)
所有子雷区的雷数+非子雷区的雷数+已确定是雷的雷数 = 总雷数。然后再做枚举,有雷的情况数的和除以总情况数,就是每块的概率了。