---
# System prepended metadata

title: 【C++】競程筆記（數學&程式設計）
tags: [程式設計, C++, 競程, 演算法, CP]

---

【C++】競程筆記（數學&程式設計）
===

[TOC]

程式碼範例參考：NTUCPC Guide，此筆記僅為個人學習用途。

---

質數（prime numbers）與因數（factor）
---

因數定義：「 $y$ 是 $x$ 的因數代表 $x$ 除以 $y$ 為整數」。

在程式設計上則以 `x % y == 0` 表示。

```cpp=
// from NTUCPC Guide
for (int y = 1; y <= x; y++) {
    if (x % y == 0) {
        // y 是 x 的因數
    }
}
```

這樣 for 迴圈的遍歷方式，時間複雜度為 $O(x)$ ，因為有 x 個元素要判斷。

而在我們正常尋找因數的方法，如下：

 $12 = 1 \times 12 = 2 \times 6 = 3 \times 4 = 4 \times 3 = 6 \times 2 = 12 \times 1$
 
可以發現，當因數 $>3$ 時，後面的因數與前面完全一致，我們到 $3$ 的時候就不會繼續找因數了。

為了要優化上面的代碼，所以可以透過這一個特性，避免重複計算，進而提升速度：

```cpp=
// from NTUCPC Guide
for (int y = 1; y * y <= x; y++) {
    if (x % y == 0) { // x = y * (x / y)
        // y 是 x 的因數
        // x / y 是 x 的因數
        // y 跟 x / y 可能是相同的，如果不能重複處理的話要特別注意
    }
}
```

因為只要滿足 $y \leq \frac{x}{y}$ ， $y^{2} \leq x$ 這個條件即可。

那這樣就可以將時間複雜度降為 $O(\sqrt x)$ 了。

> 不要把 `y * y <= x` 寫成 `y <= sqrt(x)`，因為 `sqrt(x)` 的結果是浮點數，可能會有浮點數誤差。

而至於質數部分，具體程式碼實現如下：

```cpp=
// from NTUCPC Guide
bool is_prime(int x) {
    for (int y = 2; y * y <= x; y++) { // 起始值是 2
        if (x % y == 0) { // x = y * (x / y)
            return false;
        }
    }
    return true;
}
```

大數四則運算
---

因為 C++ 的資料型態都設有一定的範圍限制，所以在做一些「大數」運算的時候，難免會出現 overflow 的問題，因此就要自己手刻運算程式。

### 儲存大數
---

利用陣列儲存每一個數字的位數：

```cpp=
int a[len] = {1,2,3,4,5,6,7,8,9}; // 123456789
int b[len] = {3,2,7,1,2,6,5,4,9,0,9,0,0,1,1,2,2,3,4,2,7,5}; // 3271265490900112234275
```

### 加法
---

兩數相加時會用直式去計算，直式就是從最右邊開始用每一位相加、進位的計算。

如果兩數的某位數相加起來超過 10，就進位 1。

若 10 -> 往左邊進位 + 1，本身變成 0。

若 11 -> 往左邊進位 + 1，本身變成 1。

```cpp=
// from NTUCPC Guide
const int len = 10; // 數字的最大位數（答案也不能超過這個位數）
 
void big_plus(int a[len], int b[len], int c[len]) { // c = a + b
    int carry = 0; // 上一個位數進位到這一位數
    for (int i = 0; i < len; i++) {
        int sum = a[i] + b[i] + carry; // 兩個數字的這一位數相加，再加上進位
        c[i] = sum % 10; // 總和的個位數就是答案
        carry = sum / 10; // 超出個位數的部分要進位到下一個位數
    }
}
```

### 減法
---

若直式在減法中上面的數字比下面小，就需要考慮借位問題。加法在程式設計上做法是將進位部分 +1 給左邊位數的，那減法則相反，將左邊的位數扣 1，等同進位 -1。

```cpp=
// from NTUCPC Guide
void big_minus(int a[len], int b[len], int c[len]) { // c = a - b
    int carry = 0; // 上一個位數進（借）位到這一位數
    for (int i = 0; i < len; i++) {
        int sum = a[i] - b[i] + carry; // -10 <= sum <= 9
        if (sum < 0) carry = -1, sum += 10; // 變負的要借位
        else carry = 0;
        c[i] = sum; // 現在 0 <= sum <= 9
    }
}
```

