Ошибка целочисленного умножения Карацубы из-за ошибки сегментации

Когда я запускаю программу, она падает с ошибкой сегментации. Кроме того, когда я отлаживаю код в IDE codeblocks, я также не могу его отлаживать. Программа вылетает еще до начала отладки. Я не могу понять проблему. Любая помощь будет оценена по достоинству. Спасибо!!

#include <iostream>
#include <math.h>
#include <string>
using namespace std;

// Method to make strings of equal length
int makeEqualLength(string& fnum,string& snum){
    int l1 = fnum.length();
    int l2 = snum.length();
    if(l1>l2){
        int d = l1-l2;
        while(d>0){
            snum  = '0' + snum;
            d--;
        }
        return l1;
    }
    else if(l2>l1){
        int d = l2-l1;
        while(d>0){
            fnum  = '0' + fnum;
            d--;
        }
        return l2;
    }
    else
        return l1;
}

int singleDigitMultiplication(string& fnum,string& snum){
    return ((fnum[0] -'0')*(snum[0] -'0'));
}

string addStrings(string& s1,string& s2){
  int length = makeEqualLength(s1,s2);
  int carry = 0;
  string result;
  for(int i=length-1;i>=0;i--){
    int fd = s1[i]-'0';
    int sd = s2[i]-'0';
    int sum = (fd+sd+carry)%10+'0';
    carry = (fd+sd+carry)/10;
    result = (char)sum + result;
  }
  result = (char)carry + result;
  return result;
}

long int multiplyByKaratsubaMethod(string fnum,string snum){

    int length = makeEqualLength(fnum,snum);
    if(length==0) return 0;
    if(length==1) return singleDigitMultiplication(fnum,snum);

    int fh = length/2;
    int sh = length - fh;

    string Xl = fnum.substr(0,fh);
    string Xr = fnum.substr(fh,sh);
    string Yl = snum.substr(0,fh);
    string Yr = snum.substr(fh,sh);

    long int P1 = multiplyByKaratsubaMethod(Xl,Yl);
    long int P3 = multiplyByKaratsubaMethod(Xr,Yr);
    long int P2 = multiplyByKaratsubaMethod(addStrings(Xl,Xr),addStrings(Yl,Yr)) - P1-P3;

    return (P1*pow(10,length) + P2*pow(10,length/2) + P3);
}


int main()
{
    string firstNum = "62";
    string secondNum = "465";
    long int result = multiplyByKaratsubaMethod(firstNum,secondNum);
    cout << result << endl;
    return 0;
}

person RK21    schedule 14.04.2017    source источник
comment
По крайней мере понаблюдайте за вызовами, добавив cerr << "(" << fnum << "," << snum << ")" << endl; в начало функции, и вы увидите проблему (бесконечная рекурсия в какой-то момент).   -  person Jean-Baptiste Yunès    schedule 14.04.2017
comment
@ RK21 Я действительно не верю, что программа вылетает до того, как будет введено main(). Я также не мог увидеть какой-либо код, который мог бы быть подозрительным для этого, и я не мог проверить это, когда отлаживал его в Visual Studio. Пожалуйста, примите во внимание рекомендацию Жана-Батиста Юнеса и/или отладьте свой код за один шаг. Кстати. из-за отладки я обнаружил серьезную проблему в addStrings(). После исправления этого я заметил переполнение стека. Это означает, что завершение рекурсии не работает (или, по крайней мере, рекурсия потребляет слишком много стека). Я просто ищу причину этого...   -  person Scheff's Cat    schedule 14.04.2017
comment
@Шефф... ты прав. Программа не падает перед вызовом main. Он выдает некоторые значения для P1 и P2 и через некоторое время завершается ошибкой сегментации.   -  person RK21    schedule 14.04.2017
comment
@Scheff, не могли бы вы подсказать, какие изменения кода вы внесли в метод addStrings?   -  person RK21    schedule 14.04.2017


Ответы (1)


