回目錄

第五章 基礎資料型別

原 Google Sites 連結:/cppzero/di-wu-zhang-ji-chu-zi-liao-xing-bie

目錄

5.1 整數型態

5.1.1 無號整數

d984. 棄保效應

題目大意:給你三個整數代表 A, B, C 三個候選人的得票數 a, b, c,如果得票數最少的候選人的票數全部給得票數第二高的候選人的話,最後會是誰當選?

候選人 A 要贏得選舉的條件如下:

  1. a > b + c,這樣不管有沒有棄保效應,A 一定會當選。 (a>b+c)
  2. 如果 c > a > b,那麼就可以運作「棄 B 保 A」,此時若 a + b > c,A 就會當選。 (c>a && a>b && a+b>c)
  3. 如果 b > a > c,那麼就可以運作「棄 C 保 A」,此時若 a + c > b,A 就會當選。 (b>a && a>c && a+c>b)

再次提醒,數學上的 c > a > b 寫成程式時要寫成 c > a && a > b 而不是 c > a > b

把以上三個條件用「或」運算子連接起來,就是 A 當選的條件了:

a>b+c || c>a && a>b && a+b>c || b>a && a>c && a+c>b

(只要其中之一成立 A 即可當選)

B 當選的條件也可以類推,寫成程式如下:

//d984. 棄保效應 by snail
#include <iostream>
using namespace std;

int main () {
    int a, b, c;
    while (cin >> a >> b >> c) {
        if      (a>b+c || c>a && a>b && a+b>c || b>a && a>c && a+c>b)
            cout << "A\n";           // ↑棄 B 保 A              ↑棄 C 保 A
        else if (b>a+c || c>b && b>a && b+a>c || a>b && b>c && b+c>a)
            cout << "B\n";           // ↑棄 A 保 B              ↑棄 C 保 B
        else
            cout << "C\n";
    }
}

可是當你把這個程式上傳時,你卻得了個「WA (line:49)」。基本上這個程式的邏輯上並沒有錯,問題出在 int 的範圍上。

雖然題目說 a, b, c 的值會 ≤ 2147483647,基本上它們並沒有超過 int 的範圍,可是當其中的兩數相加時,其結果就有可能超出範圍了。這一題的第 49 筆測試資料是

2147483647 2147483646 2

當我們在判斷 a > b + c 時,其中的 b + c 為 2147483646 + 2,所得的結果為 2147483648,因為已經超出 int 的範圍了,所以它變成了 -2147483648,導致 a > b + c 本來應該是「false」的,現在卻變成了「true」了,本來應該是 B 當選的,結果就輸出 A 了。

要解決這個問題,我們需要一個可以處理比 2147483647 更大的值的資料型態。

2147483647 = 231 - 1,轉成二進位是 1111111111111111111111111111111,也就是 31 個 1。事實上 int 資料型態在電腦裡佔用 4 個位元組 (Bytes),也就是 32 個位元 (bits),理論上它的最大值應該是 32 個 1,也就是 232 - 1,但是系統留了最左邊的那的位元來表示正負號,這個位元稱為「符號位元」(sign bit),0 表示這個數字為正,1 則表示這個數字為負。

但是如果你的程式所處理的資料沒有負數,這個符號位元就不需要了,我們可以把全部的 32 個位元全部用來表示數字的大小,如此最大值就變成了 232 - 1了。要宣告這樣的變數,只要在 int 之前加上 unsigned (無號) 這個關鍵字就可以了。

也就是說,上面的程式只要把

int a, b, c;

這一行改成

unsigned int a, b, c;

就可以 AC 了。

a007. 判斷質數

題目大意:給你一個 ≧ 2 的整數 x,判斷 x 是不是質數。

質數的定義是:除了 1 和本身以外,沒有任何其他因數的整數。要判斷 x 是否質數,我們可以先用 2 去除,看看是否可以整除,如果不能整除的話,再用 3 去除除看,如果還不能整除,再用 4 去除除看……,以此類推,一直找到第一個整數為止。用這個方式可以找到 ≧ 2 的最小因數,如果這個因數就 = x,那麼 x 就是質數了。

