【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 ÷nd, 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;
}
```

程式碼解釋:
因為這題牽涉到字串處理,需要把輸入的 + - * / 處理掉。
然後程式碼 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;
}
```

變數解釋:
- 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;
}
```

有關多項式函數值在程式設計上的計算,可見: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 了。