编程之美-第1章 游戏之乐

1. 使CPU占有率画出正弦曲线

clip_image002

clip_image004

linux下的代码:

#if 0
/*
 * Q1.1
 */
int get_tick_count(){
    struct timeval tv;
    gettimeofday(&tv,NULL);
    return tv.tv_sec*1000000+tv.tv_usec;
}
const int Interval=300;
const int COUNT=300;
const double split=0.01;
const double PI=3.1415926;

int main(){
    int busySpan[COUNT];
    int idleSpan[COUNT];
    int half=Interval/2;
    int radian=0;
    for(int i=0;i<COUNT;i++){
        busySpan[i]=(int)(half+sin(PI*radian)*half);
        idleSpan[i]=Interval-busySpan[i];
    }
    int j=0;
    int start;
    while(true){
        j=j%COUNT;
        start=get_tick_count();
        while(get_tick_count()-start<=busySpan[j])
            ;
        sleep(idleSpan[j]);
        j++;
    }
}

#endif

2. 中国象棋将帅问题

clip_image006

有三种方法:

方法1:

使用一个字节, 高4位表示A的位置,然后低4位表示B的位置, 然后剩下的就是分别对高4位和低4位进行操作了.

代码如下, 对位操作都是用宏实现的, 这样就不会占用其他变量了.

#if 0
/*
 * Q1.2
 */
#include <iostream>
#define OFFSET 4
//#define FMASK 0xFF
#define LMASK 0xF0
#define RMASK 0x0F
#define LGET(b) ((LMASK&b)>>OFFSET)
#define RGET(b) (RMASK&b)
#define RSET(b,n) (b=((LMASK&b)|n))
#define LSET(b,n) (b=((RMASK&b)|n<<OFFSET))
int main(){
    unsigned char b;
    int i=0;
    for(LSET(b,1);LGET(b)<=9;LSET(b,LGET(b)+1)){
        for(RSET(b,1);RGET(b)<=9;RSET(b,RGET(b)+1)){
            if(LGET(b)%3==RGET(b)%3)
                continue;
            cout<<"A:"<<LGET(b)<<' '<<"B:"<<RGET(b)<<endl;
            i++;
        }
    }
    cout<<i<<endl;
}
#endif

方法2:

使用一个char类型变量, 由于A和B的位置个数都是9个, 所以我们将”AB”想象成一个9进制的二位数, 那么该9进制数最小为0, 最大为80, 我们的工作就是取出该9进制的高位和低位, 然后判断高位和低位是否符合要求.

代码如下:

#if 0
/*
 * Q1.2
 */
int main(){
    char b=81;
    int i=0;
    while(b--){
        if(b/9%3==b%9%3)
            continue;
        cout<<"A:"<<b/9<<' '<<"B:"<<b%9<<endl;
        i++;
    }
    cout<<i<<endl;
}

#endif

方法3:

方法3和方法1类似, 只是采用了另一种的思考方式, 使用了C/C++的struct的一个特殊用法:位域.

代码如下:

#if 0
/*
 * Q1.2
 */
struct _p{
    unsigned char a:4;
    unsigned char b:4;
}p;
int main(){
    int i=0;
    for(p.a=1;p.a<=9;p.a++)
        for(p.b=1;p.b<=9;p.b++) {
            if(p.a%3==p.b%3)
                continue;
            cout<<"A:"<<p.a%3<<' '<<"B:"<<p.b%3<<endl;
            i++;
        }
    cout<<i<<endl;
}

#endif

3. 烙饼排序

clip_image008

基本思路:

clip_image010

4. 买书问题

clip_image012

分析:

这个题先考虑贪心算法, 优先考虑最大的折扣, 但是我们可以发现贪心策略会失效的:

clip_image014

再考虑动态规划的方法:

为了更好的使用动态规划的方法, 我们要考虑一下如何表示所出现的中间状态.

我们使用Xi表示第i(1<=i<=5) 卷购买数量. F(X1,X2,X3,X4,X5)表示所需要的费用. 从题目的要求中我们可以看出, Xi之间的大小关系是无关紧要的, 因为打折的方法只是要求卷不同, 与具体是哪一卷无关. 为了更好的表示, 我们利用(Y1,Y2,Y3,Y4,Y5)且Y1>=Y2>=Y3>=Y4>=Y5来表示(X1,X2,X3,X4,X5)的所有排列.

clip_image016

这样可以得到状态转移方程:

clip_image018

clip_image020

5. 快速找出故障机器

clip_image022

这儿给出了4种解法:

方法1:

