- 問題一
- 解法一
- 解法二
- 解法三
- 解法四
- 問題二
- 解法一
- 解法二
- 解法三
- 寫在最後
問題一
給定正整數 \(a, b\),求 \(\gcd(a, b)\)。
以下將給出四種解法。
解法一
我會暴力!
考慮列舉 \(i(1\le i\le \min(a, b))\),檢查 \(i\) 是否為 \(a, b\) 的公約數,即檢查 \((a\bmod i = 0)\land (b\bmod i=0)\) 是否成立,在所有公約數中取最大值即為最大公約數。
空間複雜度 \(O(1)\),時間複雜度 \(O(\min(a, b))\) 級別。
int g = 0;
for (int i = 1; i <= std::min(a, b); ++ i) {
if (a % i == 0 && b % i == 0) g = i;
}
std::cout << g << "\n";
解法二
我學過高中數學!
考慮更相減損術,有:
上式的數學證明:
- 對於 \(b>a\),\(\gcd(a, b) = \gcd(b, a)\) 顯然成立。
- 對於 \(a > b\),有 \(\forall d\in \mathbf{Z}, d|a, d|b\),容易證明 \(d | a-b\),將 \(a, b\) 均表示為 \(d\) 的倍數即證。則 \(a, b\) 的所有公因數均為 \(a-b\) 與 \(b\) 的公因數,則上式成立。
時間複雜度 \(O(\max(a, b))\) 級別,當 \(a>>b\) 時可達到最壞情況。
int gcd(int a_, int b_) {
if (a_ == b_) return b_;
if (a_ < b_) return gcd(b_, a_);
return gcd(a_ - b_, b_);
}
解法三
我提前看了課本!
考慮更相減損術的最佳化,有輾轉相除法:
上式的數學證明:
設 \(c = \gcd(a,b)\),則 \(\exist n, m\in \mathbf{Z}, a = nc, b = nc\)。
設 \(k = \left\lfloor\frac{a}{b}\right\rfloor, r = a\bmod b\),則有:\(r= a - k\times b = (n-km)c\)。
要證 \(\gcd(b , a\bmod b)=c\),僅需證 \(b=m\times c\),\(r=(n-km)c\) 中,\(m, n-km\) 互質,即 \(\gcd(m, n - km) = 1\)。考慮反證法,設 \(d = \gcd(m, n-km) > 1\)。
設 \(\exist p, q\in \mathbf{Z}, m=pd, n-km=qd\),則有 \(b = pdc\),\(a=dc(kp+q)\)。則 \(dc\) 也為 \(a, b\) 的公約數,且 \(dc > c\),與 \(c = \gcd(a, b)\) 矛盾,反證 \(m, n -km\) 互質,則原式成立。
使用遞迴實現即可。空間複雜度 \(O(1)\),時間複雜度 \(O(\log \max(a, b))\) 級別。
時間複雜度證明:
當 \(a, b\) 同階,即設 \(a, b\) 均為長為 \(n\) 的二進位制整數時,輾轉相除法的時間複雜度為 \(O(n)\),時間複雜度為 \(O(\log\max(a, b))\)。考慮遞迴求最大公約數的過程:
- \(a < b\),此時有 \(\gcd(a,b)=\gcd(b,a)\);
- \(a \ge b\),此時 \(\gcd(a,b)=\gcd(b,a \bmod b)\),對 \(a\) 取模會讓 \(a\) 至少折半,則此過程至多發生 \(O(\log a) = O(n)\) 次。
第一種情況發生後一定會發生第二種情況,因此第一種情況的發生次數一定 不多於 第二種情況的發生次數,則最多遞迴 \(O(n)\) 次就可以得出結果。
而當 \(a>>b\) 時,經過一次遞迴即可轉化為 \(a, b\) 同階的情況,同樣最多遞迴 \(O(n)\) 次即可得出結果。
int gcd(int a_, int b_) {
return b_ ? gcd(b_, a_ % b_) : a_;
}
解法四
我學過數論!
考慮算數基本定理,考慮 \(a, b\) 的標準質因數分解式:
則有:
將 \(a, b\) 進行質因數分解後,求得各質因數冪次的較小值相乘即得最大公約數;也可以僅對其中一方進行質因數分解,每次分解出新的質因數後則在另一方中試除,這樣求得各質因數冪次的較小值從而求得最大公約數。
時間複雜度 \(O\left(\sqrt{\max(a, b)}\right)\) 級別。若採用了第一種實現方法需要儲存各質因數冪次,空間複雜度 \(O(n)\) 級別,若採用第二種實現方法不需要則僅需常數級別的空間即可,空間複雜度 \(O(1)\) 級別。
該解法涉及到了最大公約數的本質,在解決其他較複雜的有關最大公約數問題時常使用到。
第二種實現方式:
int g = 1;
if (a < b) std::swap(a, b);
for (int i = 2; i * i <= b; ++ i) {
if (b % i != 0) continue;
int pb = 1, pa = 1;
while (b % i == 0) b /= i, pb *= i;
while (a % i == 0) a /= i, pa *= i;
g *= std::min(pa, pb);
}
if (b != 1) {
if (a % b == 0) g *= b;
}
std::cout << g << "\n";
問題二
給定集合 \(S\),求 \(S\) 的所有子集。
解法一
我剛上完大一上!
直接寫 \(|S|\) 重迴圈,第 \(i\) 層迴圈列舉第 \(i\) 個元素是否存在於子集中即可。
時間複雜度 \(O(2^{|s|})\) 級別,但是很難寫,而且只能處理 \(|s|\) 固定的情況。
int n; std::cin >> n;
std::vector <int> s;
for (int i = 1; i <= n; ++ i) {
int x; std::cin >> x;
s.push_back(x);
}
std::vector <int> t;
for (int i0 = 0; i0 <= 1; ++ i0) {
if (i0 == 1) t.push_back(s[0]);
for (int i1 = 0; i1 <= 1; ++ i1) {
if (i1 == 1) t.push_back(s[1]);
for (int i2 = 0; i2 <= 1; ++ i2) {
//...
}
if (i1 == 1) t.pop_back();
}
if (i0 == 1) t.pop_back();
}
解法二
我會搜尋!
列舉每個元素是否存在於子集中,深度優先搜尋即可。
時間複雜度 \(O(2^{|s|})\) 級別,但是不難寫。
void DFS(int pos_, int lim_, std::vector <int> &s_, std::vector <int> &t_) {
if (pos_ >= lim_) {
for (auto x: t_) std::cout << x << " ";
std::cout << "\n";
return ;
}
DFS(pos_ + 1, lim_, s_, t_);
t_.push_back(s_[pos_]);
DFS(pos_ + 1, lim_, s_, t_);
t_.pop_back();
}
int main() {
int n; std::cin >> n;
std::vector <int> s, t;
for (int i = 1; i <= n; ++ i) {
int x; std::cin >> x;
s.push_back(x);
}
DFS(0, n, s, t);
return 0;
}
解法三
我會位運算!
考慮用長度為 \(|s|\) 的 01 序列表示 \(s\) 的一個子集,01 序列的第 \(i\) 個位置表示 \(s\) 的第 \(i\) 個元素是否存在於子集中。
記集合 \(t_i(0\le i\le n)\) 代表使用 \(s\) 中的前 \(i\) 個元素組成的所有子集代表的 01 序列組成的集合,顯然 \(t_i\) 中均為長度為 \(i\) 的 01 序列,則有:
將 \(t_{|s|}\) 代表的所有 01 序列轉化為子集形式即為所求。
迴圈實現即可,時間複雜度 \(O(2^{|s|})\) 級別。
int n; std::cin >> n;
std::vector <int> s;
for (int i = 1; i <= n; ++ i) {
int x; std::cin >> x;
s.push_back(x);
}
std::vector <std::string> t, temp;
t.push_back("");
for (int i = 0; i < n; ++ i) {
temp.clear();
for (auto x: t) {
temp.push_back(x + "0");
temp.push_back(x + "1");
}
std::swap(t, temp);
}
for (auto x: t) {
for (int i = 0; i < n; ++ i) {
if (x[i] == '1') std::cout << s[i] << " ";
}
std::cout << "\n";
}
寫在最後
上述解法本質均為按順序考慮每個元素是否存在於子集中,從而構造出所有子集。