Знаходження трьох елементів у масиві, сума яких найближча до заданого числа


155

Враховуючи масив цілих чисел, A 1 , A 2 , ..., A n , включаючи негативи та позитиви, та інше ціле число S. Тепер нам потрібно знайти три різних цілих числа у масиві, сума яких найближча до заданого цілого числа S Якщо існує більше одного рішення, будь-яке з них нормально.

Можна припустити, що всі цілі числа знаходяться в межах int32_t, і при обчисленні суми не виникатиме арифметичного переповнення. S - це не що інше, як випадкове вибране число.

Чи існує якийсь ефективний алгоритм, окрім грубої сили пошуку, щоб знайти три цілі числа?


1
Якщо ви шукаєте суму, рівну числу (а не найближчої), це буде проблемою 3SUM .
Бернхард Баркер

Відповіді:


186

Чи існує якийсь ефективний алгоритм, окрім грубої сили пошуку, щоб знайти три цілі числа?

Так; ми можемо вирішити це за O (n 2 ) час! По-перше, врахуйте, що вашу проблему Pможна вирівняти рівнозначно дещо по-іншому, що виключає необхідність "цільового значення":

оригінальна проблема P : Дано масив Aз nцілих чисел і заданого значення S, чи існує 3-кортеж від того, Aщо суми в S?

змінена проблема P' : Дано масив Aз nцілих чисел, чи існує 3-кортеж від того, Aщо суми до нуля?

Зверніть увагу , що ви можете перейти від цієї версії проблеми P'з Pвирахуванням вашого S / 3 від кожного елемента A, але тепер вам не потрібно цільове значення більше.

Зрозуміло, що якщо ми просто перевіримо всі можливі 3-кортежі, ми вирішили б проблему в O (n 3 ) - це основна лінія грубої сили. Чи можна зробити краще? Що робити, якщо ми збираємо кортежі дещо розумнішим способом?

По-перше, ми вкладаємо певний час для сортування масиву, що коштує нам початкового штрафу O (n log n). Тепер ми виконуємо цей алгоритм:

for (i in 1..n-2) {
  j = i+1  // Start right after i.
  k = n    // Start at the end of the array.

  while (k >= j) {
    // We got a match! All done.
    if (A[i] + A[j] + A[k] == 0) return (A[i], A[j], A[k])

    // We didn't match. Let's try to get a little closer:
    //   If the sum was too big, decrement k.
    //   If the sum was too small, increment j.
    (A[i] + A[j] + A[k] > 0) ? k-- : j++
  }
  // When the while-loop finishes, j and k have passed each other and there's
  // no more useful combinations that we can try with this i.
}

Цей алгоритм працює шляхом розміщення трьох покажчиків, i, j, і kв різних точках масиву. iпочинається на початку і повільно працює до кінця. kвказує на самий останній елемент.jвказує на те, з чого iпочалося. Ми ітеративно намагаємося підсумовувати елементи за відповідними індексами, і кожного разу відбувається таке:

  • Сума цілком правильна! Ми знайшли відповідь.
  • Сума була занадто мала. Рухатисяj ближче до кінця, щоб вибрати наступне найбільше число.
  • Сума була занадто великою. Рухатисяk ближче до початку, щоб вибрати наступне найменше число.

Для кожного iвказівники jта kпоступово наближатимуться один до одного. Врешті-решт вони пройдуть один одного, і в цей момент нам не потрібно нічого іншого намагатисяi , оскільки ми підсумовуємо ті самі елементи, просто в іншому порядку. Після цього ми пробуємо наступне iі повторюємо.

Зрештою ми або вичерпаємо корисні можливості, або знайдемо рішення. Ви можете бачити, що це О (n 2 ), оскільки ми виконуємо зовнішній цикл O (n) разів і виконуємо внутрішній цикл O (n) разів. Це можна зробити підквадратично, якщо ви по-справжньому фантазії, представляючи кожне ціле число як бітовий вектор і виконуючи швидке перетворення Фур'є, але це виходить за межі цієї відповіді.


