Нахождение неприводимых дробей между двумя заданными дробями в порядке возрастания знаменателей и затем числителей - PullRequest
0 голосов
/ 11 января 2020

Ограничение времени на тест: 2 секунды
Ограничение памяти на тест: 512 мегабайт

Вам дается две фракции a/b <<code>c/d и положительное число N. Рассмотрим все неприводимые дроби e/f, такие что 0 < e, f ≤ N и a/b <<code>e/f <<code>c/d. Пусть s будет последовательностью этих дробей, отсортированной по возрастанию в знаменателях, а затем в числителях (дробь e1/f1 предшествует e2/f2, если f1 < f2 или f1 = f2 and e1 < e2). Вы должны напечатать первые n условия последовательности s или всю последовательность s, если она состоит из менее чем n членов.

Ввод
Первый строка каждого теста содержит 6 целых чисел a, b, c, d, N, n (0 ≤ a ≤ 10^18, 1 ≤ b, c, d, N ≤ 10^18, 1 ≤ n ≤ 200 000, a/b < c/d).

Вывод
Сначала выведите, сколько членов последовательности s выведите. И затем выведите эти термины в правильном порядке.

Примеры

  • Ввод:
    0 1 1 1 5 10
    
    Ввод:
    9
    1 2
    1 3
    2 3
    1 4
    3 4
    1 5
    2 5
    3 5
    4 5
    
  • Ввод:
    55 34 68 42 90 1
    
    Выходные данные:
    1
    89 55
    
  • Входные данные:
    49 33 45 30 50 239
    
    Выходные данные:
    0
    

Пока мне удалось написать только решение, которое повторяется по всем знаменателям от 1 до N, и для каждого знаменателя перебирает все числители от a*f/b до c*f/d, добавляя все найденные неприводимые дроби к ответу.

Вот мой код:

#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;

long long a, b, c, d, N, n;
vector<pair<long long, long long>> result;

long long gcd(long long a, long long b) {
    while (b) {
        a %= b;
        swap(a, b);
    }
    return a;
}

void computeResult() {
    for (long long f = 1; f <= N; f++) {
        long long eMax = c*f / d;
        if (c*f % d != 0) eMax++;
        eMax = min(eMax, N);
        for (long long e = a*f / b + 1; e < eMax; e++) {
            if (gcd(e, f) == 1) {
                result.push_back(make_pair(e, f));
                if (result.size() == n)
                    return;
            }
        }
    }
}

int main() {
    cin >> a >> b >> c >> d >> N >> n;  
    computeResult();
    cout << result.size() << endl;
    for (pair<long long, long long> fraction : result)
        cout << fraction.first << " " << fraction.second << endl;
}

К сожалению, это решение слишком медленное. Интересно, как решить эту проблему более эффективно.

Ответы [ 3 ]

3 голосов
/ 16 января 2020

Это очень интересный вопрос, и мне очень понравилось думать об этом, и я не знаю, почему он получил так много отрицательных голосов. В любом случае, ниже мой грубый набросок решения. Я обновлю свой ответ позже, если добавлю к нему некоторые уточнения.


Как подсказывает @ user3357359 answer , нам нужен более эффективный способ генерации взаимных кодов. Обычный метод (который используется в решателях диофантовых уравнений): Последовательность Фарея .

Определение: Последовательность Фарея порядка n между a/b и c/d - восходящая (по значению) последовательность неприводимых дробей p/q, такая, что a/b <= p/q <= c/d, где q < n.

