Враховуючи масив чисел, повернути масив продуктів усіх інших чисел (без поділу)


186

Мені було задано це питання на співбесіді, і я хотів би знати, як інші вирішуватимуть це питання. Мені найбільше подобається Java, але рішення іншими мовами вітаються.

Давши масив чисел, numsповерніть масив чисел products, де products[i]добуток усіх nums[j], j != i.

Input : [1, 2, 3, 4, 5]
Output: [(2*3*4*5), (1*3*4*5), (1*2*4*5), (1*2*3*5), (1*2*3*4)]
      = [120, 60, 40, 30, 24]

Ви повинні зробити це, O(N)не використовуючи поділ.


49
Це питання виникало кілька разів за останній тиждень або близько того; Ви всі опитуєтесь в одній компанії? :)
Майкл Мрозек

Зараз я переглядаю [interview-questions]тег, який шукаю. Чи є у вас посилання, якщо ви знайшли його?
полігенмастильні матеріали

2
@Michael: Це питання дозволяє розділити. Шахта явно забороняє це. Я б сказав, що це два різні питання.
полігенмастильні матеріали

8
Замініть поділ на log (a / b) = log (a) -log (b) та voila!
ldog

1
уявіть, якщо в масиві є 1 або більше 1 нулів, як ви будете працювати з випадком ??
gst

Відповіді:


257

Пояснення методу полігенегенних мастил : фокус полягає в побудові масивів (у випадку з 4 елементами)

{              1,         a[0],    a[0]*a[1],    a[0]*a[1]*a[2],  }
{ a[1]*a[2]*a[3],    a[2]*a[3],         a[3],                 1,  }

І те й інше можна виконати в O (n), починаючи відповідно з лівого та правого краю.

Потім множення двох елементів масиву на елементи дає необхідний результат

Мій код виглядатиме приблизно так:

int a[N] // This is the input
int products_below[N];
p=1;
for(int i=0;i<N;++i) {
  products_below[i]=p;
  p*=a[i];
}

int products_above[N];
p=1;
for(int i=N-1;i>=0;--i) {
  products_above[i]=p;
  p*=a[i];
}

int products[N]; // This is the result
for(int i=0;i<N;++i) {
  products[i]=products_below[i]*products_above[i];
}

Якщо вам також потрібно бути O (1) у просторі, ви можете це зробити (що є менш зрозумілим IMHO)

int a[N] // This is the input
int products[N];

// Get the products below the current index
p=1;
for(int i=0;i<N;++i) {
  products[i]=p;
  p*=a[i];
}

// Get the products above the curent index
p=1;
for(int i=N-1;i>=0;--i) {
  products[i]*=p;
  p*=a[i];
}

4
Це O (n) час виконання, але це також O (n) у просторовій складності. Це можна зробити в просторі O (1). Я маю на увазі, крім розміру вхідних та вихідних контейнерів, звичайно.
wilhelmtell

8
Дуже розумний! Чи існує назва цього алгоритму?
fastcodejava

2
@MichaelAnderson Чудовий чоловік, але скажіть, будь ласка, головну логіку цього і як ви почали це, як тільки отримаєте вимогу.
ACBalaji

3
Алгоритм вийде з ладу, якщо будь-який з елементів дорівнює 0. Тому не забудьте перевірити 0, щоб пропустити.
Мані

2
@Mani Алгоритм відмінний, якщо є елементи, встановлені на 0. Однак, можливо, сканувати вхід для таких елементів і бути ефективнішим, якщо вони будуть знайдені. Якщо є два нульові елементи, то весь результат дорівнює нулю, а якщо є лише один, скажіть, v_i=0єдиний ненульовий запис у результаті - i-й елемент. Однак я підозрюю, що додавання пропуску для виявлення та підрахунку нульових елементів може погіршити чіткість рішення, і, ймовірно, не призведе до реального збільшення продуктивності у більшості випадків ..
Майкл Андерсон

52

Ось невелика рекурсивна функція (в C ++), щоб зробити модифікацію на місці. Хоча для O (n) додаткового місця (у стеці). Якщо припустити, що масив знаходиться в a, а N утримує довжину масиву, ми маємо

