算法分析与设计 - 作业4

问题一

有一个序列是某个有序系列围绕着下标为 \(K\) 的元素(\(0 \le k \le \operatorname{length}\))旋转得到的序列,使数组下标变为 \([k], [k+1], \cdots, [n-1], [0], [1], …, [k-1]\),如 123456 围绕着下标为 3 的元素旋转得到 456123,请为此序列编写元素查找算法,并分析你的算法性能。

发现序列以位置 \(k\) 为界,前后分别为有序状态。在有序部分内可以进行二分查找,于是考虑对于要查询的位置,首先判断它位于 \(1\sim k\) 中还是位于 \(k + 1\sim n\) 中,然后在对应的有序部分内进行二分查找即可。

空间复杂度 \(O(n)\) 级别,单次查询即为二分查找的时间复杂度,为 \(O(\log n)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 1e5 + 10;
//=============================================================
int a[kN];
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  // std::ios::sync_with_stdio(0), std::cin.tie(0);

  int n, k;
  std::cin >> n >> k;
  for (int i = 1, j = k + 1; i <= k; ++ i, ++ j) a[i] = j;
  for (int i = k + 1, j = 1; i <= n; ++ i, ++ j) a[i] = j; 
  
  int q; std::cin >> q;
  while (q --) {
    int x, p = -1; std::cin >> x;
    if (x <= k) p = std::lower_bound(a + k + 1, a + n + 1, x) - a;
    else if (x <= n) p = std::lower_bound(a + 1, a + k + 1, x) - a;
    else p = -1;
    if ((x <= k && p == n + 1) || (k < x && x <= n && p == k + 1)) p = -1;
    std::cout << p << "\n";
  }
  return 0;
}

问题二

我们学习的二分查找算法是针对一维有序序列的,现假设有一个矩阵,其每一行每一列分别是从左到右、从上到下有序的,请为此矩阵编写元素查找算法,并分析你的算法性能。

手玩一下可知矩阵的形态类似于:

5
1 2 3 4 5
2 3 4 5 6
3 4 5 6 7
4 5 6 7 8
5 6 7 8 9

则某种元素的出现次数可能为 \(O(n^2)\) 级别,且出现位置可能并不连续,估计很难找到一个小于 \(O(n)\) 的解法。

解法一

发现每一行都是有序的,则在每一行若有待查询元素,则它们一定构成一段连续的区间。于是考虑对每一行运行二分查找算法,找到待查询元素出现的区间即可。

并不需要额外的数据结构维护,空间复杂度 \(O(n^2)\) 级别,时间复杂度 \(O(n\log n)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2000 + 10;
//=============================================================
int a[kN][kN];
//=============================================================
//=============================================================
int main() {
  //freopen("1.txt", "r", stdin);
  // std::ios::sync_with_stdio(0), std::cin.tie(0);

  int n; std::cin >> n;
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      std::cin >> a[i][j];
    }
  }

  int q; std::cin >> q;
  while (q --) {
    int x; std::cin >> x;
    for (int i = 1; i <= n; ++ i) {
      int l = std::lower_bound(a[i] + 1, a[i] + n + 1, x) - a[i];
      int r = std::upper_bound(a[i] + 1, a[i] + n + 1, x) - a[i] - 1;
      if (l <= r) std::cout << "line " << i << " : [" << l << ", " << r << "] is element " << x <<".\n";
      else std::cout << "line " << i << " has no element " << x << "!\n";
    }
  }
  return 0;
}

解法二

解法一没有使用每一列也为有序这一性质,这可不行!

通过观察可知,若每一列也为有序,则对于某元素 \(x\),其在第 \(i\) 行与第 \(i+1\) 行中出现的位置 \([l_i, r_i], [l_{i + 1}, r_{i + 1}]\) 一定满足:

\[l_{i + 1}\le l_{i}\le r_{i + 1} \le r_{i} \]

即有:

\[(l_{i + 1}\le l_i)\land (r_{i + 1}\le r_i) \]

发现数列 \(l_1\sim l_n\) 和数列 \(r_{1}\sim r_n\) 均为非递减数列,分别代表第 \(i\) 行元素 \(x\) 最靠左和最靠右出现的位置。考虑通过求得这两个数列来确定待查询元素的位置。

以求得 \(l_1\sim l_n\) 为例,考虑倒序枚举每一行 \(i(1\le i\le n)\),在此过程中维护一个指针 \(p\) 指向当前行 \(x\) 出现的最靠左的位置:

  • \(i = n\) 时,初始化 \(p = 0\);否则当 \(1\le i<n\) 时初始化 \(p = l_{i + 1}\)
  • 然后不断右移指针直至 \(a_{i, p} \ge x\),则若第 \(i\) 行存在元素 \(x\),则 \(l_i = p\)

求数列 \(r_1\sim r_n\) 同理,仅需维护指针 \(p\) 指向该行元素 \(x\) 最靠右的满足 \(a_{i,p}\le x\) 的元素,并不断右移即可。

按照上述算法求得上述两数列后,若某行满足 \(l_i \le r_i\),说明该行存在元素 \(x\),且所有元素 \(x\) 构成区间 \([l_i, r_i]\),否则不存在元素 \(x\)

空间复杂度 \(O(n)\) 级别。发现上述过程中指针 \(p\) 在倒序枚举每行时是单调不降的,则右移操作至多会进行 \(O(n)\) 次,又仅会枚举 \(O(n)\) 行,则枚举次数为 \(O(n)\) 级别。

//
/*
By:Luckyblock
*/
#include <bits/stdc++.h>
#define LL long long
const int kN = 2000 + 10;
//=============================================================
int a[kN][kN], l[kN], r[kN];
//=============================================================
//=============================================================
int main() {
  // freopen("1.txt", "r", stdin);
  // std::ios::sync_with_stdio(0), std::cin.tie(0);

  int n; std::cin >> n;
  for (int i = 1; i <= n; ++ i) {
    for (int j = 1; j <= n; ++ j) {
      std::cin >> a[i][j];
    }
  }

  int q; std::cin >> q;
  while (q --) {
    int x; std::cin >> x;

    for (int i = n; i >= 1; -- i) {
      int pl = (i == n) ? 0 : l[i + 1];
      while (pl <= n && a[i][pl] < x) ++ pl;
      l[i] = pl;

      int pr = (i == n) ? 0 : r[i + 1];
      while (pr < n && a[i][pr + 1] <= x) ++ pr;
      r[i] = pr;
    }

    for (int i = 1; i <= n; ++ i) {
      if (l[i] <= r[i]) std::cout << "line " << i << " : [" << l[i] << ", " << r[i] << "] is element " << x <<".\n";
      else std::cout << "line " << i << " has no element " << x << "!\n";
    }
  }
  return 0;
}

写在最后

问题二中解法二的时间复杂度分析是一种均摊时间复杂度分析。

posted @ 2024-03-18 13:07  Rainycolor  阅读(34)  评论(0编辑  收藏  举报