Свойства последовательности Фари представлены в this бумага . Итак, у нас есть несколько вопросов под рукой:

  • Как эффективно сгенерировать Последовательность Фарея порядка n?
    Знание первого элемента a0/b0 = a/b и второй элемент a1/b1, можно сгенерировать a2/b2, используя следующий алгоритм (со сложностью O(1)):

    k = int((n + b0) / b1)
    a2 = k * a1 - a0
    b2 = k * b1 - b0
    
  • Как получить второй элемент Последовательность Фарея порядка n?
    Мы знаем, что знаменатель находится где-то в диапазоне 1..n. Мы также знаем, что a/b и c/d являются соседями в Farey Sequence тогда и только тогда, когда b*c - a*d = 1. Таким образом, вычисляя значение от d до 1..n, мы можем найти наименьшую дробь, которая будет следующей в последовательности. Сложность O(n).

  • Как мы решаем, какой порядок последовательности Фарея мы должны сгенерировать?
    Слепое порождение порядка N, когда N по величине 10^18 глупо. Более того, вам не хватит памяти для этого. Нам нужно только сгенерировать Последовательность Фэри некоторого порядка k, чтобы длина его была больше, чем n, который ограничен 200000. Это самая сложная часть этого алгоритма, и сейчас инструменты в теории чисел позволяют нам только оценить: |F_k| = 3*k^2 / pi^2. Итак, у нас есть:

    k = ceil(sqrt(n * pi^2)) + C
    

    На стр. 11 этой статьи вы также можете найти ошибку аппроксимации этой формулы, так что вы получите больше места для ошибки, таким образом, C , Обратите внимание, что для каждого a/b и c/d длина F_k будет разной.

Подводя итог, псевдокод для алгоритма:

1. Estimate the order k of the Farey Sequence to generate, such that |F_k| >= n
2. Calculate the second element of F_k. O(k), where k << n and 1 < n < 200000
3. Generate the whole Farey Sequence. O(n), where 1 < n < 200000
4. Sort by the requirements. O(n log n), where 1 < n < 200000

В худшем случае, когда ваша оценка в step 1 дала меньше элементов, чем n, вам нужно будет сгенерировать снова, используя чуть более высокий порядок k'. Что только увеличит время выполнения вашего алгоритма на постоянную. Таким образом, общая сложность составляет в среднем O(n log n), где n < 200000.


Замечания: узким местом этого алгоритма является оценка порядка k. Этого можно полностью избежать, используя не Последовательность Фаре, а * Stern-Brocot Tree . Дерево генерируется именно в том порядке, в котором вы нуждаетесь, но я сомневаюсь, что в произвольной настройке с a/b != 0/1 и c/d != 1/1 оно будет перебирать все дроби в хорошем порядке.

1 голос
/ 20 января 2020

Помимо другой проблемы, вы заметите, что сортировка работает, если N=10^9, но если N=10^18, то ваш код будет переполнен при умножении на f, и все рухнет. Вы можете сделать long long eMax = (long long)(((long double)c) * f / d); (и аналогично для нижней границы), чтобы уменьшить эту проблему, но это замедлит ее.

1 голос
/ 13 января 2020

Вы должны перебирать все знаменатели от 1 до N, но вам не нужно перебирать все числители в данном диапазоне.

Для любой неприводимой дроби числитель / знаменатель , числитель и знаменатель являются взаимно простыми числами . Так что вам нужно эффективно сгенерировать все взаимно простые числа знаменатель , для которых совмещенное число / знаменатель l ie в заданном диапазоне.

Например, для знаменатель = 8, взаимные числа 1,3,5,7. Неприводимые доли в диапазоне [0,1] составляют 1/8, 3/8, 5/8, 7/8. Вы можете расширить этот диапазон неприводимых дробей, добавив к ним целое число: 1 + 1/8, 1 + 3/8, 1 + 5/8, 1 + 7/8 => 9/8, 11/8, 13 / 8, 15/8 - неприводимые дроби со знаменателем 8 в диапазоне [0 + 1,1 + 1] => [1,2]

Псевдокод для генерации всех взаимно простых чисел x, таких что / b

prime_factors_of_x = get_prime_factors(x)
is_coprime = boolean array of size x
set every element of is_coprime to True
// next, set to False all elements indexed by multiples of a prime factor of x
for every factor in prime_factors_of_x:
   // next loop can be a vectorial operation in some languages
   for i in 0..(x/factor):
      is_coprime[i*factor] = False

all_coprime_list = [] // empty list
min_coprime = floor(a/b * x)+1
max_coprime = floor(c/d * x)-1
for i in min_coprime..max_coprime:
   if is_coprime[i mod x]:
      all_coprimes_list.append(i)

Это общая идея.

...