### 乘法
---

乘法與加法的程式設計幾乎差不多，一樣向左進位。

```cpp=
// from NTUCPC Guide
void big_multiplies(int a[len], int b[len], int c[len]) { // c = a * b
    for (int i = 0; i < len; i++) c[i] = 0;
    for (int i = 0; i < len; i++) {
        int carry = 0;
        for (int j = 0; i + j < len; j++) {
            // 從低位做到高位就能好好處理進位
            // (a[i] * 10^i) * (b[j] * 10^j) = (a[i] * b[j]) * 10^(i+j)
            int sum = c[i + j] + a[i] * b[j] + carry;
            c[i + j] = sum % 10;
            carry = sum / 10;
        }
    }
}
```

### 除法
---

比大小函數：

```cpp=
// from NTUCPC Guide
int comp(int a[len], int b[len]) { // -1: a < b, 0: a = b, 1: a > b
    for (int i = len - 1; i >= 0; i--) {
        if (a[i] < b[i]) return -1;
        else if (a[i] > b[i]) return 1;
    }
    return 0;
}
```

除法與其他運算不同，除法是從最高位數（最左邊）開始運算，其他都是從最低位數（最右邊）。

> a / b 的最高位數，就是「b 乘上它不會超過 a」的最大的數字，而這個數字也不是很好找，對人類而言只能猜猜看，好在電腦要一個一個慢慢試沒有那麼困難。在找到最高位數後，就把 a 減去 b 乘上它，接下來再找下一個位數。

```cpp=
void big_divides(int a[len], int b[len], int c[len], int r[len]) { // a / b = c ... r
    int t = len - 1;
    for (; t >= 0 && b[t] == 0; t--); // 找到 b 的最高非 0 位數
    int tmp[len];
    for (int i = 0; i < len; i++) r[i] = a[i], c[i] = 0;
    for (int i = len - t - 1; i >= 0; i--) { 
        // 從最大的位數開始找，要確保 b * 10^i 位數不會超過 len
        int d = 0;
        for (int j = 0; j < len; j++) tmp[j] = 0;
        for (int j = 0; j <= t; j++) tmp[i + j] = b[j]; // tmp = b * 10^i
        while (comp(r, tmp) >= 0) { // r >= tmp
            big_minus(r, tmp, r); // r = tmp - r
            d++;
        }
        c[i] = d;
    }
}
```

習題練習
---

1. https://zerojudge.tw/ShowProblem?problemid=a021

```cpp=
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>

using namespace std;

// 移除前導零（因為有些字串運算完在前面會出現 0, 如: 0001112）
string removeLeadingZeros(string s) {
    int i = 0;
    while(i < s.size() - 1 && s[i] == '0') {
        i++;
    }
    return s.substr(i);
}

// 大數加法
string addBig(const string &a, const string &b) {
    int i = a.size()-1, j = b.size()-1;
    int carry = 0;
    string result = "";
    while(i >= 0 || j >= 0 || carry) {
        int x = (i >= 0 ? a[i]-'0' : 0); // 因為 x 資料型態是 int, 所以必須寫 a[i]-'0'
        int y = (j >= 0 ? b[j]-'0' : 0);
        int sum = x + y + carry;
        carry = sum / 10;
        result.push_back(sum % 10 + '0'); // 最後 push_back 再轉回去 ASCII 碼
        i--; j--;
    }
    reverse(result.begin(), result.end());
    return removeLeadingZeros(result);
}

// 大數減法 (假設 a >= b)
string subBig(const string &a, const string &b) {
    int i = a.size()-1, j = b.size()-1;
    int carry = 0;
    string result = "";
    while(i >= 0) {
        int x = a[i]-'0';
        int y = (j >= 0 ? b[j]-'0' : 0);
        int sub = x - y - carry;
        if(sub < 0) {
            sub += 10;
            carry = 1;
        } else {
            carry = 0;
        }
        result.push_back(sub + '0');
        i--; j--;
    }
    reverse(result.begin(), result.end());
    return removeLeadingZeros(result);
}

// 比較兩個大數字串（無前導零）
int compareBig(const string &a, const string &b) {
    if(a.size() < b.size()) return -1;
    if(a.size() > b.size()) return 1;
    if(a == b) return 0;
    return (a < b ? -1 : 1);
}

// 大數減法, 含負號
string subtractBig(const string &a, const string &b) {
    if(compareBig(a, b) >= 0)
        return subBig(a, b);
    else
        return "-" + subBig(b, a);
}

// 大數乘法
string mulBig(const string &a, const string &b) {
    int n = a.size(), m = b.size();
    vector<int> result(n + m, 0);
    for(int i = n - 1; i >= 0; i--){
        for(int j = m - 1; j >= 0; j--){
            int mul = (a[i]-'0') * (b[j]-'0');
            int sum = result[i+j+1] + mul;
            result[i+j+1] = sum % 10;
            result[i+j] += sum / 10;
        }
    }
    string resStr = "";
    for (int num : result) {
        resStr.push_back(num + '0');
    }
    return removeLeadingZeros(resStr);
}

// 大數除法 (取商數)
string divBig(const string &dividend, const string &divisor) {
    if(compareBig(dividend, divisor) < 0)
        return "0";
    string result = "";
    string current = "";
    for (int i = 0; i < dividend.size(); i++){
        current.push_back(dividend[i]);
        current = removeLeadingZeros(current);
        int count = 0;
        while(compareBig(current, divisor) >= 0) {
            current = subBig(current, divisor);
            count++;
        }
        result.push_back(count + '0');
    }
    return removeLeadingZeros(result);
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    string a, op, b;
    cin >> a >> op >> b;
    string ans;
    
    if(op == "+"){
        ans = addBig(a, b);
    } else if(op == "-"){
        ans = subtractBig(a, b);
    } else if(op == "*"){
        ans = mulBig(a, b);
    } else{
        ans = divBig(a, b);
    }
    cout << ans << "\n";
    return 0;
}
```

