Perlis教授折磨脑子的家庭作业

    
  这是<<C专家编程>>附录中的一个小节。如下:
/
  有些研究生学校也使用编程问题来测试它们的新生。在耶鲁大学,Alan Perlis教授(Algol-60)的创始人之一)曾用下面的作业(要求一星期内完成)测试他刚入学的研究生。

为下列各个问题编写程序:
1:读取一个字符串,并输出它里面字符的所有组合。
2:“八皇后”问题。
3:给定一个数N,要求列出所有不大于N的素数。
4:编写一个程序,进行两个任意大小的矩阵乘法运算。
  研究生们可以使用下面语言之一:
C,APL,Lisp,Fortran
  上述几个编程问题作为研究生的作业,让每位学生接受一项任务还算比较合理的。但现在我们被要求在一个星期内完成所有的任务,我们中的有些人甚至从来没有用过上述四种语言。
  当然,我们并不知道Perlis教授实际上只是想考考我们,事实上他并不打算捉弄任何一个人。绝大部分新研究生都度过了疯狂的一周,时至深夜依然蜷在电脑终端时,只是为了完成这些折磨脑子的任务。回到班上以后,教授要求自愿者在黑板上演示单个语言/问题的组合方案:
  有些问题可以被 一些习惯用法解决,比如问题3可以用一行APL代码解决:
(2=+.0=T<=.|T)/T<-N
  这样,如果谁完成 了这些作业的一部分, 都有机会进行展示。那些被问题所难倒,哪怕一小部分也没有完成的人会意味着他们可能并不适合读这个研究生。这是疯狂且巨忙的一周,我在这段时间里学习到的APL或Lisp的知识比以前几年以及以后几年里加起来学到的还要多。
//


  下面是我的解答:
1:读取一个字符串,并输出它里面字符的所有组合。

  这个题目,虽然题目中有组合二字,实则是个P(N,N)的排列问题,例如已知字符串为abc,那么它的所有组合应该就是:
abc,
acb,
bac,
bca,
cab,
cba;
  一看这个题目,就想-呵呵,这个应该比较简单,因为高中时就学过排列组合的,然后再一想就觉得-这个其实不好做,因为以前只是算排列组合的个数有多少,并不要列举出所有的组合来。然后我自己想办法,想了一会没什么可行的思路,根据“不要重新造轮子”的原则,就去图书馆继承了下前人的智慧结晶,学到了如下的算法:
首先我们确定如何按字典序对形如{1,2,3...N}的集合列出所有排列(其实不按字典序来还真不好做),字典序就是比如以1234为全部元素的情况下:1243<1324;1324<1342并且这儿要求每个串中的元素不能重复出现。(在数字的情况下,字典序与大小关系一致),又如如果以123456为全部集合,要求一个164253的后继元素是多少,应该是164325,根据一个数求出其后继元素的算法可以得出,然后依据该算法,我们从第一个元素123...N开始逐个求后继元素,最终就会得到所有的排列形式了。总的算法如下:

输入:N;
输出:按字典序的所有排列
算法:
int test[N]={1,2,3...N};
for(int j=0;j<N;j++)  //输出第一个串
{
 printf("%d",test[j]);
}
for(int i=0;i<N!;i++) //所有的排列方式数为N!种
{
 int m=N-1;
 while(test[m]>test[m+1])//从倒数第二个元素开始,从右往左找到第一个小于其后元素的元素,用m下标之
 {
  m--;
 }
 int k=N;
 while(test[m]>test[k])//从最后往左找,直到找到第一个大于上一步中找到元素的元素,用k下标之
 {
  k--;
 }
 swap(test[m],test[k]);//交换前面两步找到的两个元素之值
 int p=m+1;
 int q=N;
 while(p<q)    //将m下标的元素后面部分做逆序,因为它是后面几个元素的最大排列方式,要让它成为最小
 {
  swap(test[p],test[q]);
  p++;
  q--;
 }
 for(int a=0;a<N;a++)//输出本次所得后继排列
 {
  printf("%d",test[a]);
 }
}
END///

  有了这个算法,有一个存储在大小为N的数组的字符串,我们只需以0至N-1为全部元素,从0,1,2...N-1到N-1,N-2...1,0做如上排序,而这个排序正好可以做为数组索引来输出各个字符,就得到了所有字符的组合了。