Примітка: Оскільки це питання інтерв'ю, я тут трохи обдурив: цей алгоритм дозволяє вибирати один і той же елемент кілька разів. Тобто, (-1, -1, 2) було б дійсним рішенням, як і (0, 0, 0). Він також знаходить лише точні відповіді, а не найближчу відповідь, як зазначає заголовок. Як вправу для читача, я дозволю вам зрозуміти, як змусити його працювати лише з різними елементами (але це дуже проста зміна) і точними відповідями (що також є простою зміною).


8
Здається, алгоритм може знайти лише 3-кортеж, що дорівнює S, не найближчий до S.
ZelluX

7
ZelluX: Як я вже згадував у примітці, я не хотів занадто багато віддавати, оскільки це проблема інтерв'ю. Сподіваємось, ви можете побачити, як його змінити, щоб він отримав найближчу відповідь. (Підказка: один із способів - відстежувати найближчу відповідь до цих пір і переписувати її, якщо ви знайдете кращу.)
Джон Фемінелла

12
що робити, якщо ми не модифікуємо постановку проблеми, натомість будемо шукати aj та ak, що суми до ai + S.
Булевий

3
@ZelluX: Це схоже на те, як працює сортування злиття (саме так воно вперше натиснуло на мене). Те , що це внутрішній цикл намагається зробити , це довести , що або A [J] або [к] не може бути частиною будь-якого задовольняє рішення. Проблема в будь-якій точці: "Чи є пара j '> = j і k' <= k така, що A [j] + A [k] = S - A [i]?" Дивлячись на поточну пару (i, j), є 3 можливості: сума б'ється (стоп - ми виграли!), Вона занадто низька, або вона занадто висока. Якщо вона занадто мала, то сума A [j] + A [k '] також повинна бути занадто низькою для кожного k' <= k, оскільки в кожній такій сумі перший доданок (A [j]) буде однаковим. ..
j_random_hacker

1
... і другий член (A [k ']) буде таким же або навіть нижчим, ніж A [k]. Тож у цьому випадку ми довели, що A [j] не може брати участь у будь-якій задовольняючій сумі - тому ми можемо також відкинути його! Що ми робимо, встановлюючи j = j + 1 і починаючи спочатку (хоча це може допомогти роздумувати над вирішенням меншої субпроблеми замість цього рекурсивно). Так само якщо сума A [j] + A [k] занадто велика, то ми знаємо, що A [j '] + A [k] також повинна бути занадто високою для кожного j'> = j, оскільки A [j '] має бути принаймні таким же великим, як A [j], і ми вже занадто високі. Це означає, що ми можемо безпечно відкинути A [k], встановивши k = k-1 і перейшовши наново.
j_random_hacker

28

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

Ще одне O (n ^ 2) рішення (за допомогою хештету).

// K is the sum that we are looking for
for i 1..n
    int s1 = K - A[i]
    for j 1..i
        int s2 = s1 - A[j]
        if (set.contains(s2))
            print the numbers
    set.add(A[i])

8
Нижня сторона - це O (N) сховище, а не робити це на місці.
Чарльз Мангер

6
Використання хеш-версії не є строгим O (n ^ 2), оскільки хеш-набір може вироджуватися в рідкісних випадках, що призводить до лінійного часу пошуку.
Ext3h

@Charles - Також для рішення Джона потрібен простір O (N), оскільки ви змінюєте початковий масив під час сортування. Це означає, що абоненту може знадобитися захисна копія перед використанням функції.
gamliela

Я думаю, у вашому алгоритмі є помилка. s2може бути вже вибраним елементом. Наприклад, якщо масив є 0,1,2і Kє 2, відповіді не повинно бути. Я думаю, що ваш алгоритм виведе, 0,1,1що, очевидно, неправильно.
Ямча

7

У розчині Джона Фемінелла є помилка.

На лінії

if (A[i] + A[j] + A[k] == 0) return (A[i], A[j], A[k])