В вашем коде есть три серьезные проблемы:

  1. result = (char)carry + result; не работает.
    Перенос имеет значение от 0 (0 * 0) до 8 (9 * 9). Его необходимо преобразовать в соответствующее значение ASCII:
    result = (char)(carry + '0') + result;.

  2. Это приводит к следующей проблеме: перенос вставляется, даже если он 0. Отсутствует оператор if:
    if (carry/* != 0*/) result = (char)(carry + '0') + result;.

  3. После исправления первых двух проблем и повторного тестирования переполнение стека по-прежнему происходит. Итак, я сравнил ваш алгоритм с другим, который нашел в гугле:
    Разделяй и властвуй | Набор 4 (алгоритм Карацубы для быстрого умножения)
    (и, возможно, был вашим источником, потому что он очень похож). Не копая глубже, я исправил то, что выглядело как простая ошибка переноса:
    return P1 * pow(10, 2 * sh) + P2 * pow(10, sh) + P3;
    (я заменил length на 2 * sh и length/2 на sh, как я видел в коде, который я гуглил.) Это стало очевидным для меня, увидев в отладчик этой длины может иметь нечетные значения, так что sh и length/2 будут разными значениями.

После этого ваша программа заработала.

Я изменил функцию main(), чтобы проверить ее немного сложнее:

#include <cmath>
#include <iostream>
#include <string>

using namespace std;

string intToStr(int i)
{
  string text;
  do {
    text.insert(0, 1, i % 10 + '0');
    i /= 10;
  } while (i);
  return text;
}

// Method to make strings of equal length
int makeEqualLength(string &fnum, string &snum)
{
  int l1 = (int)fnum.length();
  int l2 = (int)snum.length();
  return l1 < l2
    ? (fnum.insert(0, l2 - l1, '0'), l2)
    : (snum.insert(0, l1 - l2, '0'), l1);
}

int singleDigitMultiplication(const string& fnum, const string& snum)
{
  return ((fnum[0] - '0') * (snum[0] - '0'));
}

string addStrings(string& s1, string& s2)
{
  int length = makeEqualLength(s1, s2);
  int carry = 0;
  string result;
  for (int i = length - 1; i >= 0; --i) {
    int fd = s1[i] - '0';
    int sd = s2[i] - '0';
    int sum = (fd + sd + carry) % 10 + '0';
    carry = (fd + sd + carry) / 10;
    result.insert(0, 1, (char)sum);
  }
  if (carry) result.insert(0, 1, (char)(carry + '0'));
  return result;
}

long int multiplyByKaratsubaMethod(string fnum, string snum)
{
  int length = makeEqualLength(fnum, snum);
  if (length == 0) return 0;
  if (length == 1) return singleDigitMultiplication(fnum, snum);

  int fh = length / 2;
  int sh = length - fh;

  string Xl = fnum.substr(0, fh);
  string Xr = fnum.substr(fh, sh);
  string Yl = snum.substr(0, fh);
  string Yr = snum.substr(fh, sh);

  long int P1 = multiplyByKaratsubaMethod(Xl, Yl);
  long int P3 = multiplyByKaratsubaMethod(Xr, Yr);
  long int P2
    = multiplyByKaratsubaMethod(addStrings(Xl, Xr), addStrings(Yl, Yr))
    - P1 - P3;
  return P1 * pow(10, 2 * sh) + P2 * pow(10, sh) + P3;
}

int main()
{
  int nErrors = 0;
  for (int i = 0; i < 1000; i += 3) {
    for (int j = 0; j < 1000; j += 3) {
      long int result
        = multiplyByKaratsubaMethod(intToStr(i), intToStr(j));
      bool ok = result == i * j;
      cout << i << " * " << j << " = " << result
        << (ok ? " OK." : " ERROR!") << endl;
      nErrors += !ok;
    }
  }
  cout << nErrors << " error(s)." << endl;
  return 0;
}

