算法分析与设计 - 作业2

问题一

给定正整数 \(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";
}

写在最后

上述解法本质均为按顺序考虑每个元素是否存在于子集中,从而构造出所有子集。

posted @ 2024-03-18 13:06  Rainycolor  阅读(24)  评论(0)    收藏  举报