Нам потрібно перевірити, чи всі i, j, k є різними. В іншому випадку, якщо мій цільовий елемент 6і якщо мій вхідний масив містить {3,2,1,7,9,0,-4,6}. Якщо я надрукую кортежі, які дорівнюють 6, я також отримаю 0,0,6як вихід. Щоб цього уникнути, нам потрібно змінити умову таким чином.

if ((A[i] + A[j] + A[k] == 0) && (i!=j) && (i!=k) && (j!=k)) return (A[i], A[j], A[k])

2
Рішення Джона Фемінелла - це просто представити алгоритм для вирішення проблеми, він також вказав, що його рішення не працюватиме за умови окремої кількості номерів, і вам доведеться змінити вище коду, який він залишив для читача.
EmptyData

3
Насправді я ніколи не буду j, оскільки ви завжди починаєте його з j = i + 1. Єдине реальне умова, яке ви повинні перевірити, - чи j == k. Однак, встановивши цикл while на j <k, ви вирішили проблеми без тривалого, якщо оператор, оскільки k завжди буде більше j, а j завжди більше, ніж i.
lorenzocastillo

2
Це виглядає не як відповідь на питання, а скоріше як коментар до відповіді Джона Фемінелла.
Бернхард Баркер

6

Як щодо чогось такого, що є O (n ^ 2)