我写的完整的实验代码如下:

#include<stdio.h>
#include<string.h>

int  length=0;     //length of input string
void myswap(int *,int *);
int  getfactorial(int N); //get factorial of length
void combin(char str[]);

int main()
{
 char str[256]={0};
 printf("Please input your string:");
 scanf("%s",str);
 length=strlen(str);
 combin(str);
 return 0;
}

void combin(char str[])
{
 int index[50]; //it limits program can only deal with string which has 50 chars at most
 int i=0;
 int Nfactorial=getfactorial(length);
 FILE * fp;
 if((fp=fopen("test.txt","w"))==NULL)
 {
  printf("Create file failed!");
  return;
 }
 if(length>50)
 {
  printf("This program can noly deal with 50 chars at most!");
  return;
 }

 for(i=0;i<50;i++)
 {
  index[i]=i;
 }
 for(i=0;i<length;i++)
 {
  printf("%c",str[i]);
  fputc(str[i],fp);
 }
 printf("/n");
 fputc('/n',fp);
 for(i=0;i<Nfactorial-2;i++)
 {
  int tmp;
  int m;
  int k; 
  int p;
  int q; 
  m=(length-1)-1;
  while(index[m]>index[m+1])
  {
   m--;
  }
  k=length-1;
  while(index[m]>index[k])
  {
   k--;
  }
  myswap(&(index[m]),&(index[k]));
  p=m+1;
  q=length-1;
  while(p<q)
  {
   myswap(&(index[p]),&(index[q]));
   p++;
   q--;
  }
  for(tmp=0;tmp<length;tmp++)
  {
   printf("%c",str[index[tmp]]);
   fputc(str[index[tmp]],fp);
  }
  printf("/n");
  fputc('/n',fp);
 }
 fclose(fp);
}

void myswap(int * a,int *b)
{
 int tmp=*a;
 *a=*b;
 *b=tmp;
}

int getfactorial(int N)
{
 if(N<=1) return 1;
 return N*getfactorial(N-1);
}

 

2:"八皇后”问题
  这是如此经典的一个问题,以至于网上到处充斥着各种语言实现的源代码,不过算法就那么一种-递归(还有堆栈实现,我认为堆栈是手工对递归方法的模拟,所以不单独列为一种)。
关于这个问题的历史,网上有两种说法:
1:“在程式語言教學中,訓練學習者分析問題的能力相當重要。很多有趣的範例程式來自數學家
的研究,以八皇后為例,Franz Nauck在1850年提出「在西洋棋的棋盤上放八個皇后,使得
沒有一個王后能吃掉其他皇后」。西洋棋的皇后如同象棋的車,可以縱、橫、斜向移動,把
8個皇后排在8乘以8的棋盤上,卻不使彼此攻伐的排法有幾種?數學家高斯猜測有96個解,
實際上只有92個形式解,本質解有12個。本質解是無法經由某一個解旋轉或反射得到的解。
八皇后問題可以擴展成N皇后,藉由程式語言的幫助將易於求解。”
2:“八皇后问题是一个古老而著名的问题,是回溯算法的典型例题。该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
  高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。” 
至于两种说法谁是对的如我泛泛之辈绝无资格妄言,也没必要关心。总之知道这个问题是典型的回溯算法的代表问题就可以了,其实这样的问题以前是碰到过的,骑士遍历问题就是一例。

我认为清晰可读的一份代码是:
#include <stdio.h>

char Queen[8][8];   //模拟棋盘
int  a[8];    //按行置皇后,不会在同一行上置两个,故只做列冲突记录
int  b[15];    //主对角线冲突记录,由方格构成的主对角线共有15条,左上至右下为主
int  c[15];    //从对角线冲突记录,由方格构成的主对角线共有15条,右上至左下为次
int  iQueenNum=0;   //记录求得的棋盘状态数

void init();
void queen(int i);   //参数i代表行

int main()
{
 init();
 queen(0);
 return 0;
}