#include <iostream>
using namespace std;

int main () {
    int x, d;
    while (cin >> x) {
        d = 2;
        while (x % d)
            d++;
        if (d == x)
            cout << "質數\n";
        else
            cout << "非質數\n";
    }
}

這個程式雖然可以判斷 x 是否為質數,但是上傳以後卻得了個「TLE (1s)」,意思是你的程式沒有辦法在所限定的 1 秒內跑出正確解答來。為什麼呢?

題目中說明 x 的最大值是 2147483647,我們就用這個值來輸入,在筆者的電腦上執行了大約 8.5 秒才輸出它是「質數」,但是題目的時間限制只有 1 秒。

由於 2147483647 是個質數,根據上面的程式,d 從 2 開始試著去除 x,迴圈要一直執行到 d = 2147483647 時才能整除 x,這時已經執行了 2147483646 次的除法,時間也早就超過了題目的限制。

其實,一個整數的因數是成對存在的。如果 d 是 x 的因數,那麼 x 也必有另一個因數為 x / d。而 d, x / d 這兩個因數一定有一個會 x\leq \sqrt x。因此我們如果在 x\leq \sqrt x 的整數中如果找不到 x 的因數,那麼 x 就是質數。

可是我們到目前為止還沒有學到如何去開根號,那這題要如何處理?開根號的函數會稍後的章節中提到,到時候這個題目也會再拿出來討論,但是這題即使沒有使用開根號的函數也仍然可以完成。

雖然我們目前沒有辦法去判斷 dxd \leq \sqrt x,但是如果我們把這個關係式的兩邊都平方,不就成了 d2xd^2 \leq x。根據這個關係式,我們把迴圈部份改寫如下:

d = 2;
while (x % d && d*d <= x)
    d++;

如此一來,跳出迴圈的條件有兩個:d 可以整除 x,或 d>xd > \sqrt x,只要其中一個情況出現了,迴圈便會中止。以 x = 2147483647 為例,當 d = 46341 時,d*d 就會 > x,迴圈也就會中止。由於迴圈最多只執行 4 萬多次,而不是原先的 21 億次,程式會變得很快!

但是接下來要判斷 x 是否是質數時,就不能再用 (d == x) 了,因為跳出迴圈時,d = 46341,而不是 2147483647。這時候可以改用 (d*d > x) 來作為判斷質數的條件。整個程式修改後如下:

//a007. 判斷質數 by Snail
#include <iostream>
using namespace std;

int main () {
    int x, d;
    while (cin >> x) {
        d = 2;
        while (x % d && d*d <= x)
            d++;
        if (d*d > x)
            cout << "質數\n";
        else
            cout << "非質數\n";
    }
}

當我們執行這個程式並輸入 2147483647 時,發現程式不但沒有變快,甚至答案也變成了「非質數」。

問題就發生在當所輸入的 x 是 int 的最大值時,只要 > x 的值就會溢位。觀察下表,當 d = 46340 時 dd 仍小於 x 的 2147483647;可是當 d = 46341 時,dd 就爆掉了而變成一個負數,而跳出迴圈的條件是 d*d > x,因此這個條件永遠不會成立,迴圈也不會在該中止的時候中止。

d d*d    
  理論值 實際值 結果
46340 2147951716 2147951716 d*d 仍 < x
46341 2147488281 -2147479015 爆掉了!


解決的辦法是,我們需要一個可以容納 2147488281 這個數字的資料型態。int 不行,但是 unsigned int (無號整數)可以。所以你只要把上列程式的 int 加上 unsigned 就可以 AC 了。

另外,我們可以利用 for 迴圈把 d 的變化範圍集中在一行,讓程式較具有可讀性:

//a007. 判斷質數 by Snail
#include <iostream>
using namespace std;