for(each ele in the sorted array)
{
    ele = arr[i] - YOUR_NUMBER;
    let front be the pointer to the front of the array;
    let rear be the pointer to the rear element of the array.;

    // till front is not greater than rear.                    
    while(front <= rear)
    {
        if(*front + *rear == ele)
        {
            print "Found triplet "<<*front<<","<<*rear<<","<<ele<<endl;
            break;
        }
        else
        {
            // sum is > ele, so we need to decrease the sum by decrementing rear pointer.
            if((*front + *rear) > ele)
                decrement rear pointer.
            // sum is < ele, so we need to increase the sum by incrementing the front pointer.
            else
                increment front pointer.
        }
    }

Це визначає, якщо сума 3 елементів точно дорівнює вашій кількості. Якщо ви хочете найближче, ви можете змінити його, щоб запам'ятати найменший дельта (різниця між вашою кількістю поточного триплета), а в кінці роздрукувати триплет, відповідний найменшій дельті.


якщо ви хочете знайти k елементів, щоб отримати суму, яка складність? Як ви з цим справляєтеся?
кодер_15

При такому підході складність для k елементів становить O (n ^ (k-1)) для k> = 2. Вам потрібно додати зовнішню петлю для кожної додаткової суми.
Ext3h

5

Зауважте, що у нас є відсортований масив. Це рішення подібне до рішення Джона лише тим, що воно шукає суму і не повторює той самий елемент.

#include <stdio.h>;

int checkForSum (int arr[], int len, int sum) { //arr is sorted
    int i;
    for (i = 0; i < len ; i++) {
        int left = i + 1;
        int right = len - 1;
        while (right > left) {
            printf ("values are %d %d %d\n", arr[i], arr[left], arr[right]);
            if (arr[right] + arr[left] + arr[i] - sum == 0) {
                printf ("final values are %d %d %d\n", arr[i], arr[left], arr[right]);
                return 1;
            }
            if (arr[right] + arr[left] + arr[i] - sum > 0)
                right--;
            else
                left++;
        }
    }
    return -1;
}
int main (int argc, char **argv) {
    int arr[] = {-99, -45, -6, -5, 0, 9, 12, 16, 21, 29};
    int sum = 4;
    printf ("check for sum %d in arr is %d\n", sum, checkForSum(arr, 10, sum));
}

Це потрібно для обчислення абсолютної різниці a[r] + a[l] + a[i] - sum. Спробуйте arr = [-1, 2, 1, -4] sum = 1.
Димитрій

3

Ось код C ++:

bool FindSumZero(int a[], int n, int& x, int& y, int& z)
{
    if (n < 3)
        return false;

    sort(a, a+n);

    for (int i = 0; i < n-2; ++i)
    {
        int j = i+1;
        int k = n-1;

        while (k >= j)
        {
            int s = a[i]+a[j]+a[k];

            if (s == 0 && i != j && j != k && k != i)
            {
                x = a[i], y = a[j], z = a[k];
                return true;
            }

            if (s > 0)
                --k;
            else
                ++j;
        }
    }

    return false;
}

2

Дуже просте рішення N ^ 2 * logN: сортуйте вхідний масив, потім пройдіть усі пари A i , A j (N ^ 2 раз), і для кожної пари перевірте, чи (S - A i - A j ) у масиві ( час logN).

Інше рішення O (S * N) використовує класичне динамічне програмування підхід до .

Коротко:

Створіть двовимірний масив V [4] [S + 1]. Наповніть його таким чином, щоб:

V [0] [0] = 1, V [0] [x] = 0;

V 1 [A i ] = 1 для будь-якого i, V 1 [x] = 0 для всіх інших x

V [2] [A i + A j ] = 1, для будь-якого i, j. V [2] [x] = 0 для всіх інших x

V [3] [сума будь-яких 3 елементів] = 1.

Щоб заповнити його, повторіть через A i , для кожного A я повторіть масив справа наліво.


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

Масив занадто великий, і це не O (s * N). Цей крок - O (N ^ 2): V [2] [Ai + Aj] = 1, для будь-якого i, j. V [2] [x] = 0 для всіх інших x.
Річард

1

Це можна ефективно вирішити в O (n log (n)) наступним чином. Я даю рішення, яке говорить, чи сума будь-яких трьох чисел дорівнює заданому числу.

import java.util.*;
public class MainClass {
        public static void main(String[] args) {
        int[] a = {-1, 0, 1, 2, 3, 5, -4, 6};
        System.out.println(((Object) isThreeSumEqualsTarget(a, 11)).toString());
}

public static boolean isThreeSumEqualsTarget(int[] array, int targetNumber) {

    //O(n log (n))
    Arrays.sort(array);
    System.out.println(Arrays.toString(array));

    int leftIndex = 0;
    int rightIndex = array.length - 1;

    //O(n)
    while (leftIndex + 1 < rightIndex - 1) {
        //take sum of two corners
        int sum = array[leftIndex] + array[rightIndex];
        //find if the number matches exactly. Or get the closest match.
        //here i am not storing closest matches. You can do it for yourself.
        //O(log (n)) complexity
        int binarySearchClosestIndex = binarySearch(leftIndex + 1, rightIndex - 1, targetNumber - sum, array);
        //if exact match is found, we already got the answer
        if (-1 == binarySearchClosestIndex) {
            System.out.println(("combo is " + array[leftIndex] + ", " + array[rightIndex] + ", " + (targetNumber - sum)));
            return true;
        }
        //if exact match is not found, we have to decide which pointer, left or right to move inwards
        //we are here means , either we are on left end or on right end
        else {

            //we ended up searching towards start of array,i.e. we need a lesser sum , lets move inwards from right
            //we need to have a lower sum, lets decrease right index
            if (binarySearchClosestIndex == leftIndex + 1) {
                rightIndex--;
            } else if (binarySearchClosestIndex == rightIndex - 1) {
                //we need to have a higher sum, lets decrease right index
                leftIndex++;
            }
        }
    }
    return false;
}

public static int binarySearch(int start, int end, int elem, int[] array) {
    int mid = 0;
    while (start <= end) {
        mid = (start + end) >>> 1;
        if (elem < array[mid]) {
            end = mid - 1;
        } else if (elem > array[mid]) {
            start = mid + 1;
        } else {
            //exact match case
            //Suits more for this particular case to return -1
            return -1;
        }
    }
    return mid;
}
}

Я не думаю, що це спрацює. Зрозуміло, у вас є два простих випадки того, як рухатись вперед leftIndexабо rightIndexколи всі елементи посередині або строго менші, або більше, ніж бажане число. Але як бути з випадком, коли двійковий пошук зупинився десь посередині? Вам потрібно буде перевірити обидві гілки (де rightIndex--і leftIndex++). У своєму рішенні ви просто ігноруєте цю ситуацію. Але я не думаю, що є спосіб подолати це питання.
Айвен

0

Зниження: Я думаю, що рішення @John Feminella O (n2) є найелегантнішим. Ми ще можемо зменшити A [n], в якому шукати кортеж. Спостерігаючи A [k], щоб усі елементи були в A [0] - A [k], коли наш пошуковий масив величезний, а SUM (s) дійсно малий.

A [0] - мінімум: - відсортований масив за зростанням.

s = 2A [0] + A [k]: Враховуючи s і A [], ми можемо знайти A [k], використовуючи двійковий пошук у часовій (n) часі.


0

Ось програма в Java, яка є O (N ^ 2)

import java.util.Stack;


public class GetTripletPair {

    /** Set a value for target sum */
    public static final int TARGET_SUM = 32;

    private Stack<Integer> stack = new Stack<Integer>();

    /** Store the sum of current elements stored in stack */
    private int sumInStack = 0;
    private int count =0 ;


    public void populateSubset(int[] data, int fromIndex, int endIndex) {

        /*
        * Check if sum of elements stored in Stack is equal to the expected
        * target sum.
        * 
        * If so, call print method to print the candidate satisfied result.
        */
        if (sumInStack == TARGET_SUM) {
            print(stack);
        }

        for (int currentIndex = fromIndex; currentIndex < endIndex; currentIndex++) {

            if (sumInStack + data[currentIndex] <= TARGET_SUM) {
                ++count;
                stack.push(data[currentIndex]);
                sumInStack += data[currentIndex];

                /*
                * Make the currentIndex +1, and then use recursion to proceed
                * further.
                */
                populateSubset(data, currentIndex + 1, endIndex);
                --count;
                sumInStack -= (Integer) stack.pop();
            }else{
            return;
        }
        }
    }

    /**
    * Print satisfied result. i.e. 15 = 4+6+5
    */

    private void print(Stack<Integer> stack) {
        StringBuilder sb = new StringBuilder();
        sb.append(TARGET_SUM).append(" = ");
        for (Integer i : stack) {
            sb.append(i).append("+");
        }
        System.out.println(sb.deleteCharAt(sb.length() - 1).toString());
    }

    private static final int[] DATA = {4,13,14,15,17};

    public static void main(String[] args) {
        GetAllSubsetByStack get = new GetAllSubsetByStack();
        get.populateSubset(DATA, 0, DATA.length);
    }
}

Хороший підхід, але я не міг зрозуміти, що ви обмежуєте кількість результатів трійкою. Наприклад, розгляньте вхідні дані: [1,11,3,4,5,6,7,8, 2] та суму 12, з вашого рішення видно, що [1, 11] [4,8] [1,4, 5,2] тощо, все працювало б.
Анупам Саїні

0

Проблему можна вирішити в O (n ^ 2) шляхом розширення задачі на 2 суми з незначними модифікаціями. A - вектор, що містить елементи, і B - необхідна сума.

int Рішення :: threeSumClosest (вектор & A, int B) {

sort(A.begin(),A.end());

int k=0,i,j,closest,val;int diff=INT_MAX;

while(k<A.size()-2)
{
    i=k+1;
    j=A.size()-1;

    while(i<j)
    {
        val=A[i]+A[j]+A[k];
        if(val==B) return B;
        if(abs(B-val)<diff)
        {
            diff=abs(B-val);
            closest=val;
        }
        if(B>val)
        ++i;
        if(B<val) 
        --j;
    }
    ++k;

}
return closest;

0

Ось код Python3

class Solution:
    def threeSumClosest(self, nums: List[int], target: int) -> int:
        result = set()
        nums.sort()
        L = len(nums)     
        for i in range(L):
            if i > 0 and nums[i] == nums[i-1]:
                continue
            for j in range(i+1,L):
                if j > i + 1 and nums[j] == nums[j-1]:
                    continue  
                l = j+1
                r = L -1
                while l <= r:
                    sum = nums[i] + nums[j] + nums[l]
                    result.add(sum)
                    l = l + 1
                    while l<=r and nums[l] == nums[l-1]:
                        l = l + 1
        result = list(result)
        min = result[0]
        for i in range(1,len(result)):
            if abs(target - result[i]) < abs(target - min):
                min = result[i]
        return min

-1

Ще одне рішення, яке рано і перевіряє та виходить з ладу:

public boolean solution(int[] input) {
        int length = input.length;

        if (length < 3) {
            return false;
        }

        // x + y + z = 0  => -z = x + y
        final Set<Integer> z = new HashSet<>(length);
        int zeroCounter = 0, sum; // if they're more than 3 zeros we're done

        for (int element : input) {
            if (element < 0) {
                z.add(element);
            }

            if (element == 0) {
                ++zeroCounter;
                if (zeroCounter >= 3) {
                    return true;
                }
            }
        }

        if (z.isEmpty() || z.size() == length || (z.size() + zeroCounter == length)) {
            return false;
        } else {
            for (int x = 0; x < length; ++x) {
                for (int y = x + 1; y < length; ++y) {
                    sum = input[x] + input[y]; // will use it as inverse addition
                    if (sum < 0) {
                        continue;
                    }
                    if (z.contains(sum * -1)) {
                        return true;
                    }
                }
            }
        }
        return false;
    }

Я додав сюди кілька тестів одиниці: GivenArrayReturnTrueIfThreeElementsSumZeroTest .

Якщо набір використовує занадто багато місця, я легко можу використовувати java.util.BitSet, який буде використовувати O (n / w) простір .


-1

Програма для отримання цих трьох елементів. Я тільки спочатку сортував масив / список і оновлював їх minClosenessна основі кожного триплета.

public int[] threeSumClosest(ArrayList<Integer> A, int B) {
    Collections.sort(A);
    int ansSum = 0;
    int ans[] = new int[3];
    int minCloseness = Integer.MAX_VALUE;
    for (int i = 0; i < A.size()-2; i++){
        int j = i+1;
        int k = A.size()-1;
        while (j < k){
            int sum = A.get(i) + A.get(j) + A.get(k);
            if (sum < B){
                j++;
            }else{
                k--;
            }
            if (minCloseness >  Math.abs(sum - B)){
                minCloseness = Math.abs(sum - B);
                ans[0] = A.get(i); ans[1] = A.get(j); ans[2] = A.get(k);
            }
        }
    }
    return ans;
}

-2

Я зробив це в n ^ 3, псевдокод внизу;

// Створіть хеш-карту з ключем як Integer та значенням як ArrayList // повторіть список за допомогою циклу for, для кожного значення в списку повторіть повтор, починаючи з наступного значення;

for (int i=0; i<=arr.length-1 ; i++){
    for (int j=i+1; j<=arr.length-1;j++){

// якщо сума arr [i] та arr [j] менша від потрібної суми, то є потенціал знайти третю цифру, тому зробіть іншу для циклу

      if (arr[i]+arr[j] < sum){
        for (int k= j+1; k<=arr.length-1;k++)

// в цьому випадку ми шукаємо третє значення; якщо сума arr [i] і arr [j] і arr [k] є бажаною сумою, тоді додайте їх до HashMap, зробивши ключ arr [i], а потім додавши arr [j] і arr [k] в ArrayList у значенні цього ключа

          if (arr[i]+arr[j]+arr[k] ==  sum){              
              map.put(arr[i],new ArrayList<Integer>());
              map.get(arr[i]).add(arr[j]);
              map.get(arr[i]).add(arr[k]);}

після цього тепер у вас є словник, який містить усі записи, що представляють три значення, додаючи до потрібної суми. Витягніть всі ці записи за допомогою функцій HashMap. Це спрацювало чудово.

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