void init()
{
 int iRow,iColumn;        
 for(iRow=0;iRow<8;iRow++)  //棋盘初始化,空格为*,放置皇后的地方为@
 {
  a[iRow]=0;     //列标记初始化,0表示无列冲突
  for(iColumn=0;iColumn<8;iColumn++)
   Queen[iRow][iColumn]='*';
 }  
 for(iRow=0;iRow<15;iRow++)  //主、从对角线标记初始化,表示没有冲突
  b[iRow]=c[iRow]=0;
}

void queen(int i)
{
 int iColumn;
 for(iColumn=0;iColumn<8;iColumn++)
 {
  if(a[iColumn]==0 && b[i-iColumn+7]==0 && c[i+iColumn]==0)//如果无冲突
  {
   Queen[i][iColumn]='@';    //放皇后
   a[iColumn]=1;     //标记,下一次该列上不能放皇后
   b[i-iColumn+7]=1;    //标记,下一次该主对角线上不能放皇后后
   c[i+iColumn]=1;     //标记,下一次该从对角线上不能放皇

   if(i<7) queen(i+1);    //如果行还没有遍历完,进入下一行

   else      //否则输出
   {   
    int Row,Column;    
    printf("第%d种状态为:/n",++iQueenNum);
    for(Row=0;Row<8;Row++)  //输出棋盘状态
    {
     for(Column=0;Column<8;Column++)
      printf("%c ",Queen[Row][Column]);
     printf("/n");
    }
    printf("/n/n");
   }
   //如果前次的皇后放置导致下面的一行无论如何放置都不能满足要求,则回溯,重置
   Queen[i][iColumn]='*';
   a[iColumn]=0;
   b[i-iColumn+7]=0;
   c[i+iColumn]=0;
  }
 }
}

3:给定一个数N,要求列出所有不大于N的素数。
关于这个问题我个人已知的有两种思路:第一种就是从2开始,一个一个判断,每个数的判断方法为首先不能是偶数,其次不能被小于它的所有数整除。其实只需判断到不能被该数平方根整除即可。下面是我刚学编程时用C++写的很笨的代码:
#include<iostream.h>
#include<math.h>

void doprime(int,int);

void main()
{
 int i_min,i_max,temp;
 cout<<"你要求**到**之间的素数,请输入第一个数:";
 cin>>i_min;
 cout<<"请输入第二个数:";
 cin>>i_max;
 cout<<endl;
 if(i_min>i_max)
 {
  temp=i_min;
  i_min=i_max;
  i_max=temp;
 }
 doprime(i_min,i_max);
}

