要心中有「數」——C語言初學者代碼中的常見錯誤與瑕疵(8)

  在 C語言初學者代碼中的常見錯誤與瑕疵(7)  中,我給出的重構代碼中存在BUG。這個BUG是在飛鳥_Asuka網友指出「是否是時間複雜度比較大」,並說他「第一眼看到我就想把它當成一個數學問題來作」以後,我又從新對問題進行了數學式的思考後發現的。
  這個BUG源於在(1<=A,B<=1000)條件下對矩形個數的數量級內心沒數。當時以爲這個題目的目的是考察窮舉,因爲題目限定了A、B的範圍,因此結果應該不是很大。事實證實這種想法是一廂情願。
  一般狀況下,我不喜歡用數學方法解決C語言編程問題。由於不少問題,一旦在數學上可以輕易解決,編寫代碼每每是索然無趣的,對學習和練習C語言幾乎沒有什麼益處。譬如,求1+2+3+……+100,若是不知道或僞裝不知道數學解法,代碼多是這樣的html

#include <stdio.h>

int main( void )
{
  int i , sum = 0 ;
  
  for ( i = 1 ; i <=100 ; i ++ )
  {
     sum += i ;
  }
  printf("%d\n",sum);
  
  return 0;
}

  而若使用數學方法,代碼則是程序員

#include <stdio.h>