int main () {
    unsigned int x, d;                          // d(enominator) -- 除數
    while (cin >> x) {
        for (d=2; d*d<=x; d++)                  // d: 2 → √x
            if (x % d == 0)                     // 若 d 可整除 x
                break;                          //  跳出迴圈
        if (d*d > x)                            // 若 d > √x
            cout << "質數\n";
        else
            cout << "非質數\n";
    }
}

整數和無號整數在作運算時,如果沒有負數還好,一但有負數就會出現問題,例如以下範例:

int a=-12;
unsigned int b=12, c=12;
cout << a*b+c << endl;

正確結果應該是 -132
但程式跑出的結果然是 4294967164
這是由於進行運算時 (unsigned int)*(signed int) 結果會是 (usingned int),於是形成一個不容易察覺的 singned、unsigned 比較。由於程式在字面上看起來吻合撰寫者的邏輯,所以這個錯誤會非常難抓。如果將 b 和 c 強制轉換成 signed 的話就不會有這種問題了!

另外,有關於整數和無號整數的差別,有另一段程式碼可以解釋:

unsigned pos = 1024;
int neg = -5;
if (pos > neg) {
    cout << pos << " is bigger.\n";
} else {
    cout << neg << " is bigger.\n";
}

程式會告訴你 -5 比較大,因為當 signed 和 unsigned 一同做運算時,signed 會自動轉型成 unsigned,在 32 bit 機器上 -5 會變成 4294967291。

所以如果你不太會用 unsigned int 時,或是你怕會搞錯,請你就用 long long 吧!以下會介紹

5.1.2 Long Long 整數

b077. C. 不公平的人,是誰?

題目大意:輸入兩個正整數 M, N,如果 M > N 就輸出 “Fair”,否則輸出 “Unfair”。

這題看起來相當簡單,但是如果你仔細看看 M, N 的範圍,最大到 4611686018427387904,也就是 2622^62。這個值遠大於 int 的上限 2147483647,也就是 23112^31 - 1。就算是上一節中所教的 unsigned int 的 23212^32 - 1 也還是不夠大。

其實在 C++ 裡的整數型態 int 除了可以用 signed, unsigned 來指定要不要正負號以外,還可以用 short, long, 及 long long 來指定變數的大小。因此,整個整數型態的名稱分成三個部分,我們稱之為「符號部」「大小部」及「型態部」,詳如下表:

符號部 大小部 (位元組) 型態部
signed (有號) short (2)  
  long (4) int
unsigned (無號) long long (8)  

由於符號部有兩種選擇、大小部有三種選擇,組合起來之後,C++ 的 int 一共有六種變化如下:

signed short int
signed long int
signed long long int
unsigned short int
unsigned long int
unsigned long long int

上面的 6 個型態名稱是完整的寫法,看起來有點冗長。於是 C++ 也把比較常用的選項設為可以省略的內定值。

符號部 大小部 型態部
signed long int


「符號部」的內定值為 signed,因此如果符號部從缺時,系統便會視為有號整數。

「大小部」的內定值則是依硬體、作業系統、及編譯器而有所不同。筆者很久以前在 DOS 上用 Borland 公司的 Turbo C++ 寫程式時,這個部份如果省略的話,系統會視為 short,所宣告出來的變數只有 2 個位元組。更慘的是,在那個環境中,系統還不提供 long long 這個資料型態呢,遇到像 b077 這樣的題目就麻煩大了!

現在因為一般的電腦硬體、作業系統至少都是 32 位元,處理 32 位元的整數的效率和處理 16 位元整數的效率是一樣的,所以大多數的編譯器,包括 VC++ 2010 在內,如果你不指定大小的話,系統則內定為 long (4 個位元組),也就是 32 個位元。

至於「型態部」的 int,是當然的內定值,所有的情況它都可以省略,唯一的條件是其他兩個部份不可以同時都省略。(這是廢話,三個部份都省略不就什麼都沒有了!)

