演算法分析與設計 - 作業2

Rainycolor發表於2024-03-18

目錄
  • 問題一
    • 解法一
    • 解法二
    • 解法三
    • 解法四
  • 問題二
    • 解法一
    • 解法二
    • 解法三
    • 寫在最後

問題一

給定正整數 \(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";

解法二

我學過高中數學!

考慮更相減損術,有:

\[\gcd(a, b) = \begin{cases} a &(a = b)\\ \gcd(b, a) &b> a\\ \gcd(a-b, b) &\text{otherwise} \end{cases}\]

上式的數學證明:

  • 對於 \(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_);
}

解法三

我提前看了課本!

考慮更相減損術的最佳化,有輾轉相除法:

\[\gcd(a, b) = \begin{cases} b &(a = 0)\\ \gcd(b, a\bmod b) &\text{otherwise} \end{cases}\]

上式的數學證明:

\(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\) 的標準質因數分解式:

\[\exist (p_1<p_2<\cdots),\ a={p_1}^{\alpha_1}{p_2}^{\alpha_2}\cdots, b = {p_1}^{\beta_1}{p_2}^{\beta_2}\cdots \]

則有:

\[\gcd(a, b) = {p_1}^{\min(\alpha_1, \beta_1)}{p_2}^{\min(\alpha_2, \beta_2)}\cdots \]

\(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_i = \begin{cases} \empty &(i = 0)\\ t_i = \{ x + \text{0} | x\in t_{i - 1}\} + \{ x + \text{1} | x\in t_{i - 1} \} &\text{otherwise} \end{cases}\]

\(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";
}

寫在最後

上述解法本質均為按順序考慮每個元素是否存在於子集中,從而構造出所有子集。

相關文章