将现实问题转换为编程问题需要转换思维,不过孰能生巧,见多了就自然懂如何做了,所以动起手来是决没错的。
1.猜名次问题
每个选手都说了两句话,而只有一句话是对的
如果把 两句话都看成两个判断句, 如果为真则结果为1 如果为假则结果为0
所有两个判断式相加 等于1则表示只有一个判断句是真另外一个是假比如A选手说对一般表示:(b= =2)+(a= =3)==1
把所有的可能性穷举出来,a可能是第1第2第……5,b可能是第1第2第……5,c可能是……,嵌套就可以了。
思路:
考虑到一共五个人,直接模拟推理有些太难,计算机最擅长的遍历此时就会派上用场,将每个人从第1到第5来一遍,则一共会产生5^5种可能性,这个只需要一个5层循环即可搞定。但是这样会导致一些不期望出现的结果出现,因为并没有查重,所以会出现两个人抢名次的情况,也就是两个人或者更多的人名次相同的情况,例如两个第二,三个第三这样的,所以即使满足了条件,也要查看一下五个人的名次是否重复,这个交给一个函数来执行,只要五个人名次并列,那就返回0,否则返回1即可。有了这个思路,就能完成以下代码。
#include <stdio.h> int checkData(int* p) { int tmp[7] = { 0 }; //标记表,实际是哈希表的思路。一开始每个元素都是0 //用来表示名次是否有人了,如果p[0](a选手)的名次是3,则tmp[3]=1,表示第3名已经有人了 //如果p[1](b选手)的名次也是3,则进入if()语句tmp[3]==1, //直接返回0,这组成绩作废,再判断下组成绩。 //直到没有重复的 才可以才能返回1 int i; for (i = 0; i < 5; i++) { if (tmp[p[i]]==1) //如果这个位置的标记已经是1,则代表重复,直接返回0。 { return 0; } tmp[p[i]] = 1; //如果不是,则给这个位置标记为1。 } return 1; //全部标记完毕也没有出现重复的情况,代表OK。 } int main() { int p[5]; //0 1 2 3 4分别代表a b c d e for (p[0] = 1; p[0] <= 5; p[0]++) { for (p[1] = 1; p[1] <= 5; p[1]++) { for (p[2] = 1; p[2] <= 5; p[2]++) { for (p[3] = 1; p[3] <= 5; p[3]++) { for (p[4] = 1; p[4] <= 5; p[4]++) //五层循环遍历 { //这里是五个人的描述,由于比较表达式只有0和1两个结果,如果要两个条件有且只有一个为真,则可以用比较表达式的值总和为1的方式直接判定。别忘了还要判定不能并列。 if ((p[1] == 2) + (p[0] == 3) == 1 && //B第二,我第三 (p[1] == 2) + (p[4] == 4) == 1 && //我第二,E第四 (p[2] == 1) + (p[3] == 2) == 1 && //我第一,D第二 (p[2] == 5) + (p[3] == 3) == 1 && //C最后,我第三 (p[4] == 4) + (p[0] == 1) == 1 && //我第四,A第一 checkData(p) //不能并列||,如果checkData返回0,则整体都为假,如果返回1则为真 ) { for (int i = 0; i < 5; i++) { printf("%d ", p[i]); } putchar('\n'); } } } } } } return 0; }
有的人可能不太懂为什么要查重复,因为5^5把全部的可能性列举出来,每个人都可能会相同,而题目给的也不是很严谨,所有会导致出现相同名次的情况比如把查重函数拿走就会出现这个现象:
会出现抢名次的现象,所以我们需要进行查重。
查重后结果:
改进一:
检查是否重复的过程,我们是用一个数组来做的,实际每个标签只有0和1两种可能,
没必要一定要用数组做,可以考虑用一个位来做(哈希中的位图)
代码如下:
int checkData(int *p) { char tmp = 0; int i; for (i = 0; i < 5; i++) { tmp |= 1 << p[i]; //tmp每次或上一位1,p[i]如果是1~5都有,则1<<1到1<<5都或上的结果将会是00111110,如果有并列,则一定会至少却其中一个1,结果就不会是00111110,所以可以判断tmp最终的结果是不是这个数字来判断有没有重复。 } return tmp == 0x3E; }
改进二:
循环代码又长又难看,可以考虑改成递归:
void diveRank(int * p, int n) { if(n >= 5) //此时的n是用来控制循环层数的。 { if ((p[1] == 2) + (p[0] == 3) == 1 && //B第二,我第三 (p[1] == 2) + (p[4] == 4) == 1 && //我第二,E第四 (p[2] == 1) + (p[3] == 2) == 1 && //我第一,D第二 (p[2] == 5) + (p[3] == 3) == 1 && //C最后,我第三 (p[4] == 4) + (p[0] == 1) == 1 && //我第四,A第一 checkData(p)) //查重 { for (int i = 0; i < 5; i++) { printf("%d ", p[i]); } putchar('\n'); } return; } for(p[n] = 1; p[n] <= 5; p[n]++) { diveRank(p, n + 1); //通过递归模拟多层循环,每进一次递归相当于进了一层新的循环。 } } int main() { int p[5]; diveRank(p, 0); return 0; }
改进三:
以上的方法只是让代码简单了点,但还是需要5 ^ 5次比较,而如果本来就是做1 ~ 5的排列组合的话只需要5!次比较,能极大的减少遍历所需的次数(复杂度由O(n^n)降低为O(n!)),那是不是可以用一个递归完成对1~5的全排列呢?当然是可以的,所以我们可以进一步优化遍历的方式,将遍历用的递归程序改成这样:
#include <stdio.h> void swapArgs(int * a, int * b) //交换函数 { int tmp; tmp = *a; *a = *b; *b = tmp; } void diveRank(int * p, int n) { if(n >= 5) //此时的n也是用来控制循环层数的。 { if ((p[1] == 2) + (p[0] == 3) == 1 && //B第二,我第三 (p[1] == 2) + (p[4] == 4) == 1 && //我第二,E第四 (p[2] == 1) + (p[3] == 2) == 1 && //我第一,D第二 (p[2] == 5) + (p[3] == 3) == 1 && //C最后,我第三 (p[4] == 4) + (p[0] == 1) == 1) //我第四,A第一 //由于此时是执行的全排列,所以查重也省了。 { for (int i = 0; i < 5; i++) { printf("%d ", p[i]); } putchar('\n'); } return; } int i; for(i = n; i < 5; i++) //这个递归方式就完成了对1~5的全排列,方法是从后向前不停的执行交换。可以参考改进二和原代码,将这个递归程序写回成循环后,可以更好的理解。 { swapArgs(p + i, p + n); diveRank(p, n + 1); swapArgs(p + i, p + n); } } int main() { int p[5] = { 1, 2, 3, 4, 5 }; //当然由于是全排列,所以初值必须给好。 diveRank(p, 0); return 0; }
至此,遍历速度上达到了一个新的高度,这种遍历大大减少了遍历的次数,极大的提升了效率。
2.猜凶手问题
这个还是将简单的现实问题转换为编程问题,跟上面的题目一样,问题就是如何把这几个人说的话转换为编程
四个嫌疑犯,3个人说了真话,1个人说了假话,那把这四个人的话看成4个表达式(判断式),如果为真则结果为1,如果为
假结果为0,所以只要让4个表达式相加等于3就可以了。
思路:
这个题就是按照正常方式,假设凶手是a,b,c,d其中的一个,看是不是满足题目条件,如果满足就找出了凶手。
int main() { char killer = 0; //分别假设凶手是a,b,c,d,看谁是凶手是符合三个人说真话,一个人 //说假话 for (killer = 'a'; killer <= 'd'; killer++) { if (('a' != killer) + (killer == 'c') + (killer == 'd') + (killer != 'd')==3) { printf("凶手是:%c", killer); } } return 0; }
总结:
这种问题可能刚做不适应,不太好想,不过接触多了就知道怎么回事了,将所说的看成表达式,根据真为1,假为0,来解释
多刷题,就能接触更多的有趣的题类型。