根據以上說明,其實我們在第一到四章中所習慣的 int 型態,完整的三部份寫法是「signed long int」,只是我們把「signed」及「long」省略掉罷了。事實上,它一共有「int」、「long」、「signed」、「long int」、「signed int」、「signed long」、「signed long int」等 7 種寫法,但它們所代表的意義完全相同!

綜上所述,我們把這六種整數型態的特質整理如下表:

完整名稱 最短名稱 位元 最小值 最大值
signed short int short 16 -32768 32767
unsigned short int unsigned short 16 0 65535
signed long int int 32 -2147483648 2147483647
unsigned long int unsigned 32 0 4294967295
signed long long int long long 64 -263 263 - 1
unsigned long long int unsigned long long 64 0 264 - 1


在「附錄 A 基礎資料型別」中列出了 C++ 語言所定義的型別,你可以在其中找到以上的六種資料型態。

(附註:263 - 1 = 9223372036854775807, 而 264 - 1 = 18446744073709551615)

對本題來說,long long 這個資料型別最大值為 263 - 1,足以處理這個題目的資料最大值 262。你可以把 M, N 宣告如下,再根據題意寫出程式就可以 AC 了。

long long m, n;

d127. 二、牧场面积

題目大意:給你矩形的周長,求最大面積。所給的周長一定為偶數,所圍的邊長必需是整數。

如果題目所給的周長保證是 4 的倍數,那麼這題就比較簡單了。但是題目只保證所給的周長是偶數,如果不是 4 的倍數的話,那麼這矩形的長就會比寬大 1。這個就有點小麻煩了,不過如果你夠聰明的話,也是可以用一個式子求出牧場面積的。

這個題目其實不難,之所以放在這一章才教,是因為它所給的周長會大於 4294967295,必需用 long long 才能 AC。

c005. 環保獎金

這個題目是從 UVa 的題庫出來的。它原來的總金總和是不會超出 int 的範圍的,但是 ZeroJudge 上的測試資料卻會使獎金總和超出 int 的上限,所以用來計算總和的那個變數要改用 long long。

5.2 浮點數資料型別

浮點數就是帶有小數的數字。

d051. 糟糕,我發燒了!

題目大意:輸入華氏溫度 f,換算成攝氏溫度後輸出。攝氏 = (華氏 - 32) * 5 / 9 ,直接按題意寫成程式如下:

#include <iostream>
using namespace std;

int main () {
    int f;
    cin >> f;
    cout << (f-32)*5/9;
}

但是,”/” 除完之後得到的是商,是一個整數,不是我們想要的答案。

那要讓它除出來的結果有小數要怎麼辦呢? 則在這個運算式裡必須帶有浮點數。

所以我們可以把 “5” 改成 “5.0” 或是直接打 “5.” 就可以了。

#include <iostream>
using namespace std;

int main () {
    int f;
    cin >> f;
    cout << (f-32)*5./9;
}

寫成這樣就差不多完成了,可是題目還有一個要求,只要取到小數以下第 3 位。
這時就要在 cout 你的答案之前,打上 << fixed << setprecision(3) 這樣他就會輸出至小數點後第三位。
要用 fixed setprecision() 之前,必須先 #include <iomanip>。

這樣這題就完成了:

#include <iostream>
#include <iomanip>
using namespace std;
int main () {
    int f;
    cin >> f;
    cout << fixed << setprecision(3) << (f-32)*5./9;
}

d055. 11437 - Triangle Fun

之前學到的數字的資料型別,像 int 或 long long ,都是 “整數” 的資料型別,那帶小數點的數字呢?

帶有小數的資料型別(浮點數)只有兩種:

名稱 位元 範圍
float 16 3.4E +/- 38 (7 digits)
double(long double) 32 1.7E +/- 308 (15 digits)

其實,PQR = ABC / 7,至於 ABC 的面積可以利用題目下面的提示所提供的公式去求,這樣就可以得到答案。但是這裡有一個小小的陷阱:

