Альтернативный способ вычисления произведения попарных сумм mod 10 ^ 9 + 7 быстрее, чем O (N ^ 2)

Учитывая массив A целых чисел размера N, я хочу вычислить

gif.latex?%5Cleft&space;(%5Cprod_%7B1&space;%5Cleq&space;i&space;<&space;j&space;%5Cleq&space;N%7D%5E%7BN%7D%7B%5Cleft(a_i&space;&plus;&space;a_j%5Cright)%7D%5Cright)&space;%5C,&space;%5Cmathrm%7Bmod%7D&space;%5C,&space;%5Cleft(10%5E9&space;&plus;&space;7%5Cright)

Это было проблемой в прошлом конкурсе между колледжами. Нам пришлось написать программу, которая разрешила бы до 5 экземпляров этой проблемы, с N & le; 200 000 и каждый a i ≤ 200 000, в течение 20 секунд, лимит времени. Ясно, что решение O (N 2) будет проходить через время. Согласно редакция, предполагаемое решение включало полиномиальное умножение с использованием быстрого преобразования Фурье. Я ищу альтернативные алгоритмы, которые решат это быстрее, чем наивный алгоритм O (N 2) без FFT (или NTT). Существуют ли простые и элегантные решения этой проблемы?

Известные факты:

mod может быть "распределен" внутри продукта, потому что (x * y)% m = ((x% m) * (y% m))% m

Обновление: вот файл тестового файла ввода/вывода во время конкурса: если он пройдет через 20 секунд, он будет принят. Вход: https://www.dropbox.com/s/nw81hts9rniter5/algol.in?dl=0 Выход: https://www.dropbox.com/s/kpa7wit35xr4xm4/algol.out?dl=0

Ответы

Ответ 1

Если бы он преподавал еще больше, а Треугольник Pascal не прошел, потому что это привело бы к еще большему количеству операций. К счастью, операция mod может быть перенесена под PI, поэтому вам не нужно использовать большую int, но вместо 64-битной арифметики (или 32-битный modmul).

PI(ai+aj) mod p == PI((ai+aj)mod p) mod p ... 1<=i<j<=n

поэтому наивное решение С++ (где p<2^16) ваша задача требует вместо этого 64-битных переменных (к которым у меня нет доступа к простым термам).

DWORD modpi(DWORD *a,int n,DWORD p)
    {
    int i,j;
    DWORD x=1;
    for (i=0;i<n;i++)
     for (j=i+1;j<n;j++)
        {
        x*=a[i]+a[j];
        x%=p;
        }
    return x;
    }

Теперь p намного больше, затем max(a[i]), чтобы вы могли изменить:

x%=p;

с:

while (x>=p) x-=p;

но в настоящее время CPU это еще медленнее.

Тем не менее этот подход слишком медленный (~280 ms для n=10000). Если мы переупорядочиваем значения (сортируем их), то внезапно все становится намного лучше. Каждое значение, которое в массиве несколько раз приводит к упрощению, поскольку его частичная сумма почти одинакова. Например:

a[] = { 2,3,3,4 }
x = (2+3).(2+3).(2+4)
  . (3+3).(3+4)
  . (3+4)

therms для 3 почти то же самое, поэтому мы можем это использовать. подсчитайте, сколько из тех же a[i] там подсчитывает частичный PI для одного из них. поместите это значение по счетчику и для каждого экземпляра умножите также на a[i]^instance здесь С++ пример:

DWORD modpi1(DWORD *a,int n,DWORD p)
    {
    int i,j,k;
    DWORD x,y,z;
    sort_asc(a,n);
    for (x=1,i=0;i<n;i++)
        {
        // count the values in k
        for (k=1;(i+1<n)&&(a[i]==a[i+1]);i++,k++);
        // compute partial modPI y
        for (y=1,j=i+1;j<n;j++)
            {
            y*=a[i]+a[j];
            y%=p;
            }
        // add the partial modPI y^k;
        for (j=0;j<k;j++)
            {
            x*=y;
            x%=p;
            }
        // add powers for instances of a[i]
        for (k--;k;k--)
         for (j=0;j<k;j++)
            {
            x*=a[i]+a[i];
            x%=p;
            }
        }
    return x;
    }

