以Farey序列的n阶查找第k个元素

时间:2019-05-18 09:06:04

标签: algorithm sequence computer-science

n阶的妖怪序列是完全减少的分数的序列,介于0和1之间,当以最低的术语表示时,分母小于或等于n,按大小递增的顺序排列。详细说明here

enter image description here

问题

问题是,给定n和k,其中n = seq的顺序,k =元素索引,我们可以从序列中找到特定元素。例如(n = 5,k = 6)的答案是1/2。

领导

有许多不尽人意的解决方案,但正在寻找一种接近最佳的解决方案。 here中讨论了一种这样的算法,对此我无法理解其逻辑,因此无法应用示例。

问题

可以请更详细地说明解决方案,最好举个例子。

谢谢。

1 个答案:

答案 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个元素。

  1. 正如解释中所述,我们首先编写一个子例程,该子例程为我们提供“ k”值(在给定分数之前存在多少个Farey序列归约分数-可以归结或不归结) li>

所以,按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

我相信这是社论和公认的解决方案的基本要点。