最直接的方法就是对这个ID列表进行遍历, 同时记录每个ID出现的次数, 根据最后记录得到的次数很容易找到出现一次的ID. 不论有几台机器死机, 这种情况都可以完成. 这种情况的时间复杂度是O(N), 空间复杂度是O(N)

方法2:

由于在这个问题中, 大部分ID出现的次数都是2, 所以这个2我们可以不必存储.

做法就是: 使用一个变长数组记录ID出现的次数, 如果某个ID出现次数等于2, 则删除这个ID, 则最后剩下的ID就是出现一次的ID. 这个方法最好情况下空间为O(1), 最坏为O(N)

方法3:

这个方法的思想是寻找一个函数, 该函数利用所有的ID可以计算出缺失的ID. 这样的一个函数比较典型的是异或运算, 由于异或运算满足结合律和交换律, 所以将所有ID进行异或之后, 如果ID出现两次, 结果就为0, 这样最终的结果就是只出现一次的ID.

如果只有一台机器死机, 那么该ID就是所求的ID. 如果有两台机器死机, 比如A和B, 如果A=B那么A⊕B=0, 就是说一份数据的两个拷贝都丢失了, 这样我们就没法确认是哪个了. 如果A⊕B不为0, 那么其结果中肯定有一位为1,这样就根据该位是否为1将所有的ID分为两类:一类是该位为1, 一类是该位为0, 这样两类ID分别异或就得到了A和B.

方法4:

该方法是寻找所有ID的一个不变量, 这个不变量就是所有ID的和.所以可以预先计算出所有ID的和,然后再计算出当前所有ID和, 两者的差就是出现一次的ID. 如果是有两个ID出现一次的话,则x+y=a. 这样为了求出x和y, 我们还必须找出一个方程. 可以用同样的方式计算x*y=b, 或者x^2+y^2=c,这样都可以求出x和y.

1.6 饮料供货

1.7 光影切割问题

clip_image024

clip_image026

clip_image028

clip_image030

clip_image032clip_image034

1.8 电梯调度算法

clip_image036

clip_image038

clip_image040

代码如下:

void best_floor(int *presons,int n,int &floor,int& min_floors){
    if(persons==0||n<=0)
        return;
    floor=-1;
    min_floors=0x7fffffff;
    int count=0;
    for(int i=0;i<n;i++){
        for(int j=1;j<i;j++)
            count+=persons[j]*(i-j);
        for(int j=i+1;j<n;j++)
            count+=persons[j]*(j-i);
        if(floor==-1||min_floors>count){
            min_floors=count;
            floor=i;
        }
    }
}

clip_image042

代码如下:

void best_floor2(int *presons,int n,int &floor,int& min_floors){
    if(persons==0||n<=0)
        return;
    int N1=0,
        N2=persons[0],
        N3=0;
    min_floors=0;
    for(int i=1;i<n;i++){
        N3+=persons[i];
        min_floors+=persons[i]*i;
    }
    for(int i=1;i<n;i++){
        if(N1+N2<N3){
            floor=i;
            min_floors+=(N1+N2-N3);
            N1+=N2;
            N2=persons[i];
            N3-=persons[i];
        }
        else
            break;
    }
}

clip_image044

1.9 高效的安排见面会

1.11 NIM(1) 一排石头的游戏

clip_image046

clip_image048clip_image050clip_image052clip_image054

1.12 NIM(2)

clip_image056

问题:

有N块石头和两个玩家A和B,玩家A先将石头随机分成若干堆,然后按照BABA...的顺序不断轮流取石头,能将剩下的石头一次取光的玩家获胜,每次取石头时,每个玩家只能从若干堆石头中任选一堆,取这一堆石头中任意数目(大于0)个石头。

请问:

玩家A要怎样分配和取石头才能保证自己有把握取胜?

如果石头的个数N为偶数,A只要将其分为相同的两份,就一定能取胜。

初始:XOR(M1, M1) == 0

玩家B:XOR(M1, M2) != 0  (其中一堆的个数减少到M2)

玩家A:XOR(M2, M2) == 0  (玩家A将另一堆的个数也减少到M2)

结果:XOR(M2, M2) == 0  (直到结束状态(0, 0))

如果石头的个数N为奇数,B有必胜的方法。

初始:XOR(M1, M2, ... , Mn) != 0

玩家B:XOR(M1, ... , Mi', ... , Mn) == 0 (其中一堆Mi的个数减少到Mi')