int multiply(int *a, int fwdProduct, int indx) {
    int revProduct = 1;
    if (indx < N) {
       revProduct = multiply(a, fwdProduct*a[indx], indx+1);
       int cur = a[indx];
       a[indx] = fwdProduct * revProduct;
       revProduct *= cur;
    }
    return revProduct;
}

Хтось може пояснити цю рекурсію?
nikhil

1
@nikhil Спочатку робить рекурсію, запам'ятовуючи проміжні продукти, з часом формуючи продукт кількості num[N-1]; потім на зворотному шляху він обчислює другу частину множення, яка потім використовується для зміни масиву на місці.
Ja͢ck

уявіть, якщо в масиві є 1 або більше 1 нулів, як ви будете працювати з випадком ??
gst

18

Ось моя спроба вирішити це на Java. Вибачте за нестандартне форматування, але в коді багато дублювання, і це найкраще, що я можу зробити, щоб зробити його читабельним.

import java.util.Arrays;

public class Products {
    static int[] products(int... nums) {
        final int N = nums.length;
        int[] prods = new int[N];
        Arrays.fill(prods, 1);
        for (int
           i = 0, pi = 1    ,  j = N-1, pj = 1  ;
           (i < N)         && (j >= 0)          ;
           pi *= nums[i++]  ,  pj *= nums[j--]  )
        {
           prods[i] *= pi   ;  prods[j] *= pj   ;
        }
        return prods;
    }
    public static void main(String[] args) {
        System.out.println(
            Arrays.toString(products(1, 2, 3, 4, 5))
        ); // prints "[120, 60, 40, 30, 24]"
    }
}

Петльові інваріанти є pi = nums[0] * nums[1] *.. nums[i-1]і pj = nums[N-1] * nums[N-2] *.. nums[j+1]. iЧастина зліва є «префікс» логіка, а jчастина по праву є «суфікс» логікою.


Рекурсивний однолінійний

Джасмет дав (прекрасне!) Рекурсивне рішення; Я перетворив це на цей (огидний!) Однояйцевий Java. Це робить модифікацію на місці з O(N)тимчасовим простором у стеці.

static int multiply(int[] nums, int p, int n) {
    return (n == nums.length) ? 1
      : nums[n] * (p = multiply(nums, nums[n] * (nums[n] = p), n + 1))
          + 0*(nums[n] *= p);
}

int[] arr = {1,2,3,4,5};
multiply(arr, 1, 0);
System.out.println(Arrays.toString(arr));
// prints "[120, 60, 40, 30, 24]"

3
Я думаю, що цикл із 2 змінними ускладнює розуміння, ніж це потрібно (принаймні для мого бідного мозку!), Два окремі цикли також будуть виконувати цю роботу.
Гійом

Тому я розділив код ліворуч / праворуч, прагнучи показати, що вони незалежні один від одного. Я не впевнений, чи справді це працює, хоча =)
полігеноматеріали

15

Переклад рішення Майкла Андерсона на Haskell:

otherProducts xs = zipWith (*) below above

     where below = scanl (*) 1 $ init xs

           above = tail $ scanr (*) 1 xs

13

Покірно обійти правило "без поділів":

sum = 0.0
for i in range(a):
  sum += log(a[i])

for i in range(a):
  output[i] = exp(sum - log(a[i]))

2
Чіплятися: наскільки я знаю, комп'ютери реалізувати логарифми , використовуючи їх біноміальний розкладання - що робить вимагають поділів ...

10

Ось простий і чистий розчин зі складністю O (N):

int[] a = {1,2,3,4,5};
    int[] r = new int[a.length];
    int x = 1;
    r[0] = 1;
    for (int i=1;i<a.length;i++){
        r[i]=r[i-1]*a[i-1];
    }
    for (int i=a.length-1;i>0;i--){
        x=x*a[i];
        r[i-1]=x*r[i-1];
    }
    for (int i=0;i<r.length;i++){
        System.out.println(r[i]);
    }

6

C ++, O (n):

