Блог пользователя Medic

Автор Medic, 15 лет назад, По-русски
Стандартная задача с одной из тренировок ВолгоградскогоГосударственногоУниверситета: деление длинного на короткое и вывод 2-х результатов: целочисленный результат и остаток.
стандартное решение на Java:
BigInteger a = in.nextBigInteger();
BigInteger b = in.nextBigInteger();
BigInteger r[] = a.divideAndRemainder(b);
out.print(r[0]);
out.print(r[1]);

программа не проходит и как оказалось работает 2.032 с.

Запрограммированная ручками по алгоритму ДА на С++ работает 0.009 с.
В ходе тех же экспериментов над встроенной ДА в Питоне получилось 2.074 с.
и дает всего в 10 раз худшее время чем С++ LISP.

В чем проблема? неужели встроенная ДА НАСТОЛЬКО тормазная??? У кого какие версии?

З.Ы. Знала бы как приложить файл, приложила бы input.


  • Проголосовать: нравится
  • +12
  • Проголосовать: не нравится

15 лет назад, # |
  Проголосовать: нравится +12 Проголосовать: не нравится
В Java довольно медленный алгоритм преобразования длинного числа в строку, т.е. считает еще более менее, но вот выводит долго.

Я как-то раз даже писал свою функцию перевода длинного в строку.
Вот - выдрал из старого кода:

     public static String ToString(BigInteger x) {
        BigInteger pows[] = new BigInteger[32];
        pows[0] = BigInteger.valueOf(10);
        int i = 0;
        for (; x.compareTo(pows[i]) >= 0; ++i)
            pows[i + 1] = pows[i].multiply(pows[i]);
        return ToString(x, pows, i - 1, false);
    }

    private static String ToString(BigInteger x, BigInteger[] pows, int k,
            boolean needZeros) {
        if (k <= 0) {
            if (needZeros || x.compareTo(BigInteger.ZERO) != 0)
                return x.toString();
            else
                return "";
        }
        int cmp = x.compareTo(BigInteger.ZERO);
        if (!needZeros && cmp == 0)
            return "";
        BigInteger[] parts = x.divideAndRemainder(pows[k]);
        StringBuffer buf = new StringBuffer(ToString(parts[0], pows, k - 1,
                needZeros));
        String tmp = ToString(parts[1], pows, k - 1, needZeros
                || parts[0].compareTo(BigInteger.ZERO) > 0);
        if (needZeros || parts[0].compareTo(BigInteger.ZERO) > 0) {
            StringBuffer[] t = new StringBuffer[32];
            t[0] = new StringBuffer("0");
            int needLen = (1 << k) - tmp.length();
            if (needLen > 0) {
                int j = 0;
                for (int i = needLen; i > 0; i >>= 1) {
                    t[j + 1] = new StringBuffer();
                    t[j + 1].append(t[j].toString());
                    t[j + 1].append(t[j].toString());
                    ++j;
                }
                j = 0;
                while (needLen > 0) {
                    if ((needLen & 1) > 0)
                        buf.append(t[j]);
                    ++j;
                    needLen >>= 1;
                }
            }
        }
        buf.append(tmp);
        return buf.toString();
    }

Попробуйте применить. На действительно длинных числах заметно ускоряет.
15 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится
Дело в том, что на C++ ты наверняка реализовала именно алгоритм деления длинного числа на короткое, а в Java ты делишь BigInteger на BigInteger, для чего используется более медленный алгоритм деления длинного числа на длинное. Насчет быстродействия длинной арифметики в Java даже скажу больше: она зачастую эффективнее популярных самописных реализаций на C++ за счет более экономного хранения длинных чисел в памяти. В C++ числа хранятся в системе счисления с основанием вида 10 в степени k (где k обычно 4 или 9), т.е. не все биты типа int используются. А в Java используются все 32 бита. Поэтому размер (длина) чисел получается меньше, что с одной стороны ускоряет исполнение основных операций длинной арифметики, а с другой стороны действительно несколько замедляет преобразование в строковый вид (но в данном случае преобразование в строку - не основная причина медленной работы).
  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    А вы потестируйте... Я потестировал деление числа из 10^5 цифр на 11.
    Вот лог работы на моем не слишком шустром ноуте:

    Done reading input and computing in:
    2.567
    Done quick output in:
    2.305
    Done default output in:
    5.639

    Т.е. дефолтный вывод занимает времени больше чем чтение, подсчет, и мой вывод вместе взятые. Поэтому я утверждаю, что в данном случае как раз вывод числа - самое тонкое место.
    И проблема не в том сколько бит используется для хранения, проблема в ассимптотике алгоритма перевода из 2-ичной системы в 10-чную. Тот метод, что привел я ассимптотически лучше.
  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    И кстати, в Java если делить длинный BigInteger на короткий BigInteger (который влазит в инт) то деление происходит быстрее, т.е. внутри вызывается другой метод в котором реализовно деление на короткое.
  • 15 лет назад, # ^ |
      Проголосовать: нравится +12 Проголосовать: не нравится
    Число, представленное в системе с основанием 10^9, будет длинее числа в системе 2^32 в ln(2^32)/ln(10^9)  = 1.07 раза, вычисления O(beta^2) будут медленее в 1.14 раз -- всем известно, что разница в быстродействии С++ и Явы гораздо больше. Операции по основанию 10^9 медленее совсем по другой причине -- там есть необходимость деления по модулю 10^9, что при 2^32 заменяется сдвигом. Самописные реализации, как правило, потому и используют кратные десяти основания, что ввод-вывод с десятичной системой реализуется за O(beta), а не O(beta^2)
    • 15 лет назад, # ^ |
        Проголосовать: нравится 0 Проголосовать: не нравится
      И в какое число раз C++ быстрее Java, раз уж это всем известно?
  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится

    В Java достаточно неплохая реализация деления длинного на длинное A / B выполнится за время O(Len(A) * Len(B))

    Len(B) == 1 в нашем случае т.к. оно короткое

    а вот перевод из десятичной систему в двоичную 

    и обратно займёт O(Len(A) ^ 2) .

    Поэтому в данному случае преобразование систем счисления основная причина медленной работы.

  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    А что мешает в С++ использовать все 32 бита ?
15 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится
Совсем народ обленился... куда ушли те времена, когда все писали нормальную длиную арифметику?
Кстати, аналогичную задачу из книги Ф.В. Меньшикова я сумел сдать именно реализовав длиную арифметику. И возможно это было первый и последний раз, когда я её писал руками.
  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    Никуда не ушли. Все так же пишем :)
  • 15 лет назад, # ^ |
      Проголосовать: нравится 0 Проголосовать: не нравится
    Офигеть, как пафосно звучит "сумел сдать именно реализовав длинную арифметику". Кстати, в слове "длинная" 2 буквы "Н", если что.

    Там писанины на три строчки, а ты говоришь "сумел сдать". Ну молодец, че тут скажешь. Растешь над собой :)

    А вообще, выучи С++ и волей-неволей придется вспоминать как ты однажды "сумел сдать" ))
15 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится
Насчёт Питона. Он считает-то очень быстро в своём внутреннем 256-ичном типе. Основное время уходит на то, чтобы перевести исходное число из десятичного формата во внутренний, а потом перевести ответ обратно для вывода.

Можно посмотреть на скорость вывода циферок в следующем примере:

print 0
s = '1' + '0' * 100000
print 1
n = int (s)
print 2
m = n / 3
print 3
t = '%d' % m
print 4
15 лет назад, # |
  Проголосовать: нравится 0 Проголосовать: не нравится
А какое количество тестов и размер чисел?