玩家A:XOR(M1, ... , Mj', ... , Mn) != 0

玩家B:XOR(M1, ... , Mi', ... , Mn) == 0 (其中一堆Mi的个数减少到Mi')

结果:XOR(M1, ... , Mj' , ... , Mn) == 0 (直到结束状态(0,0))

这里就有个问题:已知XOR(M1, M2, ... , Mn) != 0,玩家B该改变那个Mi以使得XOR(M1, ... , Mi', ... , Mn) == 0呢?

对于这个问题的答案,书中并未准确的结论。

经过本人的分析,所得到的结论如下:

设k=XOR(M1, M2, ... , Mn),已知k!=0,取一个数Mi,其二进制表达中在k的最高二进制位上的数为1,且这个

数Mi肯定存在(k的这个最高位在异或运算中肯定来自某一个Mi)。在程序中满足(Mi&k) > (k>>1)条件的数即为Mi。

简单证明:即假设k的二进制表达是1xx,那么Mi的二进制表达是x...x1xx,这样玩家B将该Mi改成Xi'=XOR(Mi, k)后,

Mi'的二进制表达是x...x0yy,肯定小于Mi,并且有XOR(M1, ... , Mi', ... , Mn) == 0。

参考: http://blog.csdn.net/linyunzju/article/details/7661060

1.13 NIM(3) 游戏

问题:

假设有两堆石头,有两个玩家会根据如下的规则轮流取石头:

每人每次可以从两堆石头中各取出数量相等的石头,或者仅从一堆石头中取出

任意数量的石头;最后把剩下的石头一次拿光的人获胜。请问在哪些局面(依

据两堆石头中的石头个数)下,先取石头的玩家有必胜的策略。

解法:

类似构造质数的筛选方法,这里我们利用找到的必输局面(后取的玩家有必胜策略)

来筛去掉能通过一次操作达该必输局面的其它必胜局面(先取的玩家有必胜策略)。

最后选出的局面都是必输局面。

构造必胜策略:

如果一开始的局面就是必输局面,那么可能先取的玩家没有必胜策略(当然如果后取

的玩家不太聪明,先取的玩家依然有可能能赢)。如果一开始的局面不是必输局面,

那么先取的玩家一定有必胜策略,且必胜策略就是保证每次都将当前非必输局面转变

为必输局面(后取的玩家必输)。

参考:http://blog.csdn.net/linyunzju/article/details/7674596

1.15 构造数独

问题:

构造一个9*9的方格矩阵,玩家要在每个方格中,分别填上1至9的任意一个数字,
让整个棋盘每一列、每一行以及每一个3*3的小矩阵中的数字都不重复。

首先我们通过一个深度优先搜索来生成一个可行解,然后随机删除一定数量的数字,
以生成一个数独。

参考: http://blog.csdn.net/linyunzju/article/details/7673959

1.16 24点游戏

问题:

给玩家4张牌,每张牌的面值在1-13之间,允许其中有数值相同的牌,采用加、减、乘、除四则运算,允许中间运算存在小数,并且可以使用括号,但每张牌只能用一次。构造表达式,使其结果为24.

解法:

传统的枚举解法会产生大量重复的运算,主要有两类重复:运算结果的重复和排列的重复。假设4张牌为3 3 8 8,我们对3 3进行一次操作(6种运算)得到6 0 0 1 1 9,其中重复的数据就是我们所说的运算结果重复,使用集合不重复性来解决。枚举算法在枚举时要对牌的顺序进行排列,由于牌可以重复,所以产生的排列会有大量的重复(3 3) 8 8, (3 8) 3 8, (3 8) 3 8, (3 8) 3 8,(3 8) 3 8, (8 8) 3 3,这属于排列重复,使用分治法加memo来解决。采用二进制数来表达集合和子集的概念,我们可以用一个数来表示子集中拥有哪些元素,再用这个数作为索引来找出该集合运算后产生的结果集。

1.17 俄罗斯方块

问题:

让电脑自动下俄罗斯方块游戏。

解法:

对当前的积木块,枚举它旋转后的每一个形状从每一列落下的棋盘,将该棋盘和前一个棋盘进行对比,并打分,最后取得分最高的那个形状和那一列作为电脑的当前操作。(由于程序的输入数据比较多,我将其和代码打包放在资源下载中,需要的读者可以去下载http://download.csdn.net/detail/linyunzju/4389102,鉴于有读者不会用这个程序,在此说明下,运行这个程序需要加上文件重定向<1_17.txt,或者把程序中的控制台输入scanf改成文件输入FILE* fin=fopen("1_17.txt"); fscanf(fin,"%d",...);)

代码:

参考:

http://blog.csdn.net/linyunzju/article/details/7686822

posted @ 2012-11-07 10:12  Mr.Rico  阅读(251)  评论(0编辑  收藏  举报