int main( void )
{
  printf("%d\n", (1 + 100) * 100 / 2 );
  
  return 0;
}

  前者,C語言和編程成分的含量很高,數學含量卻很低;然後者數學含量很高,可C語言和編程成分的含量幾乎爲0。
  從解決實際問題的角度來講,顯然應該用後一種方法;而從學習C語言和練習編程的角度來講,則應該使用前一種方法。因此一個好的編程問題,應該是沒有數學解的,至少應該沒有顯而易見、容易獲得的數學解。不然,只是用C語言對公式作一個簡單的翻譯(這大概是FORTRAN語言的哲學),編程的味道就全沒有了,譚浩強的不少題目就是如此(參見:濫用變量綜合症)。「矩形的個數」問題應該說還不是那麼容易獲得數學解的。
  因此,在飛鳥_Asuka 網友說他「第一眼看到我就想把它當成一個數學問題來作」以後,我也嘗試着用數學的方式思考了一下。我發現,在同一條直線上的n個不一樣的點一共能夠構成(n-1)n/2條不一樣的線段,A*B的矩形相鄰兩邊各有A+1和B+1個不一樣的點,於是能夠分別構成(A+1)A/2和(B+1)B/2條不一樣的線段,這樣構成的矩形的個數一共就是(A+1)A/2×(B+1)B/2個。當A和B取最大值1000時,結果顯然不小於25×1010,而這個值顯然大於231-1,甚至也大於232-1(多數編譯器中 unsigned 類型所能表示的最大整數)。這樣,原來的重構代碼中用int類型做爲結果的類型,顯然錯了。
  我一貫認爲C語言程序員應該心中有「數」,即對錶達式中的數據和結果有最起碼的範圍估計。沒想到,此次因爲刻意迴避數學解法,卻馬上遭到了違背信條的報應。正應驗了Muphry's law所言:Anything that can go wrong, will go wrong。
  馮諾依曼也認爲程序員至少應該清楚計算過程當中數據的數量級,爲此他反對浮點數。與之相反,約翰.巴科斯則盲目樂觀地發明了浮點數,不少程序員盡情地享受浮點數的方便,卻因爲盲目樂觀屢屢被浮點數這種「有缺陷的抽象」所傷。更有甚至,不少程序員連浮點數最基本的原理都不懂,居然能寫出k=sqrt(n) 這樣狗屁不通的句子。(參見:似是而非的k=sqrt(n)
  回過頭來再談我代碼的BUG。這個BUG的另外一個教訓是沒有進行比較充分的測試,若是測試一下邊界狀況可能不難發現這個BUG。後來我又從新測試了一下,發現程序運行時間比較長,這說明飛鳥_Asuka 網友指出「是否是時間複雜度比較大」的問題也是存在的。但當時爲了算法敘述的方便,就沒有按照下面的方法寫count()函數:算法

int count( int A , int B )
{
   int x1 , y1 ;//第一個點的座標 
   int x2 , y2 ;//第二個點的座標
   int num = 0 ;
   
   for ( x1 = 0 ; x1 < B ; x1 ++ )//   for ( x1 = 0 ; x1 <= B ; x1 ++ )
      for ( y1 = 0 ; y1 < A ; y1 ++ )//     for ( y1 = 0 ; y1 <= A ; y1 ++ )//窮舉第一個點的各類可能 
         for ( x2 = x1 + 1 ; x2 <= B ; x2 ++ )//    for ( x2 = 0 ; x2 <= B ; x2 ++ )
            for ( y2 = y1 + 1 ; y2 <= A ; y2 ++ )//     for ( y2 = 0 ; y2 <= A ; y2 ++ )//窮舉第二個點的各類可能 
                  num ++ ;                //            {
                                          //               if ( x1 < x2 && y1 < y2 )
                                          //                   num ++ ;
                                          //            }
   return num ;
}

  若是寫成這種形式,不難發現第二層循環與第三層是能夠對調的,對調後爲:編程

   for ( x1 = 0 ; x1 < B ; x1 ++ )
      for ( x2 = x1 + 1 ; x2 <= B ; x2 ++ )
         for ( y1 = 0 ; y1 < A ; y1 ++ )
            for ( y2 = y1 + 1 ; y2 <= A ; y2 ++ )
                  num ++ ;

  這時應該可以看出,最內兩層循環次數爲 A + A-1 + A-2 +……+1,最外兩層循環的循環次數爲B + B-1 + B-2 +……+1,於是結果能夠直接獲得,即(A+1)A/2*(B+1)B/2。數組

  算法問題解決了,又產生了新的問題,那就是如何表示這麼大的整數,這是一個數據結構的問題。(或許,這纔是題目的本意?)
  辦法之一就是使用表示範圍更大的整數類型,例如C99中的long long int類型。
  若是編譯器不支持C99也沒有表示範圍更大的整數類型,那就只有本身着手構造新的數據結構了。
  矩形個數的最大值大約爲25×1010,這並非一個很大的數,一個int不夠,那就用兩個好了。這裏把存儲大數的數據結構設計爲一數組,數據結構

typedef int BIG_NUM[2] ;

  數組的第0個元素存儲低6位,第1個元素存儲高位。存儲低6位的緣由是避免乘法運算時溢出(乘數不超過1001,與一個6十進制整數相乘不超過109,在int類型的表示範圍以內)。函數

  經過調用post

BIG_NUM num ;
count( num , A , B );

將結果寫到num中。學習

count()函數的實現:測試

void count( BIG_NUM m , int A , int B )
{
   m[0] = 1 ;
   m[1] = 0 ;
   mul_sum( m , A );//將1+2+……+A的結果乘入m 
   mul_sum( m , B );//將1+2+……+B的結果乘入m
}

  因爲結果是累乘獲得的,因此初始化爲1。爲防止溢出,只能

void mul_sum( BIG_NUM m , int n )
{
   int t1 = n % 2 == 0 ? n / 2 : n , 
       t2 = n % 2 != 0 ? (n + 1) / 2 : n + 1 ;
   mul( m , t1 );
   mul( m , t2 );
}

  當心翼翼地一個個地乘(每一個乘數都不得超過1001)。t1,t2這兩個變量是爲了迴避BIG_NUM類型的除法運算。

void mul( BIG_NUM m , int n )
{   
   m[0] *= n ;
   m[1] *= n ;
   m[1] += m[0]/1000000;//進位 
   m[0] %= 1000000;
}

  每乘以一個數,馬上就處理進位問題。
  最後還要考慮如何輸出:

void output( BIG_NUM m )
{
   if ( m[1] == 0 )
   {
      printf( "%d\n" , m[0] );
      return ;
   }
   
   printf( "%d" , m[1] );
   printf( "%06d\n" , m[0] );
   
}

  這裏的"%06"是爲了保證低位不夠6位時仍能正確輸出。

  完整的代碼以下:

/*
矩形的個數 
在一個3*2的矩形中,能夠找到6個1*1的矩形,4個2*1的矩形3個1*2的矩形,
2個2*2的矩形,2個3*1的矩形和1個3*2的矩形,總共18個矩形。 
給出A,B,計算能夠從中找到多少個矩形。 

輸入: 
本題有多組輸入數據(<10000),你必須處理到EOF爲止 
輸入2個整數A,B(1<=A,B<=1000) 

輸出: 
輸出找到的矩形數。 

樣例:

輸入: 
1 2 
3 2 

輸出: 
3 
18

做者:薛非
出處:http://www.cnblogs.com/pmer/   「C語言初學者代碼中的常見錯誤與瑕疵」系列博文 

*/

#include <stdio.h>
typedef
int BIG_NUM[2] ; void count( BIG_NUM , int , int ); void mul_sum( BIG_NUM , int ); void mul( BIG_NUM , int ); void output( BIG_NUM ); #define POW 1000000 #define WID 6 int main( void ) { int A , B ; while ( printf( "輸入2個整數A,B(1<=A,B<=1000)\n" ), scanf( "%d%d" , &A , &B )!= EOF ) { BIG_NUM num ; count( num , A , B ); output( num ); }   return 0; } void output( BIG_NUM m ) { if ( m[1] == 0 ) { printf( "%d\n" , m[0] ); return ; } printf( "%d" , m[1] ); printf( "%0*d\n" , WID , m[0] ); } void mul( BIG_NUM m , int n ) { m[0] *= n ; m[1] *= n ; m[1] += m[0]/POW;//進位 m[0] %= POW; } void count( BIG_NUM m , int A , int B ) { m[0] = 1 ; m[1] = 0 ; mul_sum( m , A );//將1+2+……+A的結果乘入m mul_sum( m , B );//將1+2+……+B的結果乘入m } void mul_sum( BIG_NUM m , int n ) { int t1 = n % 2 == 0 ? n / 2 : n , t2 = n % 2 != 0 ? (n + 1) / 2 : n + 1 ; mul( m , t1 ); mul( m , t2 ); }

  寫的有點醜。

相關文章
相關標籤/搜索