算法分析与设计 - 作业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_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;
}
写在最后
问题二中解法二的时间复杂度分析是一种均摊时间复杂度分析。