Это дает вам некоторое ускорение для каждого множественного появления значения в массиве. Но поскольку ваш массив не меньше, чем возможные числа в нем, не ожидайте слишком многого. Для равномерно случайных данных, где max(a[i])~=n и быстрая сортировка - это скорость немного меньше 50%. Но если вы используете простое декомпозицию, например, MSalters, вы можете получить реальное ускорение, потому что тогда частота повторения должна быть намного выше, чем ~1, но для этого потребуется много работы с уравнениями.

Этот код O(N.N'), где N' - количество различных значений в a[]. Вы также можете увеличить это до O(N'.N') с помощью:

  • sort a[i] по сортировке ведра O(n) или быстрой сортировке O(n.log(n))
  • сделать RLE (кодирование длины пробега) O(n)
  • учетная запись также относится к частичной сумме O(N'.N'), где n'<=n

    Простое разложение должно просто изменить n'<= n на n' <<< n.

Здесь некоторые измерения, используя быстрый сортировать полный 32-битный modmul (с использованием 32-битного x86 asm, который значительно замедляет работу моего компилятора). Случайные данные, где max(a[i])~=n:

n=1000;
[   4.789 ms]0: 234954047
[   3.044 ms]1: 234954047
n=10000;
[ 510.544 ms]0: 629694784
[ 330.876 ms]1: 629694784
n=20000;
[2126.041 ms]0: 80700577
[1350.966 ms]1: 80700577

В скобках указано время в [мс], 0: означает наивный подход, 1: означает отсортированную и частичную RLE-разложение PI. Последнее значение является результатом для p=1000000009

Если этого еще недостаточно, то помимо использования DFT/NTT я не вижу другого ускорения.

[Edit1] полная RLE-декомпозиция a[i]

//---------------------------------------------------------------------------
const DWORD p=1000000009;
const int n=10000;
const int m=n;
DWORD a[n];
//---------------------------------------------------------------------------
DWORD modmul(DWORD a,DWORD b,DWORD p)
    {
    DWORD _a,_b;
    _a=a;
    _b=b;
    asm {
        mov    eax,_a
        mov    ebx,_b
        mul    ebx   // H(edx),L(eax) = eax * ebx
        mov    ebx,p
        div    ebx   // eax = H(edx),L(eax) / ebx
        mov    _a,edx// edx = H(edx),L(eax) % ebx
        }
    return _a;
    }
//---------------------------------------------------------------------------
DWORD modpow(DWORD a,DWORD b,DWORD p)
    {   // b is not mod(p) !
    int i;
    DWORD d=1;
    for (i=0;i<32;i++)
        {
        d=modmul(d,d,p);
        if (DWORD(b&0x80000000)) d=modmul(d,a,p);
        b<<=1;
        }
    return d;
    }
//---------------------------------------------------------------------------
DWORD modpi(DWORD *a,int n,DWORD p)
    {
    int i,j,k;
    DWORD x,y;
    DWORD *value=new DWORD[n+1];// RLE value
    int   *count=new int[n+1];  // RLE count
    // O(n) bucket sort a[] -> count[] because max(a[i])<=n
    for (i=0;i<=n;i++) count[i]=0;
    for (i=0;i< n;i++) count[a[i]]++;
    // O(n) RLE packing value[n],count[n]
    for (i=0,j=0;i<=n;i++)
     if (count[i])
        {
        value[j]=    i;
        count[j]=count[i];
        j++;
        } n=j;
    // compute the whole PI to x
    for (x=1,i=0;i<n;i++)
        {
        // compute partial modPI value[i]+value[j] to y
        for (y=1,j=i+1;j<n;j++)
         for (k=0;k<count[j];k++)
          y=modmul(y,value[i]+value[j],p);
        // add the partial modPI y^count[j];
        x=modmul(x,modpow(y,count[i],p),p);
        // add powers for instances of value[i]
        for (j=0,k=1;k<count[i];k++) j+=k;
        x=modmul(x,modpow(value[i]+value[i],j,p),p);
        }
    delete[] value;
    delete[] count;
    return x;
    }
//---------------------------------------------------------------------------

Это даже бит быстрее, поскольку он сортирует в O(n) и RLE в O(n), поэтому это приводит к O(N'.N'). Вы можете воспользоваться более продвинутыми процедурами modmul,modpow, если у вас есть. Но для равномерного распределения значений это все еще не приближается к используемым скоростям.

[edit2] полное RLE-разложение a[i]+a[j]

DWORD modpi(DWORD *a,int n,DWORD p) // full RLE(a[i]+a[j]) O(n'.n') n' <= 2n
    {
    int i,j;
    DWORD x,y;
    DWORD nn=(n+1)*2;
    int   *count=new int[nn+1]; // RLE count
    // O(n^2) bucket sort a[] -> count[] because max(a[i]+a[j])<=nn
    for (i=0;i<=nn;i++) count[i]=0;
    for (i=0;i<n;i++)
     for (j=i+1;j<n;j++)
      count[a[i]+a[j]]++;
    // O(n') compute the whole PI to x
    for (x=1,y=0;y<=nn;y++)
     if (count[y])
      x=modmul(x,modpow(y,count[y],p),p);
    delete[] count;
    return x;
    }
//---------------------------------------------------------------------------

И это еще быстрее приближается к желаемым временам, но все же остается мало.

n=20000
[3129.710 ms]0: 675975480 // O(n^2) naive
[2094.998 ms]1: 675975480 // O(n'.n) partial RLE decomposition of a[i] , n'<= n
[2006.689 ms]2: 675975480 // O(n'.n') full RLE decomposition of a[i] , n'<= n
[ 729.983 ms]3: 675975480 // T(c0.n^2+c1.n') full RLE decomposition of a[i]+a[j] , n'<= 2n , c0 <<< c1

[Edit3] full RLE (a[i]) -> Разделение RLE (a[i]+a[j])

Я объединил все подходы выше и создал гораздо более быструю версию. Алгоритм таков:

  • RLE encode a[i]

    просто создайте гистограмму a[i] по сортировке ведра в O(n), а затем упакуйте в кодирование value[n'],count[n'], так что в массиве нет нулей. Это довольно быстро.

  • преобразовать RLE (a[i]) в RLE (a[i]+a[j])

    просто создайте счетчик каждого терма a[i]+a[j] в окончательном PI, подобном разложению RLE (a[i]+a[j]), но в O(N'.N') без какой-либо временной операции. Да, это квадратично, но на n'<=n и с очень маленьким постоянным временем. Но эта часть является узким местом...

  • вычислить modpi из RLE (a[i]+a[j])

    Это просто modmul/modpow в O(n') наибольшее постоянное время, но низкая сложность, так что все еще очень быстро.

Код С++ для этого:

DWORD modpi(DWORD *a,int n,DWORD p) // T(c0.n+c1.n'.n'+c2.n'') full RLE(a[i]->a[i]+a[j]) n' <= n , n'' <= 2n , c0 <<< c1 << c2
    {
    int i,j,k;
    DWORD x,y;
    DWORD nn=(n+1)*2;
    DWORD *rle_iv =new DWORD[ n+1]; // RLE a[i] value
    int   *rle_in =new int[ n+1];   // RLE a[i] count
    int   *rle_ij=new int[nn+1];    // RLE (a[i]+a[j]) count
    // O(n) bucket sort a[] -> rle_i[] because max(a[i])<=n
    for (i=0;i<=n;i++) rle_in[i]=0;
    for (i=0;i<n;i++)  rle_in[a[i]]++;
    for (x=0,i=0;x<=n;x++)
     if (rle_in[x])
        {
        rle_iv[i]=       x;
        rle_in[i]=rle_in[x];
        i++;
        } n=i;
    // O(n'.n') convert rle_iv[]/in[] to rle_ij[]
    for (i=0;i<=nn;i++) rle_ij[i]=0;
    for (i=0;i<n;i++)
        {
        rle_ij[rle_iv[i]+rle_iv[i]]+=(rle_in[i]*(rle_in[i]-1))>>1; // 1+2+3+...+(rle_iv[i]-1)
        for (j=i+1;j<n;j++)
         rle_ij[rle_iv[i]+rle_iv[j]]+=rle_in[i]*rle_in[j];
        }
    // O(n') compute the whole PI to x
    for (x=1,y=0;y<=nn;y++)
     if (rle_ij[y])
      x=modmul(x,modpow(y,rle_ij[y],p),p);
    delete[] rle_iv;
    delete[] rle_in;
    delete[] rle_ij;
    return x;
    }

И сравнения:

n=10000
[ 751.606 ms] 814157062 O(n^2) naive
[ 515.944 ms] 814157062 O(n'.n) partial RLE(a[i]) n' <= n
[ 498.840 ms] 814157062 O(n'.n') full RLE(a[i]) n' <= n
[ 179.896 ms] 814157062 T(c0.n^2+c1.n') full RLE(a[i]+a[j]) n' <= 2n , c0 <<< c1
[  66.695 ms] 814157062 T(c0.n+c1.n'.n'+c2.n'') full RLE(a[i]->a[i]+a[j]) n' <= n , n'' <= 2n , c0 <<< c1 << c2
n=20000
[ 785.177 ms] 476588184 T(c0.n^2+c1.n') full RLE(a[i]+a[j]) n' <= 2n , c0 <<< c1
[ 255.503 ms] 476588184 T(c0.n+c1.n'.n'+c2.n'') full RLE(a[i]->a[i]+a[j]) n' <= n , n'' <= 2n , c0 <<< c1 << c2
n=100000
[6158.516 ms] 780587335 T(c0.n+c1.n'.n'+c2.n'') full RLE(a[i]->a[i]+a[j]) n' <= n , n'' <= 2n , c0 <<< c1 << c2

В последний раз для этого метода. Удвоение n умножает время выполнения на cca 4 раз. поэтому для n=200000 время выполнения составляет около 24 секунд в моей настройке.

[Edit4] мой NTT для сравнения

Я знаю, что вы хотите избежать FFT, но все же я думаю, что это хорошо для сравнения. Для этого 32-битный NTT. Поскольку он применяется только на гистограмме, которая состоит только из экспонентов, которые имеют всего несколько бит ширины и в основном равны 1, что предотвращает переполнение даже на n=200000. Здесь С++:

DWORD modpi(DWORD *a,int n,int m,DWORD p) // O(n.log(n) RLE(a[i])+NTT convolution
    {
    int i,z;
    DWORD x,y;
    for (i=1;i<=m;i<<=1); m=i<<1;   // m power of 2 > 2*(n+1)
    #ifdef _static_arrays
    m=2*M;
    DWORD rle[2*M];                 // RLE a[i]
    DWORD con[2*M];                 // convolution c[i]
    DWORD tmp[2*M];                 // temp
    #else
    DWORD *rle =new DWORD[m];       // RLE a[i]
    DWORD *con =new DWORD[m];       // convolution c[i]
    DWORD *tmp =new DWORD[m];       // temp
    #endif
    fourier_NTT ntt;
    // O(n) bucket sort a[] -> rle[] because max(a[i])<=n
    for (i=0;i<m;i++) rle[i]=0.0;
    for (i=0;i<n;i++) rle[a[i]]++;

    // O(m.log(m)) NTT convolution
    for (i=0;i<m;i++) con[i]=rle[i];
    ntt.NTT(tmp,con,m);
    for (i=0;i<m;i++) tmp[i]=ntt.modmul(tmp[i],tmp[i]);
    ntt.iNTT(con,tmp,m);
    // O(n') compute the whole PI to x
    for (x=1,i=0;i<m;i++)
        {
        z=con[i];
        if (int(i&1)==0) z-=int(rle[(i+1)>>1]);
        z>>=1; y=i;
        if ((y)&&(z)) x=modmul(x,modpow(y,z,p),p);
        }
    #ifdef _static_arrays
    #else
    delete[] rle;
    delete[] con;
    delete[] tmp;
    #endif
    return x;
    }

Вы можете игнорировать _static_arrays (обрабатывать его, поскольку он не определен), это просто для более простой отладки. Опасайтесь, что свертка ntt.modmul не работает с задачами p, а с NTT по модулю вместо!!! Если вы хотите быть абсолютно уверенным, что это будет работать с более высоким n или разными дистрибутивами данных, используйте 64-битный NTT.

Здесь comaprison к подходу Edit3:

n=200000
[24527.645 ms] 863132560 O(m^2) RLE(a[i]) -> RLE(a[i]+a[j]) m <= n
[  754.409 ms] 863132560 O(m.log(m)) RLE(a[i])+NTT

Как вы можете видеть, я не был слишком далеко от расчетного ~ 24 сек:).

Здесь несколько раз, чтобы сравнить с дополнительными быстрыми методами свертки, я попытался использовать Karatsuba и FastSQR из Быстрое вычисление квадратов bignum, чтобы избежать использования FFT/NTT:

n=10000
[ 749.033 ms] 149252794 O(n^2)        naive
[1077.618 ms] 149252794 O(n'^2)       RLE(a[i])+fast_sqr32
[ 568.510 ms] 149252794 O(n'^1.585)   RLE(a[i])+Karatsuba32
[  65.805 ms] 149252794 O(n'^2)       RLE(a[i]) -> RLE(a[i]+a[j])
[  53.833 ms] 149252794 O(n'.log(n')) RLE(a[i])+FFT
[  34.129 ms] 149252794 O(n'.log(n')) RLE(a[i])+NTT
n=20000
[3084.546 ms] 365847531 O(n^2)        naive
[4311.491 ms] 365847531 O(n'^2)       RLE(a[i])+fast_sqr32
[1672.769 ms] 365847531 O(n'^1.585)   RLE(a[i])+Karatsuba32
[ 238.725 ms] 365847531 O(n'^2)       RLE(a[i]) -> RLE(a[i]+a[j])
[ 115.047 ms] 365847531 O(n'.log(n')) RLE(a[i])+FFT
[  71.587 ms] 365847531 O(n'.log(n')) RLE(a[i])+NTT
n=40000
[12592.250 ms] 347013745 O(n^2)        naive
[17135.248 ms] 347013745 O(n'^2)       RLE(a[i])+fast_sqr32
[5172.836 ms] 347013745 O(n'^1.585)   RLE(a[i])+Karatsuba32
[ 951.256 ms] 347013745 O(n'^2)       RLE(a[i]) -> RLE(a[i]+a[j])
[ 242.918 ms] 347013745 O(n'.log(n')) RLE(a[i])+FFT
[ 152.553 ms] 347013745 O(n'.log(n')) RLE(a[i])+NTT

К сожалению, накладные расходы на Karatsuba слишком велики, поэтому порог выше n=200000 делает это бесполезным для этой задачи.

Ответ 2

Так как ai <= 200.000 и N<=200.000, может быть всего 40.000.000.000 терминов, но вы знаете, что ai + aj <= 400.000. Может быть не более 400 000 уникальных условий. Это на 5 порядков лучше.

Однако большинство этих терминов не являются простыми; там только ~ 40.000 простых чисел под 400.000. Вы можете получить несколько более высокую множественность каждого отдельного термина, но это не очень важно. Вычисление (prime ^ N) по модулю 1000000007 достаточно быстро даже для больших X.

Вы можете разумно предварительно рассчитать факторизацию всех чисел <= 400 000 и получить простые числа <= 400 000 в качестве свободного побочного эффекта.

Этот метод обеспечивает ускорение, потому что мы задерживаем умножение и вместо этого подсчитываем малые простые факторы, найденные при поиске. К тому времени, когда нам нужно делать умножения, мы имеем ряд показателей и можем использовать повторное квадратирование для эффективного их сокращения.

Это противоречиво, возможно, что мы используем простое факторизацию как ускорение, когда "общеизвестный факт" заключается в том, что простая факторизация сложна. Но это возможно, потому что каждый член мал, и нам не раз нужна такая же факторизация.

[править] Из комментариев кажется, что выяснение множественности ai+aj является трудным, так как вы можете рассчитывать только те термины, где i<j. Но это не проблема. Подсчитайте кратность всех членов ai + aj и разделим на две, так как aj + я == ai + aj. Это неверно для диагонали, где i==j. Это фиксируется добавлением кратности всех членов ai + ai до деления на 2.

Ex: a={1 2 3}, термины для рассмотрения: {1+1, 1+2, 1+3, 2+2, 2+3, 3+3} [треугольник]. Множество 4 равно 2 (через 1 + 3 и 2 + 2). Вместо этого рассмотрите {1+1, 1+2, 1+3, 2+1, 2+2, 2+3, 3+1, 3+2, 3+3} [square] + {1+1, 2+2, 3+3} [диагональ]. Множество 4 теперь составляет 4 (1 + 3,2 + 2,3 + 1 и 2 + 2), разделите на 2, чтобы получить правильный результат.

А так как порядок a[] больше не имеет значения для квадратного варианта, вы можете использовать его сортировку. Например. учитывая {4,5,6,5}, получим 4:1, 5:2, 6:1. Таким образом, кратность 10 равна 4+6:1, 5+5:2, 6+4:1