long long prod = accumulate(in.begin(), in.end(), 1LL, multiplies<int>());
transform(in.begin(), in.end(), back_inserter(res),
          bind1st(divides<long long>(), prod));

9
поділ не дозволений
Майкл Андерсон

Це все ще дивовижно виглядає код. Заперечуючи, що він використовує поділ, я все-таки буду схвалювати, якщо давати пояснення.
полігенмастильні матеріали

Чорт, я не читав питання наскрізь. : s Пояснення @polygenelubricants: ідея зробити це у два етапи. Спочатку візьмемо факторіал першої послідовності чисел. Ось що робить алгоритм накопичення (за замовчуванням додає числа, але може замінити додавання будь-яку іншу двійкову операцію, в даному випадку множення). Далі я повторював над вхідною послідовністю вдруге, перетворюючи її таким чином, що відповідний елемент у вихідній послідовності факторіал I обчислював на попередньому етапі, поділений на відповідний елемент у послідовності введення.
wilhelmtell

1
"факториал першої послідовності"? wtf? я мав на увазі добуток елементів послідовності.
wilhelmtell

5
  1. Подорожуйте ліворуч-> Праворуч і продовжуйте зберігати продукт. Назвіть це минулим. -> O (n)
  2. Подорожі праворуч -> ліворуч тримайте виріб Називай це майбутнє. -> O (n)
  3. Результат [i] = Минуле [i-1] * майбутнє [i + 1] -> O (n)
  4. Минуле [-1] = 1; і майбутнє [n + 1] = 1;

O (n)


3

Ось моє рішення в сучасному C ++. Це використовує std::transformі запам'ятовується досить легко.

Інтернет-код (wandbox).

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

using namespace std;

vector<int>& multiply_up(vector<int>& v){
    v.insert(v.begin(),1);
    transform(v.begin()+1, v.end()
             ,v.begin()
             ,v.begin()+1
             ,[](auto const& a, auto const& b) { return b*a; }
             );
    v.pop_back();
    return v;
}

int main() {
    vector<int> v = {1,2,3,4,5};
    auto vr = v;

    reverse(vr.begin(),vr.end());
    multiply_up(v);
    multiply_up(vr);
    reverse(vr.begin(),vr.end());

    transform(v.begin(),v.end()
             ,vr.begin()
             ,v.begin()
             ,[](auto const& a, auto const& b) { return b*a; }
             );

    for(auto& i: v) cout << i << " "; 
}

2

Це O (n ^ 2), але f # настільки красиво:

List.fold (fun seed i -> List.mapi (fun j x -> if i=j+1 then x else x*i) seed) 
          [1;1;1;1;1]
          [1..5]

Я не впевнений, що або величезний один вкладиш, або рішення O (n ^ 2) для проблеми O (n) коли-небудь "прекрасні".
Божевільний фізик

2

Попередньо підрахуйте добуток чисел зліва та праворуч від кожного елемента. Для кожного елемента бажане значення - це добуток продуктів його сусідів.

#include <stdio.h>

unsigned array[5] = { 1,2,3,4,5};

int main(void)
{
unsigned idx;

unsigned left[5]
        , right[5];
left[0] = 1;
right[4] = 1;

        /* calculate products of numbers to the left of [idx] */
for (idx=1; idx < 5; idx++) {
        left[idx] = left[idx-1] * array[idx-1];
        }

        /* calculate products of numbers to the right of [idx] */
for (idx=4; idx-- > 0; ) {
        right[idx] = right[idx+1] * array[idx+1];
        }

for (idx=0; idx <5 ; idx++) {
        printf("[%u] Product(%u*%u) = %u\n"
                , idx, left[idx] , right[idx]  , left[idx] * right[idx]  );
        }

return 0;
}

Результат:

$ ./a.out
[0] Product(1*120) = 120
[1] Product(1*60) = 60
[2] Product(2*20) = 40
[3] Product(6*5) = 30
[4] Product(24*1) = 24

(ОНОВЛЕННЯ: зараз я придивляюсь ближче, для цього використовується той самий метод, що і Майкл Андерсон, Даніель Міговський та полігенімастичні матеріали)


Як називається цей алгоритм?
onepiece

1

