Perlis教授折磨腦子的家庭作業

這是<>附錄中的一個小節。如下:
/////////////////////////////////////////////////////////////////////////////////////
  有些研究生學校也使用編程問題來測試它們的新生。在耶魯大學,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教授當初讓不讓他的研究生上網或去圖書館查資料,如果是的話呵呵還是挺有成就感滴。 



Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1147878

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章