void doprime(int i_min,int i_max)
{
 if(i_min%2==0)
 {
  i_min++;
 }
 int n=0;
 for(;i_min<=i_max;i_min+=2)
 {
         for(int j=3;j<i_min;j+=2)
    {
   if(i_min%j==0)
   {
    break;
   }
     }  
    if(j==i_min)
     {   
     if(n%6==0)
     {
        cout<<endl;
     }
     n++;         
     cout<<"  "<<i_min;
     }
 }
}
这种做法可以称之为一种直接法,前几天在看数据结构介绍数组的索引作用时学到了另一种解法,可称之为间接法,下面是当时做的笔记:
/
例一:求所有小于N的素数
这个程序的功能是:如果自然数i是素数,就把a[i]的值变为1,否则变为0。首先所有的数组元素都设置为1,以表明没有任何数已被证明是非素数,然后把被证明是合数(已知素数的倍数)的数作为访问数组元素的索引,设对应的a[i]为0。如果所有小于api[的素数的倍数所对应的项都变为0以后,a[i]依然是1,那我们可以知道它也是素数。
  因为本程序调用一个由最简单的元素类型--0-1开关类型组成的数组,所以直接使用由位构成的数组会比整数数组更节省空间。另外,有些情况下需要动态分配数组占用的存储空间。

#include <iostream.h>
static const int N=10000;
int main()
{
 int num=0;
 int i,a[N];
 for(i=2;i<N;i++) a[i]=1;
 for(i=2;i<N;i++)
 {
  if(a[i])
  {
   for(int j=i;j*i<N;j++) a[j*i]=0;
  }
 }
 for(i=2;i<N;i++)
 {
  if(a[i])
  {
   cout<<" "<<i;
   num++;
  }
 }
 cout<<endl;
 cout<<"There are:"<<num<<"primer numbers"<<endl;
 return 0;
}

我现在想做一个这两种算法的效率比较,但是精确统计程序运行时间的方法我还没有掌握,不过这几天正在看<<深入理解计算机系统>>其中第9章详细介绍了这方面的内容,等我看到那儿了回来再补做这一比较。如果有人认为还有更好的算法,请不要小气,把它告诉我。   

4:编写一个程序,进行两个任意大小的矩阵乘法运算;
  这个题目真是搞不懂,进行两个任意大小的矩阵乘法?任意大小的两个矩阵怎么能相乘呢?相乘不是需前一个矩阵的列数等于后一个矩阵的行数吗?我想这一步的检查应该也是包含在程序中的。
  这个程序中要输入矩阵,如果用printf("%d",matrix[i][j]);那么每输入一个数字要按一下回车键,并且回车键要被当做下一个字符,所以不成,要用getche()挨个输入每位数字,然后合并之,其次,我还发现:getche()接受到一个回车键并不自动换到下一行,而是回到了本行行首,这时如果你继续输入数字,那么就会将上一次的输入覆盖,所以这时你要手动输出一个printf("/n");才行,下面是我的代码:
 
#include<stdio.h>
#include<stdlib.h>
#include<ctype.h>

struct matrix
{
 int matrix[10][10];
 int row;
 int column;
};

struct matrix inputmatrix();
struct matrix calculate(struct matrix faciend,struct matrix ier);
void outputmatrix(struct matrix product);

void main()
{
 struct matrix matrix1,matrix2,product;
 printf("Insert space between two numbers,and enter when finish a line of data!/nAnd finish whole matrix with double enter!/n"); 
 printf("Please input your first matix:/n");
 matrix1=inputmatrix();
 printf("Please input your second matix:/n");
 matrix2=inputmatrix();
 product=calculate(matrix1,matrix2);
 printf("The product is :/n");
 outputmatrix(product);
}

struct matrix inputmatrix()
{
 struct matrix tmpmatrix;
 int i,j,num,tmp,flag=0;
 tmpmatrix.row=0;
 tmpmatrix.column=0;
 for(i=0;1;i++)
 {
  num=0;
  tmp=getche();
  for(j=0;tmp!=0x0D;)    //0x0D is ASCII of 'enter'
  {
   if(tmp==' ')
   {
    tmpmatrix.matrix[i][j]=num;
    j++;
    tmp=getche();
    num=0;
    continue;
   }
   num=num*10+(tmp-'0');
   flag=0;   
   tmp=getche();
  }
  if(num!=0)
  {
   tmpmatrix.matrix[i][j]=num;
   j++;
  }
  printf("/n");     //because getche() doesn't start a new line
  if(flag==1) break; 
  if(tmpmatrix.column!=0)
  {
   if(j!=tmpmatrix.column)
   {
    printf("Input error!");
    exit(-1);
   }
  }
  else
  {
   tmpmatrix.column=j;
  }  
  flag=1;
 }
 tmpmatrix.row=i;
 printf("Matrix input success!/n");
 return tmpmatrix;
}

struct matrix calculate(struct matrix faciend,struct matrix ier)
{
 int i,a,j,tmpresult=0;
 struct matrix product;
 if(faciend.column!=ier.row)
 {
  printf("These two matrix cannot do multiply!/n");
  exit(-2);
 }
 product.row=faciend.row;
 product.column=ier.column;
 for(i=0;i<faciend.row;i++)
 {  
  for(a=0;a<ier.column;a++)
  {
   tmpresult=0;
   for(j=0;j<faciend.column;j++)
   {
    tmpresult+=faciend.matrix[i][j]*ier.matrix[j][a];
   }
   product.matrix[i][a]=tmpresult;
  }
 }
 return product;
}

void outputmatrix(struct matrix product)
{
 int i,j;
 for(i=0;i<product.row;i++)
 {
  for(j=0;j<product.column;j++)
  {
   printf("%d ",product.matrix[i][j]);
  }
  printf("/n");
 }
}

 

  这次做了这4道题,一共花了4天时间吧,平均每天做4,5个小时的样子,不知道Perlis教授当初让不让他的研究生上网或去图书馆查资料,如果是的话呵呵还是挺有成就感滴。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值