Примечания об изменениях, которые я сделал:

  1. По поводу библиотеки std: Пожалуйста, не путайте заголовки с ".h" и без. Каждый заголовок библиотеки std доступен в "не суффиксном вкусе". (Заголовок с «.h» является либо заголовком C, либо устаревшим.) Заголовки библиотеки C были адаптированы для C++. У них старое название с префиксом "c" и без суффикса ".h".
    Таким образом, я заменил #include <math.h> на #include <cmath>.

  2. Я не мог не сделать makeEqualLength() немного короче.

  3. Обратите внимание, что многие методы в std используют std::size_t вместо int или unsigned. std::size_t имеет подходящую ширину для выполнения индекса массива и арифметики указателя, т.е. имеет «ширину машинного слова». Я долгое время считал, что int и unsigned также должны иметь "ширину машинного слова" и не заботился о size_t. Когда мы перешли в Visual Studio с x86 (32 бита) на x64 (64 бита), я на горьком опыте понял, что сильно ошибался: std::size_t теперь 64-битное, а int и unsigned по-прежнему 32-битное. (MS VC++ не является исключением. Другие поставщики компиляторов (но не все) делают то же самое.)
    Я вставил несколько приведений типов C, чтобы удалить предупреждения из вывода компилятора. Такие приведения для удаления предупреждений (независимо от того, используете ли вы приведения C или, лучше, приведения C++) всегда следует использовать с осторожностью и понимать как подтверждение: Уважаемый компилятор. Я вижу, что у вас есть опасения, но я (верю) знаю и уверяю вас, что все должно работать нормально.

  4. Я не уверен в вашем намерении использовать long int в некоторых местах. (Возможно, вы перенесли этот код из исходного кода, не заботясь об этом.) Как вы наверняка знаете, фактический размер всех типов int может отличаться, чтобы соответствовать максимальной производительности целевой платформы. Я работаю на Intel-PC с Windows 10, используя Visual Studio. sizeof (int) == sizeof (long int) (32 бита). Это не зависит от того, компилирую ли я код x86 (32 бита) или код x64 (64 бита). То же самое верно для gcc (в моем случае на cygwin), а также на любом Intel-ПК с Linux (AFAIK). Для предоставленного большего типа, чем int, вы должны выбрать long long int.

Я сделал пример сеанса в cygwin в Windows 10 (64-разрядная версия):

$ g++ -std=c++11 -o karatsuba karatsuba.cc 

$ ./karatsuba
0 * 0 = 0 OK.
0 * 3 = 0 OK.
0 * 6 = 0 OK.

и т.д.

999 * 993 = 992007 OK.
999 * 996 = 995004 OK.
999 * 999 = 998001 OK.
0 error(s).

$
person Scheff's Cat    schedule 14.04.2017
comment
спасибо за ваши предложения. Код работает, но не могли бы вы уточнить третий пункт, так как мне трудно найти связь. - person RK21; 14.04.2017
comment
Кроме того, как я могу заставить его работать для умножения 64-значных чисел - person RK21; 14.04.2017
comment
Извините, это вы должны разработать сами. Я бы использовал std::vector для хранения целых чисел с произвольной точностью. Затем я бы изменил алгоритм, чтобы он работал с этими std::vector вместо std::string. Преобразование символов было бы устаревшим. В этом случае смещение будет выполняться простым перемещением элементов вектора вместо pow() или <<. Вы можете представить значения uint64 как цифры в вашем текущем алгоритме. Учтите, что operator * (uint64, uint64) должно вернуть uint128. Может, лучше попробовать сначала с uint32... - person Scheff's Cat; 14.04.2017
comment
Вы также можете заметить это: Как я могу перемножить 64-битные операнды и получить 128-битный результат переносимым образом?. Использование uint32 может быть быстрее, потому что вы можете преобразовать их в uint64, умножить их просто на operator * и разделить возврат на сохраненный результат и перенести с помощью битовых операторов. - person Scheff's Cat; 14.04.2017
comment
Извините, я не прочитал ваш 1-й комментарий полностью. Я просто добавил дополнительное объяснение. - person Scheff's Cat; 14.04.2017