C++ 內建的 abs() 函數只能用來求整數的絕對值,如果你用浮點數代進去,它會先把小數部份無條件捨去並轉成整數型態後再取絕對值。

但是在 <cmath> 這個標頭檔中,包含了三個 abs() 的重載:

(待補)

x 的絕對值:abs(x)

#include <cmath>

(現在已改成 #include <stdlib>)

(補充:如果學過副程式,你也可以自己寫,寫法如下:

int abs (int a) {
    if (a<0)
        return -a;
    return a;
}

b227. F. 優惠方案Ⅱ

5.3 cmath 數學函數

用 C++ 進行開根號及平方

在一些程式語言中 (e.g. Visual Basic),如果要求一些數的二次方或三次方,可以直接用上引號 (e.g. 2^2=4) 表示,但很可惜的是 C++ 中並沒有這種功能,

這種時候,如果我們要對一個數二次方或三次方,除了使用 n*n*n 之外,還可以用 cmath 標頭檔裡的 pow 函數,格式如下:

22 的 5 次方: 22^5 = pow(22, 5)

22 的 0.5 次方: 22^0.5 = pow(22, 0.5) = sqrt(22)

a006. 一元二次方程式

//a006: 一元二次方程式 by Snail

#include <iostream>
#include <cmath>
using namespace std;

int main () {
   int a, b, c, d;
   while (cin >> a >> b >> c) {
      d = b*b - 4*a*c;                   // d(iscriminator) -- 判別式
      if (d < 0)
          cout << "No real root\n";
      else if (d == 0)
          cout << "Two same roots x=" << -b/(2*a) << endl;
       else {
          d = (int) sqrt ((double)d);
          cout << "Two different roots x1=" << (-b+d)/(2*a)
                                << " , x2=" << (-b-d)/(2*a) << endl;
      }
   }
}

b004: 繩子上吃草的牛

//b004. 繩子上吃草的牛 by Snail
#include <iostream>
#include <iomanip>      // for setprecision()
#include <cmath>        // for sqrt(), acos()
using namespace std;

int main () {
    double D, L, r1, r2, area, PI=acos(-1.);
    while (cin >> D >> L) {
        r1 = L / 2;                             // 長軸 /2
        r2 = sqrt (L*L - D*D) / 2;              // 短軸 /2
        area = PI * r1 * r2;                    // area-- 面積
        cout << fixed << setprecision(3) << area << endl;
    }
}

附註: 橢圓形面積 ==PI* 半長軸 * 半短軸 ==PIab

(圖片來源: Yahoo 知識 +)

a020. 身分證檢驗

//a020: 身分證檢驗 by Snail
#include <iostream>
using namespace std;

int main () {
     int id, cs, i;
     char c;
     while (cin >> c) {
           if (c == 'I')
                cs = 34;
           else if (c == 'O')
                cs = 35;
           else if (c == 'W')
                cs = 32;
           else if (c == 'Z')
                cs = 33;
           else {
                cs = c - 55;
                if (c > 'I')
                     cs--;
                if (c > 'O')
                     cs--;
                if (c > 'W')
                     cs--;
           }
           cs = cs%10 * 9 + cs / 10;
           cin >> id;
           cs += id % 10;
           id /= 10;
           for (i=1; i<=8; i++) {
                cs += id%10 * i;
                id /= 10;
           }
           if (cs % 10)
                cout << "fake\n";
           else
                cout << "real\n";
     }
}

c007: TeX Quotes

//c007. TeX Quotes.cpp (by Snail)
#include <iostream>
using namespace std;

int main () {
    int n = 1;
    char c;
    while (cin.get(c)) {
        if (c == '"')
            if (n++ % 2)
                cout << "``";
            else
                cout << "''";
        else
            cout << c;
    }
}

d658. 11636 - Hello World!

a024. 最大公因數(GCD)