![image](https://hackmd.io/_uploads/ryF-83561l.png)

程式碼解釋：

因為這題牽涉到字串處理，需要把輸入的 + - * / 處理掉。

然後程式碼 23 行的 `a[i] - '0'` ，是因為 a[i] 本身是一個 char 資料型態，他們都有對應的 ASCII 值，如果直接用 a[i] 會出錯，如：`'3'` ASCII 碼為 51。

而 `'0'` 的 ASCII 碼為 48，若要求 3，則 `'3' - '0' = 51 - 48 = 3`，接下來的數字以此類推。

其他的部分大概就跟上面的大數運算範例差不多。

只是這題麻煩的是字串處理部分。

2. https://zerojudge.tw/ShowProblem?problemid=d481

矩陣乘法教學，個人推薦優質教師楊翰數學：https://www.youtube.com/watch?v=11H7HLNyg0Q&t=356s

```cpp=
#include <iostream>
#include <vector>
using namespace std;

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int a, b, c, d;
    while(cin >> a >> b >> c >> d){
        // 檢查矩陣是否可乘
        if(b != c){
            cout << "Error" << "\n";
            continue; // 直接換下一組測資, 不讀取矩陣資料
        }
        
        // 讀取第一個矩陣 (a 列, b 行)
        vector<vector<long long>> mat1(a, vector<long long>(b));
        for (int i = 0; i < a; i++){
            for (int j = 0; j < b; j++){
                cin >> mat1[i][j];
            }
        }
        
        // 讀取第二個矩陣 (c 列, d 行)
        vector<vector<long long>> mat2(c, vector<long long>(d));
        for (int i = 0; i < c; i++){
            for (int j = 0; j < d; j++){
                cin >> mat2[i][j];
            }
        }
        
        // 計算矩陣乘法，結果矩陣大小為 a * d
        vector<vector<long long>> result(a, vector<long long>(d, 0));
        for (int i = 0; i < a; i++){
            for (int j = 0; j < d; j++){
                for (int k = 0; k < b; k++){
                    result[i][j] += mat1[i][k] * mat2[k][j];
                }
            }
        }
        
        for (int i = 0; i < a; i++){
            for (int j = 0; j < d; j++){
                cout << result[i][j] << (j < d - 1 ? " " : "");
            }
            cout << "\n";
        }
    }
    return 0;
}
```

![image](https://hackmd.io/_uploads/rkhs5ncpkg.png)

變數解釋：

- a ： 第一個矩陣的行數（Row）
- b ： 第一個矩陣的列數（Col）
- c ： 第二個矩陣的行數（Row）
- d ： 第二個矩陣的列數（Col）

滿足矩陣乘法的條件，是 b、c 相同。如： 1x2 矩陣 跟 2x1 矩陣就可相乘，1x2 1x3 就不行。

假設矩陣 A、B 存在，則矩陣乘法為：

\begin{equation}
A_{m \times n} \cdot B_{n \times k} = C_{m \times k}
\end{equation}

\begin{equation}
c_{ij} = \sum_{r=1}^{n} a_{ir} b_{rj} , \quad \forall \; i = 1, 2, \dots, m, \quad j = 1, 2, \dots, k.
\end{equation}

以下範例舉例矩陣乘法的應用，線性變換（linear transformation）：

$$
\begin{bmatrix}
a & b \\
c & d
\end{bmatrix} 
\begin{bmatrix}
1 \\
2
\end{bmatrix} =
\begin{bmatrix}
a + 2b \\
c + 2d
\end{bmatrix}
$$

乘法結果的 1,1 項，為 $a \cdot 1 + b \cdot 2 = a + 2b$ ，2,1 項為 $c \cdot 1 + d \cdot 2 = c + 2d$ 。

那既然知道矩陣乘法的規則就可以開始寫程式了，如同上面所寫的 `result[i][j] += mat1[i][k] * mat2[k][j];`。

3. https://zerojudge.tw/ShowProblem?problemid=b893

這題順便還考你跳脫字元，真棒。

（勘根定理教學）然後再次推薦：https://www.youtube.com/watch?v=ZvjkCYVAHSs

假設實係數 $f(x) = 0$ ，則所謂勘根定理就是當 $f(a) \cdot f(b) < 0$ ，至少會有一個實根。

```cpp=
#include <bits/stdc++.h>

using namespace std;

// 計算多項式函數值（秦九韶算法）
double poly(const vector<int>& coeff, double x) {
    double result = 0;
    for (int i = 0; i < 6; i++) {
        result = result * x + coeff[i];
    }
    return result;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    
    vector<int> coeff(6);
    
    while (cin >> coeff[0] >> coeff[1] >> coeff[2] >> coeff[3] >> coeff[4] >> coeff[5]) {
        
        bool allZero = true; // 檢查係數是否全都 0
        for (int i = 0; i < 6; i++) {
            if (coeff[i] != 0) {
                allZero = false;
                break;
            }
        }
        if (allZero) { // 全都 0 代表無限多組根
            cout << "Too many... = =\"" << "\n";
            continue;
        }

        bool isConstant = true; // 檢查是否為常數函數
        for (int i = 0; i < 5; i++) {
            if (coeff[i] != 0) {
                isConstant = false;
                break;
            }
        }
        if (isConstant) { // 無根
            cout << "N0THING! >\\\\\\<" << "\n";
            continue;
        }

        vector<pair<int, int>> roots; // pair 關聯式容器
        bool foundRoot = false;
        
        // 因為題目限制區間 [-40, 40] 可直接暴力破解
        for (int i = -40; i <= 40; i++) {
            double val1 = poly(coeff, i);
            double val2 = poly(coeff, i + 1);

            if (fabs(val1) < 1e-9) { // 避免浮點數誤差
                roots.push_back({i, i});
                foundRoot = true;
            }

            if (val1 * val2 < 0) {
                roots.push_back({i, i + 1});
                foundRoot = true;
            }
        }

        if (!foundRoot) {
            cout << "N0THING! >\\\\\\<" << "\n";
        } else {
            for (auto &p : roots) {
                cout << p.first << " " << p.second << "\n";
            }
        }
    }
    return 0;
}
```

![image](https://hackmd.io/_uploads/BJenq2s6yg.png)

有關多項式函數值在程式設計上的計算，可見：https://ohiyooo2.pixnet.net/blog/post/403086710

C++ 解法（秦九韶算法）：https://github.com/Rocketng/Algorithm/blob/master/%E7%A7%A6%E4%B9%9D%E9%9F%B6%E7%AE%97%E6%B3%95.cpp

這邊用到了 pair 關聯式容器，因為勘根定理要求哪兩個連續整數之間有實根，所以用 pair 來表示會更加明瞭。

pair 使用 `pair.fisrt`、`pair.second` 來存取前面跟後面的資料。

然後不知道為啥 long long 會 80%，試好久改成 double 就 AC 了。
