n阶的妖怪序列是完全减少的分数的序列,介于0和1之间,当以最低的术语表示时,分母小于或等于n,按大小递增的顺序排列。详细说明here。
问题
问题是,给定n和k,其中n = seq的顺序,k =元素索引,我们可以从序列中找到特定元素。例如(n = 5,k = 6)的答案是1/2。
领导
有许多不尽人意的解决方案,但正在寻找一种接近最佳的解决方案。 here中讨论了一种这样的算法,对此我无法理解其逻辑,因此无法应用示例。
问题
可以请更详细地说明解决方案,最好举个例子。
谢谢。
答案 0 :(得分:1)
我已经阅读了您的链接中提供的方法,以及该方法的公认C ++解决方案。让我发布它们,以供参考:
编辑说明
存在几种不尽人意的解决方案。使用优先级队列,一个 可以迭代O(K中的分数(一一生成) 记录N)时间。使用更高级的数学关系,可以将其简化为 好)。但是,这些解决方案都无法获得很多积分,因为 分数的个数(即K)在N中是二次方的。
“好”解决方案基于元二进制搜索。构造这个 解决方案,我们需要以下子例程:给定分数A / B (不一定是不可约的),从中找出多少分数 Farey序列小于此分数。假设我们有这个 子程序那么该算法的工作原理如下:
- 确定一个数字X,使答案在X / N和(X + 1)/ N之间;这样的数字可以通过二进制搜索范围来确定 1 ... N,因此调用子例程O(log N)次。
- 列出X / N ...(X + 1)/ N范围内的所有分数A / B。对于任何给定的B,此范围内最多有一个A,并且可以是 在O(1)中确定。
- 确定此列表中的适当顺序统计信息(通过排序在O(N log N)中进行此操作就足够了。)
这仍然显示了我们如何构造所需的子例程。我们 将展示如何在O(N log N)中实现它,从而给出O(N log ^ 2 N)整体算法。让我们用C [j]表示 小于X / N的不可约分数i / j。该算法是 基于以下观察:C [j] =下限(X * B / N)– Sum(C [D], 其中D除以j)。直接执行,测试是否有D 是除数,产生二次算法。更好的方法 受Eratosthene筛子启发,如下:在步骤j,我们知道 C [j],然后从j的所有倍数中减去它。的运行时间 子例程变为O(N log N)。
相关代码
#include <cassert>
#include <algorithm>
#include <fstream>
#include <iostream>
#include <vector>
using namespace std;
const int kMaxN = 2e5;
typedef int int32;
typedef long long int64_x;
// #define int __int128_t
// #define int64 __int128_t
typedef long long int64;
int64 count_less(int a, int n) {
vector<int> counter(n + 1, 0);
for (int i = 2; i <= n; i += 1) {
counter[i] = min(1LL * (i - 1), 1LL * i * a / n);
}
int64 result = 0;
for (int i = 2; i <= n; i += 1) {
for (int j = 2 * i; j <= n; j += i) {
counter[j] -= counter[i];
}
result += counter[i];
}
return result;
}
int32 main() {
// ifstream cin("farey.in");
// ofstream cout("farey.out");
int64_x n, k; cin >> n >> k;
assert(1 <= n);
assert(n <= kMaxN);
assert(1 <= k);
assert(k <= count_less(n, n));
int up = 0;
for (int p = 29; p >= 0; p -= 1) {
if ((1 << p) + up > n)
continue;
if (count_less((1 << p) + up, n) < k) {
up += (1 << p);
}
}
k -= count_less(up, n);
vector<pair<int, int>> elements;
for (int i = 1; i <= n; i += 1) {
int b = i;
// find a such that up/n < a / b and a / b <= (up+1) / n
int a = 1LL * (up + 1) * b / n;
if (1LL * up * b < 1LL * a * n) {
} else {
continue;
}
if (1LL * a * n <= 1LL * (up + 1) * b) {
} else {
continue;
}
if (__gcd(a, b) != 1) {
continue;
}
elements.push_back({a, b});
}
sort(elements.begin(), elements.end(),
[](const pair<int, int>& lhs, const pair<int, int>& rhs) -> bool {
return 1LL * lhs.first * rhs.second < 1LL * rhs.first * lhs.second;
});
cout << (int64_x)elements[k - 1].first << ' ' << (int64_x)elements[k - 1].second << '\n';
return 0;
}
基本方法
以上社论说明产生了以下简化版本。让我从一个例子开始。
比方说,我们想找到N = 5的Farey序列的第7个元素。
所以,按F5顺序:
k = 0, 0/1
k = 1, 1/5
k = 2, 1/4
k = 3, 1/3
k = 4, 2/5
k = 5, 1/2
k = 6, 3/5
k = 7, 2/3
k = 8, 3/4
k = 9, 4/5
k = 10, 1/1
如果我们可以找到一个函数来查找Farey序列中先前减少的分数的计数,则可以执行以下操作:
int64 k_count_2 = count_less(2, 5); // result = 4
int64 k_count_3 = count_less(3, 5); // result = 6
int64 k_count_4 = count_less(4, 5); // result = 9
此功能写在公认的解决方案中。它使用社论最后一段中解释的确切方法。
如您所见,count_less()
函数生成的k
值与我们的手写列表相同。
我们知道使用该函数的k = 4、6、9的缩减分数的值。那么k = 7呢?如社论所述,我们将列出所有在 X / N和(X + 1)/ N 范围内的归约分数,这里X = 3和N = 5。
使用接受的解决方案中的函数(其底部附近),我们列出并排序归约分数。
之后,我们将重新排列我们的k值,以使其适合我们的新数组:
k = -, 0/1
k = -, 1/5
k = -, 1/4
k = -, 1/3
k = -, 2/5
k = -, 1/2
k = -, 3/5 <-|
k = 0, 2/3 | We list and sort the possible reduced fractions
k = 1, 3/4 | in between these numbers
k = -, 4/5 <-|
k = -, 1/1
(这就是为什么有这段代码:k -= count_less(up, n);
,它基本上重新映射了k个值)
(而且在索引编制过程中,我们还减去了一个,即cout << (int64_x)elements[k - 1].first << ' ' << (int64_x)elements[k - 1].second << '\n';
。这只是为了基本上调用生成的数组中的正确位置。)
因此,对于我们新的重新映射的k值,对于N = 5和k = 7(原始k),我们的结果是2/3。
(在新地图中,我们选择值k = 0)
如果您编译并运行接受的解决方案,它将为您提供:
Input: 5 7 (Enter)
Output: 2 3
我相信这是社论和公认的解决方案的基本要点。