Хитрий:

Використовуйте наступне:

public int[] calc(int[] params) {

int[] left = new int[n-1]
in[] right = new int[n-1]

int fac1 = 1;
int fac2 = 1;
for( int i=0; i<n; i++ ) {
    fac1 = fac1 * params[i];
    fac2 = fac2 * params[n-i];
    left[i] = fac1;
    right[i] = fac2; 
}
fac = 1;

int[] results = new int[n];
for( int i=0; i<n; i++ ) {
    results[i] = left[i] * right[i];
}

Так, я впевнений, що я пропустив якийсь i-1 замість i, але це спосіб його вирішити.


1

Існує також неоптимальне рішення O (N ^ (3/2)) . Хоча це досить цікаво.

Спочатку попередньо обробіть кожне часткове множення розміру N ^ 0,5 (це робиться за часовою складністю O (N)). Тоді, обчислення для інших значень-кратних для кожного числа може бути здійснено за 2 * O (N ^ 0,5) час (чому? Тому що вам потрібно помножити лише останні елементи інших ((N ^ 0,5) - 1) чисел, і помножте результат на ((N ^ 0,5) - 1) числа, що належать до групи поточного числа). Роблячи це для кожного числа, можна отримати час O (N ^ (3/2)).

Приклад:

4 6 7 2 3 1 9 5 8

часткові результати: 4 * 6 * 7 = 168 2 * 3 * 1 = 6 9 * 5 * 8 = 360

Щоб обчислити значення 3, потрібно помножити значення інших груп 168 * 360, а потім на 2 * 1.


1
public static void main(String[] args) {
    int[] arr = { 1, 2, 3, 4, 5 };
    int[] result = { 1, 1, 1, 1, 1 };
    for (int i = 0; i < arr.length; i++) {
        for (int j = 0; j < i; j++) {
            result[i] *= arr[j];

        }
        for (int k = arr.length - 1; k > i; k--) {
            result[i] *= arr[k];
        }
    }
    for (int i : result) {
        System.out.println(i);
    }
}

Це рішення я придумав, і мені стало так зрозуміло, що ви думаєте !?


1
Здається, ваше рішення має складну часову складність O (n ^ 2).
Божевільний фізик

1
def productify(arr, prod, i):
    if i < len(arr):
            prod.append(arr[i - 1] * prod[i - 1]) if i > 0 else prod.append(1)
            retval = productify(arr, prod, i + 1)
            prod[i] *= retval
            return retval * arr[i]
    return 1

arr = [1, 2, 3, 4, 5] prod = [] productify (arr, prod, 0) друкувати prod


1

Щоб заповнити ось код Scala:

val list1 = List(1, 2, 3, 4, 5)
for (elem <- list1) println(list1.filter(_ != elem) reduceLeft(_*_))

Це дозволить роздрукувати наступне:

120
60
40
30
24

Програма відфільтрує поточний елем (_! = Elem); і помножити новий список методом reduLeft. Я думаю, що це буде O (n), якщо ви використовуєте view Scala або Iterator для ледачого eval.


Незважаючи на те, що це дуже елегантно, він не працює, якщо є більше елементів з однаковим значенням: val list1 = Список (1, 7, 3, 3, 4, 4)
Giordano Scalzo

Я знову перевірив код з повторюваними значеннями. Він дає наступні 1008 144 112 112 63 63 Я думаю, що це правильно для даного елемента.
Біллз

1

Виходячи з відповіді Білца - вибачте, що не можу коментувати, але ось версія scala, яка правильно обробляє дублікати елементів у списку, і, ймовірно, O (n):

val list1 = List(1, 7, 3, 3, 4, 4)
val view = list1.view.zipWithIndex map { x => list1.view.patch(x._2, Nil, 1).reduceLeft(_*_)}
view.force

повертає:

List(1008, 144, 336, 336, 252, 252)

1

Додавши сюди своє рішення Javascript, оскільки я не знайшов того, хто б це запропонував. Що поділити, окрім підрахунку кількості разів, коли можна дістати число з іншого числа? Я пройшов обчислення добутку всього масиву, а потім перейдіть по кожному елементу і піддаючи поточний елемент до нуля:

//No division operation allowed
// keep substracting divisor from dividend, until dividend is zero or less than divisor
function calculateProducsExceptCurrent_NoDivision(input){
  var res = [];
  var totalProduct = 1;
  //calculate the total product
  for(var i = 0; i < input.length; i++){
    totalProduct = totalProduct * input[i];
  }
  //populate the result array by "dividing" each value
  for(var i = 0; i < input.length; i++){
    var timesSubstracted = 0;
    var divisor = input[i];
    var dividend = totalProduct;
    while(divisor <= dividend){
      dividend = dividend - divisor;
      timesSubstracted++;
    }
    res.push(timesSubstracted);
  }
  return res;
}

1

Я використовую C #:

    public int[] ProductExceptSelf(int[] nums)
    {
        int[] returnArray = new int[nums.Length];
        List<int> auxList = new List<int>();
        int multTotal = 0;

        // If no zeros are contained in the array you only have to calculate it once
        if(!nums.Contains(0))
        {
            multTotal = nums.ToList().Aggregate((a, b) => a * b);

            for (int i = 0; i < nums.Length; i++)
            {
                returnArray[i] = multTotal / nums[i];
            }
        }
        else
        {
            for (int i = 0; i < nums.Length; i++)
            {
                auxList = nums.ToList();
                auxList.RemoveAt(i);
                if (!auxList.Contains(0))
                {
                    returnArray[i] = auxList.Aggregate((a, b) => a * b);
                }
                else
                {
                    returnArray[i] = 0;
                }
            }
        }            

        return returnArray;
    }

1

Ми можемо спочатку виключити nums[j](де j != i) зі списку, а потім отримати продукт решти; Далі python wayслід вирішити цю головоломку:

from functools import reduce
def products(nums):
    return [ reduce(lambda x,y: x * y, nums[:i] + nums[i+1:]) for i in range(len(nums)) ]
print(products([1, 2, 3, 4, 5]))

[out]
[120, 60, 40, 30, 24]

0

Ну, це рішення можна вважати рішенням C / C ++. Скажімо, у нас є масив "a", що містить n елементів на зразок [n], тоді псевдо-код буде, як показано нижче.

for(j=0;j<n;j++)
  { 
    prod[j]=1;

    for (i=0;i<n;i++)
    {   
        if(i==j)
        continue;  
        else
        prod[j]=prod[j]*a[i];
  }

0

Ще одне рішення, Використання поділу. з подвійним обходом. Помножте всі елементи, а потім починайте ділити його на кожен елемент.


0
{-
Рекурсивне рішення з використанням підмножини sqrt (n). Працює в O (n).

Рекурсивно обчислює рішення на підмножинах sqrt (n) розміру sqrt (n). 
Потім повторюється сума продуктів кожного підмножини.
Потім для кожного елемента кожного підмножини він обчислює добуток
сума продуктів усіх інших продуктів.
Потім вирівнюється всі підмножини.

Повторення часу виконання T (n) = sqrt (n) * T (sqrt (n)) + T (sqrt (n)) + n

Припустимо, що T (n) ≤ cn в O (n).

T (n) = sqrt (n) * T (sqrt (n)) + T (sqrt (n)) + n
    ≤ sqrt (n) * c * sqrt (n) + c * sqrt (n) + n
    ≤ c * n + c * sqrt (n) + n
    ≤ (2c + 1) * n
    ∈ O (n)

Зауважте, що стеля (sqrt (n)) можна обчислити за допомогою двійкового пошуку 
і ітерації O (logn), якщо інструкція sqrt не дозволена.
-}

otherProducts [] = []
otherProducts [x] = [1]
otherProducts [x, y] = [y, x]
otherProducts a = foldl '(++) [] вирішено $ zipWith (\ sp -> map (* p) s) Підмножина підмножиниOtherProducts
    де 
      n = довжина a

      - Розмір підмножини Потрібно, щоб 1 <s <n.
      s = стеля $ sqrt $ fromIntegral n

      freedSubsets = відобразити інші підмножини продуктів
      subsetOtherProducts = otherProducts $ підмножини продукту

      підмножини = зворотний $ цикл a []
          де цикл [] acc = соотв
                loop a ac = цикл (drop sa) ((take sa): acc)

0

Ось мій код:

int multiply(int a[],int n,int nextproduct,int i)
{
    int prevproduct=1;
    if(i>=n)
        return prevproduct;
    prevproduct=multiply(a,n,nextproduct*a[i],i+1);
    printf(" i=%d > %d\n",i,prevproduct*nextproduct);
    return prevproduct*a[i];
}

int main()
{
    int a[]={2,4,1,3,5};
    multiply(a,5,1,0);
    return 0;
}

0

Ось трохи функціональний приклад із використанням C #:

            Func<long>[] backwards = new Func<long>[input.Length];
            Func<long>[] forwards = new Func<long>[input.Length];

            for (int i = 0; i < input.Length; ++i)
            {
                var localIndex = i;
                backwards[i] = () => (localIndex > 0 ? backwards[localIndex - 1]() : 1) * input[localIndex];
                forwards[i] = () => (localIndex < input.Length - 1 ? forwards[localIndex + 1]() : 1) * input[localIndex];
            }

            var output = new long[input.Length];
            for (int i = 0; i < input.Length; ++i)
            {
                if (0 == i)
                {
                    output[i] = forwards[i + 1]();
                }
                else if (input.Length - 1 == i)
                {
                    output[i] = backwards[i - 1]();
                }
                else
                {
                    output[i] = forwards[i + 1]() * backwards[i - 1]();
                }
            }

Я не зовсім впевнений, що це O (n), завдяки напіврекурсії створених Funcs, але мої тести, схоже, вказують на те, що це O (n) в часі.


0

// Це рекурсивне рішення на Java // Викликається як основний продукт (a, 1,0);

public static double product(double[] a, double fwdprod, int index){
    double revprod = 1;
    if (index < a.length){
        revprod = product2(a, fwdprod*a[index], index+1);
        double cur = a[index];
        a[index] = fwdprod * revprod;
        revprod *= cur;
    }
    return revprod;
}

0

Акуратне рішення з O (n) тривалістю виконання:

  1. Для кожного елемента обчисліть добуток усіх елементів, що виникають до цього, і він зберігається в масиві "до".
  2. Для кожного елемента обчисліть добуток усіх елементів, що виникають після цього елемента, і збережіть його у масиві "post"
  3. Створіть остаточний масив "результат" для елемента i,

    result[i] = pre[i-1]*post[i+1];
    

1
Це те саме рішення, що і прийняте, правда?
Томас Ейл

0
function solution($array)
{
    $result = [];
    foreach($array as $key => $value){
        $copyOfOriginalArray = $array;
        unset($copyOfOriginalArray[$key]);
        $result[$key] = multiplyAllElemets($copyOfOriginalArray);
    }
    return $result;
}

/**
 * multiplies all elements of array
 * @param $array
 * @return int
 */
function multiplyAllElemets($array){
    $result = 1;
    foreach($array as $element){
        $result *= $element;
    }
    return $result;
}

$array = [1, 9, 2, 7];

print_r(solution($array));

0

Ось ще одна проста концепція, яка вирішує проблему O(N).

        int[] arr = new int[] {1, 2, 3, 4, 5};
        int[] outArray = new int[arr.length]; 
        for(int i=0;i<arr.length;i++){
            int res=Arrays.stream(arr).reduce(1, (a, b) -> a * b);
            outArray[i] = res/arr[i];
        }
        System.out.println(Arrays.toString(outArray));

0

У мене є рішення із складністю O(n)простору та O(n^2)часу, наведеними нижче,

public static int[] findEachElementAsProduct1(final int[] arr) {

        int len = arr.length;

//        int[] product = new int[len];
//        Arrays.fill(product, 1);

        int[] product = IntStream.generate(() -> 1).limit(len).toArray();


        for (int i = 0; i < len; i++) {

            for (int j = 0; j < len; j++) {

                if (i == j) {
                    continue;
                }

                product[i] *= arr[j];
            }
        }

        return product;
    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.