Як шукати число у 2d-масиві, відсортованому зліва направо та зверху вниз?


90

Нещодавно мені дали це запитання для співбесіди, і мені цікаво, яким хорошим рішенням було б це.

Скажімо, мені дано двовимірний масив, де всі числа в масиві зростають по порядку зліва направо та зверху вниз.

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

Зараз моє перше схильність - використовувати двійковий пошук, оскільки мої дані сортуються. Я можу визначити, чи є число в одному рядку за час O (log N). Однак саме два напрямки мене відкидають.

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

Хтось має якісь хороші ідеї щодо вирішення цієї проблеми?

Приклад масиву:

Відсортовано зліва направо, зверху вниз.

1  2  4  5  6  
2  3  5  7  8  
4  6  8  9  10  
5  8  9  10 11  

Просте запитання: чи може бути так, що ви можете мати сусіда з однаковим значенням [[1 1][1 1]]:?
Matthieu M.

Відповіді:


115

Ось простий підхід:

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

Для NxMмасиву це виконується O(N+M). Думаю, важко було б зробити краще. :)


Редагувати: Багато хороших дискусій. Я говорив про загальний випадок вище; ясно, що якщо Nабо M маленькі, ви могли б використовувати двійковий пошук підхід , щоб зробити це в чому - то наближається логарифмічна час.

Ось декілька деталей для тих, хто цікавиться:

Історія

Цей простий алгоритм називається Saddleback Search . Це існує якийсь час, і оптимально коли N == M. Деякі посилання:

Однак, коли N < M, інтуїція підказує, що двійковий пошук повинен бути кращим, ніж O(N+M): Наприклад, коли N == 1, чистий двійковий пошук буде виконуватися в логарифмічному, а не лінійному часі.

Найгірший випадок

Річард Берд дослідив цю інтуїцію того, що двійковий пошук може покращити алгоритм Saddleback у статті 2006 року:

Використовуючи досить незвичний розмовний прийом, Птах показує нам, що для N <= Mцієї проблеми нижня межа Ω(N * log(M/N)). Це має сенс, оскільки це дає нам лінійну ефективність коли N == Mі логарифмічну продуктивність коли N == 1.

Алгоритми прямокутних масивів

Один із підходів, який використовує двійковий пошук по рядках, виглядає так:

  1. Почніть з прямокутного масиву де N < M. Скажімо, Nце рядки та Mстовпці.
  2. Виконайте двійковий пошук у середньому рядку для value. Якщо ми знайдемо це, ми закінчимо.
  3. В іншому випадку ми знайшли сусідню пару чисел sі g, де s < value < g.
  4. Прямокутник чисел зверху та ліворуч sменше value, тому ми можемо його усунути.
  5. Прямокутник внизу та праворуч від gбільше, ніж value, тому ми можемо його усунути.
  6. Перейдіть до кроку (2) для кожного з двох залишилися прямокутників.

Що стосується найгіршої складності, цей алгоритм log(M)працює над усуненням половини можливих рішень, а потім рекурсивно два рази викликає дві менші проблеми. Нам доведеться повторювати меншу версію цієї log(M)роботи для кожного рядка, але якщо кількість рядків невелика порівняно з кількістю стовпців, то можливість усунути всі ці стовпці за логарифмічний час починає ставати вартим вартості .

Це надає алгоритму складність T(N,M) = log(M) + 2 * T(M/2, N/2), якою Bird показує себе O(N * log(M/N)).

Інший підхід, опублікований Крейгом Гідні, описує алгоритм, подібний підходу, наведеному вище: він вивчає рядок за раз, використовуючи розмір кроку M/N. Його аналіз показує, що це призводить і до O(N * log(M/N))продуктивності.

Порівняння продуктивності

Аналіз Big-O - це добре, але наскільки добре ці підходи працюють на практиці? У таблиці нижче розглянуто чотири алгоритми для все більш "квадратних" масивів:

продуктивність алгоритму проти квадратності

("Наївний" алгоритм просто здійснює пошук кожного елемента масиву. "Рекурсивний" алгоритм описаний вище. "Гібридний" алгоритм є реалізацією алгоритму Гідні . Для кожного розміру масиву продуктивність вимірювалася шляхом синхронізації кожного алгоритму за встановленим набором з 1000000 випадково сформованих масивів.)

Деякі помітні моменти:

  • Як і слід було очікувати, алгоритми "двійкового пошуку" пропонують найкращу продуктивність для прямокутних масивів, а алгоритм Saddleback найкраще працює для квадратних масивів.
  • Алгоритм Saddleback працює гірше, ніж "наївний" алгоритм для 1-d масивів, імовірно тому, що він робить кілька порівнянь для кожного елемента.
  • Попадання продуктивності, яке алгоритми "двійкового пошуку" приймають для квадратних масивів, імовірно, пов'язано з накладними витратами на повторний двійковий пошук.

Резюме

Розумне використання двійкового пошуку може забезпечити O(N * log(M/N)продуктивність як прямокутних, так і квадратних масивів. Алгоритм O(N + M)"сідла" набагато простіший, але страждає від погіршення продуктивності, оскільки масиви стають дедалі прямокутнішими.


6
застосуйте двійковий пошук до діагональної прогулянки, і ви отримаєте O (logN) або O (logM), залежно від того, що вище.
Анураг,

3
@Anurag - Я не думаю, що складність виходить так добре. Двійковий пошук дасть вам гарне місце для початку, але вам доведеться пройти той чи інший вимір цілим шляхом, і в гіршому випадку ви все одно можете почати в одному куті і закінчити в іншому.
Jeffrey L Whitledge

1
Якщо N = 1 і M = 1000000, я можу зробити краще, ніж O (N + M), Отже, іншим рішенням є застосування двійкового пошуку в кожному рядку, що приносить O (N * log (M)), де N <M, якщо це дає менша константа.
Лука Ране,

1
Я провів кілька тестів, використовуючи як ваш метод, так і метод двійкового пошуку, і опублікував результати ТУТ . Здається, зигзагоподібний метод найкращий, якщо я не зумів належним чином сформувати гірші умови для обох методів.
The111

1
Приємного використання посилань! Однак коли M==Nми хочемо O(N)складності, не O(N*log(N/N))тому, що остання дорівнює нулю. Правильна "уніфікована" різка межа - це O(N*(log(M/N)+1))коли N<=M.
hardmath

35

Ця проблема вимагає Θ(b lg(t))часу, де b = min(w,h)і t=b/max(w,h). Я обговорюю рішення у цій публікації в блозі .

Нижня межа

Противник може змусити алгоритм робити Ω(b lg(t))запити, обмежуючись головною діагоналлю:

Противник, використовуючи головну діагональ

Легенда: білі клітини - це менші предмети, сірі клітини - це більші предмети, жовті клітини - менші або рівні предмети, а оранжеві клітини - більші або однакові елементи. Супротивник змушує рішення бути тією, якою жовтою або оранжевою клітинкою алгоритм виконує останні запити.

Зверніть увагу, що існують bнезалежні відсортовані списки розмірів t, що вимагаютьΩ(b lg(t)) повного виключення запитів.

Алгоритм

  1. (Припустимо без втрати загальності, що w >= h )
  2. Порівняйте цільовий елемент із клітинкою t ліворуч у верхньому правому куті дійсної області
    • Якщо елемент комірки збігається, поверніть поточну позицію.
    • Якщо елемент комірки менше цільового, усуньте решту t комірок у рядку за допомогою двійкового пошуку. Якщо під час цього буде знайдено відповідний елемент, поверніться з його позицією.
    • В іншому випадку елемент комірки перевищує цільовий елемент, виключаючи tкороткі стовпці.
  3. Якщо не залишилося дійсної області, поверніть помилку
  4. Перейдіть до кроку 2

Пошук предмета:

Пошук предмета

Визначення елемента не існує:

Визначення елемента не існує

Легенда: білі клітини - це менші предмети, сірі клітини - це більші предмети, а зелені клітини - рівний елемент.

Аналіз

Є b*tкороткі стовпці для усунення. Є bдовгі ряди для усунення. Усунення довгого ряду коштує O(lg(t))часу. Усунення tкоротких колонок коштує O(1)часу.

У гіршому випадку нам доведеться усунути кожен стовпець і кожен рядок, займаючи час O(lg(t)*b + b*t*1/t) = O(b lg(t)).

Зверніть увагу, що я припускаю lgзатискачі для результату вище 1 (тобто lg(x) = log_2(max(2,x))). Ось чому w=h, маючи на увазі t=1, ми отримуємо очікувану межу O(b lg(1)) = O(b) = O(w+h).

Код

public static Tuple<int, int> TryFindItemInSortedMatrix<T>(this IReadOnlyList<IReadOnlyList<T>> grid, T item, IComparer<T> comparer = null) {
    if (grid == null) throw new ArgumentNullException("grid");
    comparer = comparer ?? Comparer<T>.Default;

    // check size
    var width = grid.Count;
    if (width == 0) return null;
    var height = grid[0].Count;
    if (height < width) {
        var result = grid.LazyTranspose().TryFindItemInSortedMatrix(item, comparer);
        if (result == null) return null;
        return Tuple.Create(result.Item2, result.Item1);
    }

    // search
    var minCol = 0;
    var maxRow = height - 1;
    var t = height / width;
    while (minCol < width && maxRow >= 0) {
        // query the item in the minimum column, t above the maximum row
        var luckyRow = Math.Max(maxRow - t, 0);
        var cmpItemVsLucky = comparer.Compare(item, grid[minCol][luckyRow]);
        if (cmpItemVsLucky == 0) return Tuple.Create(minCol, luckyRow);

        // did we eliminate t rows from the bottom?
        if (cmpItemVsLucky < 0) {
            maxRow = luckyRow - 1;
            continue;
        }

        // we eliminated most of the current minimum column
        // spend lg(t) time eliminating rest of column
        var minRowInCol = luckyRow + 1;
        var maxRowInCol = maxRow;
        while (minRowInCol <= maxRowInCol) {
            var mid = minRowInCol + (maxRowInCol - minRowInCol + 1) / 2;
            var cmpItemVsMid = comparer.Compare(item, grid[minCol][mid]);
            if (cmpItemVsMid == 0) return Tuple.Create(minCol, mid);
            if (cmpItemVsMid > 0) {
                minRowInCol = mid + 1;
            } else {
                maxRowInCol = mid - 1;
                maxRow = mid - 1;
            }
        }

        minCol += 1;
    }

    return null;
}

1
Цікаво і, можливо, частково над головою. Я не знайомий з цим "супротивним" стилем аналізу складності. Чи справді супротивник якось динамічно змінює масив під час пошуку, чи це просто ім’я, яке дається нещастю, з яким ви стикаєтесь у гіршому випадку пошуку?
The111

2
@ The111 Невдача еквівалентна тому, що хтось обирає невдалий шлях, який не порушує баченого на сьогоднішній день, тому обидва ці визначення працюють однаково. У мене насправді виникають проблеми з пошуком посилань, що пояснюють техніку конкретно щодо обчислювальної складності ... Я думав, що це набагато більш відома ідея.
Крейг Гідні

Оскільки log (1) = 0, оцінку складності слід подавати O(b*(lg(t)+1))замість O(b*lg(t)). Хороший запис, особливо. за звернення уваги на "техніку супротивника" у демонстрації "найгіршого випадку".
hardmath

@hardmath Я згадую це у відповіді. Я це трохи прояснив.
Крейг Гідні,

17

Для цієї проблеми я б використав стратегію «розділи і владай», подібно до того, що ти запропонував, але деталі дещо інші.

Це буде рекурсивний пошук піддіапазонів матриці.

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

В іншому випадку, якщо знайдене значення менше значення, яке ви шукаєте, то ви знаєте, що воно не знаходиться у верхньому квадранті та ліворуч від вашої поточної позиції. Тож рекурсивно шукайте два піддіапазони: все (виключно) нижче поточної позиції та все (виключно) праворуч, що знаходиться в або вище поточної позиції.

В іншому випадку (знайдене значення перевищує значення, яке ви шукаєте), ви знаєте, що воно не знаходиться в квадранті нижче і праворуч від вашої поточної позиції. Тож рекурсивно шукайте два піддіапазони: все (виключно) ліворуч від поточної позиції та все (виключно) над поточною позицією, що знаходиться в поточному стовпці або стовпці праворуч.

І ба-да-бінг, ти знайшов це.

Зверніть увагу, що кожен рекурсивний виклик має справу лише з поточним піддіапазоном, а не (наприклад) з ВСІМ рядками над поточною позицією. Тільки в поточному піддіапазоні.

Ось вам псевдокод:

bool numberSearch(int[][] arr, int value, int minX, int maxX, int minY, int maxY)

if (minX == maxX and minY == maxY and arr[minX,minY] != value)
    return false
if (arr[minX,minY] > value) return false;  // Early exits if the value can't be in 
if (arr[maxX,maxY] < value) return false;  // this subrange at all.
int nextX = (minX + maxX) / 2
int nextY = (minY + maxY) / 2
if (arr[nextX,nextY] == value)
{
    print nextX,nextY
    return true
}
else if (arr[nextX,nextY] < value)
{
    if (numberSearch(arr, value, minX, maxX, nextY + 1, maxY))
        return true
    return numberSearch(arr, value, nextX + 1, maxX, minY, nextY)
}
else
{
    if (numberSearch(arr, value, minX, nextX - 1, minY, maxY))
        return true
    reutrn numberSearch(arr, value, nextX, maxX, minY, nextY)
}

+1: Це стратегія O (log (N)), і, отже, таке ж хороше замовлення, як і одержуване.
Рекс Керр,

3
@Rex Kerr - Це схоже на O (log (N)), оскільки це те, що є звичайним двійковим пошуком, однак, зверніть увагу, що потенційно є два рекурсивні виклики на кожному рівні. Це означає, що він набагато гірший, ніж звичайний логарифмічний. Я не вважаю, що гірший випадок є кращим за O (M + N), оскільки потенційно кожен рядок або кожен стовпець потрібно шукати. Я гадаю, що цей алгоритм міг би перевершити найгірший випадок для багатьох значень. І найкраще - це паралелізація, оскільки останнім часом саме туди прямує обладнання.
Jeffrey L Whitledge

1
@JLW: Це O (log (N)) - але насправді O (log_ (4/3) (N ^ 2)) або щось подібне. Дивіться відповідь Сванте нижче. Ваша відповідь насправді однакова (якщо ви мали на увазі рекурсивний спосіб, як я гадаю).
Рекс Керр,

1
@Svante - Підмасиви не перекриваються. У першому варіанті вони не мають спільного елемента y. У другому варіанті вони не мають спільного елемента x.
Jeffrey L Whitledge

1
Я не впевнений, що це логарифмічне. Я обчислив складність, використовуючи приблизне відношення рекуррентності T (0) = 1, T (A) = T (A / 2) + T (A / 4) + 1, де A - область пошуку, і закінчилося з T ( A) = O (Fib (lg (A))), що приблизно дорівнює O (A ^ 0,7) і гірше O (n + m), що становить O (A ^ 0,5). Можливо, я зробив якусь дурну помилку, але схоже, що алгоритм витрачає багато часу, спускаючись по безплідних гілках.
Крейг Гідні,

6

На сьогоднішній день двома основними відповідями є, безперечно, O(log N)"метод ZigZag" та O(N+M)метод двійкового пошуку. Я думав провести тестування, порівнявши два методи з різними настройками. Ось деталі:

У кожному тесті масив дорівнює N x N квадрату, при цьому N варіюється від 125 до 8000 (найбільша, яку міг обробити мій куп JVM). Для кожного розміру масиву я вибрав випадкове місце в масиві, щоб поставити одне 2. Потім я поставив 3скрізь, де можна (праворуч і знизу від 2), а потім заповнив решту масиву1. Деякі з попередніх коментаторів думали, що такий тип налаштування дасть найгірший час роботи обох алгоритмів. Для кожного розміру масиву я вибрав 100 різних випадкових розташувань для 2 (ціль пошуку) і провів тест. Я записав середній час роботи та найгірший час роботи для кожного алгоритму. Оскільки це відбувалося занадто швидко, щоб отримати хороші показники ms на Java, і тому, що я не довіряю Java nanoTime (), я повторював кожен тест 1000 разів лише для того, щоб постійно додавати рівномірний коефіцієнт упередженості. Ось результати:

введіть тут опис зображення

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

Ось код Java:

public class SearchSortedArray2D {

    static boolean findZigZag(int[][] a, int t) {
        int i = 0;
        int j = a.length - 1;
        while (i <= a.length - 1 && j >= 0) {
            if (a[i][j] == t) return true;
            else if (a[i][j] < t) i++;
            else j--;
        }
        return false;
    }

    static boolean findBinarySearch(int[][] a, int t) {
        return findBinarySearch(a, t, 0, 0, a.length - 1, a.length - 1);
    }

    static boolean findBinarySearch(int[][] a, int t,
            int r1, int c1, int r2, int c2) {
        if (r1 > r2 || c1 > c2) return false; 
        if (r1 == r2 && c1 == c2 && a[r1][c1] != t) return false;
        if (a[r1][c1] > t) return false;
        if (a[r2][c2] < t) return false;

        int rm = (r1 + r2) / 2;
        int cm = (c1 + c2) / 2;
        if (a[rm][cm] == t) return true;
        else if (a[rm][cm] > t) {
            boolean b1 = findBinarySearch(a, t, r1, c1, r2, cm - 1);
            boolean b2 = findBinarySearch(a, t, r1, cm, rm - 1, c2);
            return (b1 || b2);
        } else {
            boolean b1 = findBinarySearch(a, t, r1, cm + 1, rm, c2);
            boolean b2 = findBinarySearch(a, t, rm + 1, c1, r2, c2);
            return (b1 || b2);
        }
    }

    static void randomizeArray(int[][] a, int N) {
        int ri = (int) (Math.random() * N);
        int rj = (int) (Math.random() * N);
        a[ri][rj] = 2;
        for (int i = 0; i < N; i++) {
            for (int j = 0; j < N; j++) {
                if (i == ri && j == rj) continue;
                else if (i > ri || j > rj) a[i][j] = 3;
                else a[i][j] = 1;
            }
        }
    }

    public static void main(String[] args) {

        int N = 8000;
        int[][] a = new int[N][N];
        int randoms = 100;
        int repeats = 1000;

        long start, end, duration;
        long zigMin = Integer.MAX_VALUE, zigMax = Integer.MIN_VALUE;
        long binMin = Integer.MAX_VALUE, binMax = Integer.MIN_VALUE;
        long zigSum = 0, zigAvg;
        long binSum = 0, binAvg;

        for (int k = 0; k < randoms; k++) {
            randomizeArray(a, N);

            start = System.currentTimeMillis();
            for (int i = 0; i < repeats; i++) findZigZag(a, 2);
            end = System.currentTimeMillis();
            duration = end - start;
            zigSum += duration;
            zigMin = Math.min(zigMin, duration);
            zigMax = Math.max(zigMax, duration);

            start = System.currentTimeMillis();
            for (int i = 0; i < repeats; i++) findBinarySearch(a, 2);
            end = System.currentTimeMillis();
            duration = end - start;
            binSum += duration;
            binMin = Math.min(binMin, duration);
            binMax = Math.max(binMax, duration);
        }
        zigAvg = zigSum / randoms;
        binAvg = binSum / randoms;

        System.out.println(findZigZag(a, 2) ?
                "Found via zigzag method. " : "ERROR. ");
        //System.out.println("min search time: " + zigMin + "ms");
        System.out.println("max search time: " + zigMax + "ms");
        System.out.println("avg search time: " + zigAvg + "ms");

        System.out.println();

        System.out.println(findBinarySearch(a, 2) ?
                "Found via binary search method. " : "ERROR. ");
        //System.out.println("min search time: " + binMin + "ms");
        System.out.println("max search time: " + binMax + "ms");
        System.out.println("avg search time: " + binAvg + "ms");
    }
}

1
+1 Так, дані. :) Також може бути цікаво побачити, як ці два підходи працюють на масивах NxM, оскільки двійковий пошук здається таким, що він повинен інтуїтивно стати кориснішим, чим більше ми підходимо до одновимірного випадку.
Nate Kohl

5

Це короткий доказ нижньої межі проблеми.

Ви не можете зробити це краще, ніж лінійний час (з точки зору розмірів масиву, а не кількості елементів). У наведеному нижче масиві кожен із елементів, позначених як, *може бути 5 або 6 (незалежно від інших). Отже, якщо ваше цільове значення 6 (або 5), алгоритм повинен вивчити їх усі.

1 2 3 4 *
2 3 4 * 7
3 4 * 7 8
4 * 7 8 9
* 7 8 9 10

Звичайно, це розширюється і на більші масиви. Це означає, що така відповідь є оптимальною.

Оновлення: Як зазначив Джеффрі Л. Уітледж, це оптимально лише як асимптотична нижня межа часу роботи проти розміру вхідних даних (розглядається як одна змінна). Час роботи, який розглядається як функція з двома змінними для обох розмірів масиву, може бути покращений.


Ви не продемонстрували, що така відповідь є оптимальною. Розглянемо, наприклад, масив, що перевищує десять і один мільйон, у якому п’ятий рядок містить значення, що перевищують цільове значення. У цьому випадку запропонований алгоритм здійснить лінійний пошук до 999 995 значень, перш ніж наблизитись до цілі. Алгоритм роздвоєння, такий як мій, буде шукати лише 18 значень перед наближенням до цілі. І він працює (асимптотично) не гірше запропонованого алгоритму у всіх інших випадках.
Джеффрі Л. Уітледж,

@ Джеффрі: Це нижча межа проблеми для песимістичного випадку. Ви можете оптимізувати для хороших входів, але існують входи, де ви не можете зробити краще, ніж лінійні.
Рафал Довгірд,

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

Це показує, що алгоритм повинен приймати час BigOmega (хв (n, m)), а не BigOmega (n + m). Ось чому ви можете зробити набагато краще, коли один вимір значно менший. Наприклад, якщо ви знаєте, що буде лише 1 рядок, ви можете вирішити проблему за логарифмічним часом. Я думаю, що оптимальний алгоритм займе час O (min (n + m, n lg m, m lg n)).
Крейг Гідні,

Відповідно оновлено відповідь.
Рафал Довгірд,

4

Я думаю, ось відповідь, і вона працює для будь-якої сортованої матриці

bool findNum(int arr[][ARR_MAX],int xmin, int xmax, int ymin,int ymax,int key)
{
    if (xmin > xmax || ymin > ymax || xmax < xmin || ymax < ymin) return false;
    if ((xmin == xmax) && (ymin == ymax) && (arr[xmin][ymin] != key)) return false;
    if (arr[xmin][ymin] > key || arr[xmax][ymax] < key) return false;
    if (arr[xmin][ymin] == key || arr[xmax][ymax] == key) return true;

    int xnew = (xmin + xmax)/2;
    int ynew = (ymin + ymax)/2;

    if (arr[xnew][ynew] == key) return true;
    if (arr[xnew][ynew] < key)
    {
        if (findNum(arr,xnew+1,xmax,ymin,ymax,key))
            return true;
        return (findNum(arr,xmin,xmax,ynew+1,ymax,key));
    } else {
        if (findNum(arr,xmin,xnew-1,ymin,ymax,key))
            return true;
        return (findNum(arr,xmin,xmax,ymin,ynew-1,key));
    }
}

1

Цікаве питання. Розгляньте цю ідею - створіть одну межу, де всі цифри будуть більшими, ніж ваша ціль, а іншу, де всі цифри менше, ніж ваша ціль. Якщо щось між ними залишається, це ваша мета.

Якщо я шукаю 3 у вашому прикладі, я читаю перший рядок, поки не наберу 4, тоді шукаю найменше сусіднє число (включаючи діагоналі) більше 3:

1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11

Тепер я роблю те саме для тих чисел менше 3:

1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11

Тепер я запитую, чи є щось у двох межах? Якщо так, це повинно бути 3. Якщо ні, тоді немає 3. Начебто непряме, оскільки я насправді не знаходжу номер, я просто роблю висновок, що воно повинно бути там. Це має додатковий бонус підрахунку ВСІХ 3.

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


Голос проти без коментарів? Я думаю, що це O (N ^ 1/2), оскільки найгірша робота вимагає перевірки діагоналі. Принаймні покажіть мені зустрічний приклад, коли цей метод не працює!
Грембо

+1: приємне рішення ... креативне, і добре, що воно знаходить усі рішення.
Тоні Делрой

1

Двійковий пошук по діагоналі масиву - найкращий варіант. Ми можемо з’ясувати, чи є елемент менше або дорівнює елементам по діагоналі.


0

A. Виконайте двійковий пошук у тих рядках, де міститься цільовий номер.

B. Складіть це графіком: Шукайте число, беручи завжди найменший невідвіданий сусідний вузол і зворотне відстеження, коли виявляється занадто велике число


0

Бінарний пошук був би найкращим підходом, imo. Починаючи з 1/2 х, 1/2 у зменшить її навпіл. Тобто квадрат 5x5 буде приблизно таким як x == 2 / y == 3. Я округлив одне значення вниз і одне значення до кращої зони в напрямку цільового значення.

Для наочності наступна ітерація дасть вам щось на зразок x == 1 / y == 2 АБО x == 3 / y == 5


0

Ну, для початку припустимо, що ми використовуємо квадрат.

1 2 3
2 3 4
3 4 5

1. Пошук квадрата

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

Скажімо, я шукаю, 4наприклад, тоді я в кінцевому підсумку знайду 5в (2,2).

Тоді, я впевнений , що якщо 4в таблиці, знаходиться в положенні або (x,2)або (2,x)з xв [0,2]. Ну, це лише 2 двійкові пошукові запити.

Складність не лякає: O(log(N))(3 двійкові пошуки за діапазонами довжини N)

2. Пошук прямокутника, наївний підхід

Звичайно, стає дещо складніше, коли Nі Mвідрізняються (за допомогою прямокутника), розглянемо цей вироджений випадок:

1  2  3  4  5  6  7  8
2  3  4  5  6  7  8  9
10 11 12 13 14 15 16 17

І скажімо, я шукаю 9... Діагональний підхід все ще хороший, але визначення діагональних змін. Ось моя діагональ [1, (5 or 6), 17]. Скажімо, я взяв [1,5,17], тоді я знаю, що якщо 9є в таблиці, це або в підрозділі:

            5  6  7  8
            6  7  8  9
10 11 12 13 14 15 16

Це дає нам 2 прямокутники:

5 6 7 8    10 11 12 13 14 15 16
6 7 8 9

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

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

  • Застосувати двійковий пошук 10 11 12 13 14 15 16, не знайдено
  • Застосувати двійковий пошук 5 6 7 8, не знайдено
  • Застосувати двійковий пошук 6 7 8 9, не знайдено

Це складно, тому що для отримання хороших показників ви можете розрізняти кілька випадків, залежно від загальної форми ....

3. Пошук у прямокутнику, жорстокий підхід

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

1  2  3  4  5  6  7  8
2  3  4  5  6  7  8  9
10 11 12 13 14 15 16 17
17 .  .  .  .  .  .  17
.                    .
.                    .
.                    .
17 .  .  .  .  .  .  17

Зараз у нас є квадрат.

Звичайно, ми, мабуть, фактично НЕ будемо створювати ці рядки, ми могли б просто наслідувати їх.

def get(x,y):
  if x < N and y < M: return table[x][y]
  else: return table[N-1][M-1]            # the max

тому він поводиться як квадрат, не займаючи більше пам'яті (ціною швидкості, мабуть, залежно від кеш-пам'яті ... ну добре: p)


0

РЕДАГУВАТИ:

Я неправильно зрозумів запитання. Як зазначають коментарі, це працює лише у більш обмеженому випадку.

У такій мові, як C, яка зберігає дані у великому порядку, просто обробляйте їх як 1D-масив розміром n * m та використовуйте двійковий пошук.


Так, навіщо робити це більш складним, ніж повинно бути.
erikkallen,

Масив не відсортовано, тому до нього не можна застосовувати пошук по
смітниках

1
Це спрацює, лише якщо останній елемент кожного рядка перевищує перший елемент наступного рядка, що є набагато більш обмежувальною вимогою, ніж пропонується проблема.
Jeffrey L Whitledge

Дякую, я відредагував свою відповідь. Не читав досить уважно, особливо приклад масиву.
Х'ю Брекетт,

0

У мене є рекурсивне рішення Divide & Conquer. Основна ідея для одного кроку: ми знаємо, що лівий верхній (LU) найменший, а правий нижній (RB) - найбільший номер, тому даний No (N) повинен: N> = LU і N <= RB

ЯКЩО N == LU та N == RB :::: Елемент знайдено та Перервати, що повертає позицію / Індекс Якщо N> = LU та N <= RB = FALSE, No там немає і перервати. Якщо N> = LU і N <= RB = TRUE, розділіть 2D-масив на 4 рівні частини 2D-масиву, кожен логічно. А потім застосуйте той самий алгоритм до всіх чотирьох підмасивів.

Мій Алго правильно, я впровадив його на ПК своїх друзів. Складність: кожні 4 порівняння можуть b використовуватися для виведення загального числа елементів до однієї четвертої в гіршому випадку .. Отже, моя складність становить 1 + 4 x lg (n) + 4 Але справді очікується, що це буде працювати над O (n)

Я думаю, що десь не так у моєму розрахунку складності, будь ласка, виправте, якщо так ..


0

Оптимальне рішення - почати з верхнього лівого кута, який має мінімальне значення. Рухайтеся по діагоналі вниз праворуч, поки не потрапите на елемент, значення якого = = значення даного елемента. Якщо значення елемента дорівнює значенню даного елемента, повертається як true.

Інакше звідси ми можемо рухатись двома шляхами.

Стратегія 1:

  1. Перемістіться вгору у стовпці та шукайте даний елемент, поки не дійдемо до кінця. Якщо знайдено, повернення знайдено як істинне
  2. Рухайтеся ліворуч у рядку та шукайте даний елемент, поки не дійдемо до кінця. Якщо знайдено, повернення знайдено як істинне
  3. повернення визнано помилковим

Стратегія 2: Нехай i позначає індекс рядка, а j - індекс стовпця діагонального елемента, на якому ми зупинилися. (Тут маємо i = j, до речі). Нехай k = 1.

  • Повторюйте наведені нижче дії, поки ik> = 0
    1. Шукати, чи дорівнює [ik] [j] даному елементу. якщо так, повернення визнано істинним.
    2. Шукати, якщо [i] [jk] дорівнює даному елементу. якщо так, повернення визнано істинним.
    3. Приріст k

1 2 4 5 6
2 3 5 7 8
4 6 8 9 10
5 8 9 10 11


0
public boolean searchSortedMatrix(int arr[][] , int key , int minX , int maxX , int minY , int maxY){

    // base case for recursion
    if(minX > maxX || minY > maxY)
        return false ;
    // early fails
    // array not properly intialized
    if(arr==null || arr.length==0)
        return false ;
    // arr[0][0]> key return false
    if(arr[minX][minY]>key)
        return false ;
    // arr[maxX][maxY]<key return false
    if(arr[maxX][maxY]<key)
        return false ;
    //int temp1 = minX ;
    //int temp2 = minY ;
    int midX = (minX+maxX)/2 ;
    //if(temp1==midX){midX+=1 ;}
    int midY = (minY+maxY)/2 ;
    //if(temp2==midY){midY+=1 ;}


    // arr[midX][midY] = key ? then value found
    if(arr[midX][midY] == key)
        return true ;
    // alas ! i have to keep looking

    // arr[midX][midY] < key ? search right quad and bottom matrix ;
    if(arr[midX][midY] < key){
        if( searchSortedMatrix(arr ,key , minX,maxX , midY+1 , maxY))
            return true ;
        // search bottom half of matrix
        if( searchSortedMatrix(arr ,key , midX+1,maxX , minY , maxY))
            return true ;
    }
    // arr[midX][midY] > key ? search left quad matrix ;
    else {
         return(searchSortedMatrix(arr , key , minX,midX-1,minY,midY-1));
    }
    return false ;

}

0

Я пропоную зберегти всі символи в 2D list . потім знайдіть індекс необхідного елемента, якщо він існує у списку.

Якщо немає, надрукуйте відповідне повідомлення, інакше надрукуйте рядок і стовпець як:

row = (index/total_columns) і column = (index%total_columns -1)

Це потягне лише двійковий час пошуку у списку.

Будь ласка, запропонуйте будь-які виправлення. :)


0

Якщо O (M log (N)) нормально для масиву MxN -

template <size_t n>
struct MN * get(int a[][n], int k, int M, int N){
  struct MN *result = new MN;
  result->m = -1;
  result->n = -1;

  /* Do a binary search on each row since rows (and columns too) are sorted. */
  for(int i = 0; i < M; i++){
    int lo = 0; int hi = N - 1;
    while(lo <= hi){
      int mid = lo + (hi-lo)/2;
      if(k < a[i][mid]) hi = mid - 1;
      else if (k > a[i][mid]) lo = mid + 1;
      else{
        result->m = i;
        result->n = mid;
        return result;
      }
    }
  }
  return result;
}

Робоча демонстрація на C ++.

Будь ласка, дайте мені знати, якщо це не спрацює, чи є помилка.


0

Я задавав це питання в інтерв’ю протягом більшої частини десятиліття, і я думаю, що лише одна людина змогла придумати оптимальний алгоритм.

Моє рішення завжди було:

  1. Двійковий пошук середньої діагоналі, тобто діагоналі, що проходить вниз і праворуч, що містить елемент на (rows.count/2, columns.count/2).

  2. Якщо цільовий номер знайдено, поверніть true.

  3. В іншому випадку будуть знайдені два числа ( uі v), які uменші за ціль, vбільші за ціль і vодне право та одне вниз від u.

  4. Рекурсивний пошук підматриці праворуч uі зверху vта тієї, що знаходиться знизу uта зліва від v.

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

Ось код у (швидше за все, не дуже Swifty) Swift:

import Cocoa

class Solution {
    func searchMatrix(_ matrix: [[Int]], _ target: Int) -> Bool {
        if (matrix.isEmpty || matrix[0].isEmpty) {
            return false
        }

        return _searchMatrix(matrix, 0..<matrix.count, 0..<matrix[0].count, target)
    }

    func _searchMatrix(_ matrix: [[Int]], _ rows: Range<Int>, _ columns: Range<Int>, _ target: Int) -> Bool {
        if (rows.count == 0 || columns.count == 0) {
            return false
        }
        if (rows.count == 1) {
            return _binarySearch(matrix, rows.lowerBound, columns, target, true)
        }
        if (columns.count == 1) {
            return _binarySearch(matrix, columns.lowerBound, rows, target, false)
        }

        var lowerInflection = (-1, -1)
        var upperInflection = (Int.max, Int.max)
        var currentRows = rows
        var currentColumns = columns
        while (currentRows.count > 0 && currentColumns.count > 0 && upperInflection.0 > lowerInflection.0+1) {
            let rowMidpoint = (currentRows.upperBound + currentRows.lowerBound) / 2
            let columnMidpoint = (currentColumns.upperBound + currentColumns.lowerBound) / 2
            let value = matrix[rowMidpoint][columnMidpoint]
            if (value == target) {
                return true
            }

            if (value > target) {
                upperInflection = (rowMidpoint, columnMidpoint)
                currentRows = currentRows.lowerBound..<rowMidpoint
                currentColumns = currentColumns.lowerBound..<columnMidpoint
            } else {
                lowerInflection = (rowMidpoint, columnMidpoint)
                currentRows = rowMidpoint+1..<currentRows.upperBound
                currentColumns = columnMidpoint+1..<currentColumns.upperBound
            }
        }
        if (lowerInflection.0 == -1) {
            lowerInflection = (upperInflection.0-1, upperInflection.1-1)
        } else if (upperInflection.0 == Int.max) {
            upperInflection = (lowerInflection.0+1, lowerInflection.1+1)
        }

        return _searchMatrix(matrix, rows.lowerBound..<lowerInflection.0+1, upperInflection.1..<columns.upperBound, target) || _searchMatrix(matrix, upperInflection.0..<rows.upperBound, columns.lowerBound..<lowerInflection.1+1, target)
    }

    func _binarySearch(_ matrix: [[Int]], _ rowOrColumn: Int, _ range: Range<Int>, _ target: Int, _ searchRow : Bool) -> Bool {
        if (range.isEmpty) {
            return false
        }

        let midpoint = (range.upperBound + range.lowerBound) / 2
        let value = (searchRow ? matrix[rowOrColumn][midpoint] : matrix[midpoint][rowOrColumn])
        if (value == target) {
            return true
        }

        if (value > target) {
            return _binarySearch(matrix, rowOrColumn, range.lowerBound..<midpoint, target, searchRow)
        } else {
            return _binarySearch(matrix, rowOrColumn, midpoint+1..<range.upperBound, target, searchRow)
        }
    }
}

-1

Дано квадратну матрицю наступним чином:

[abc]
[def]
[ijk]

Ми знаємо, що a <c, d <f, i <k. Ми не знаємо, чи d <c чи d> c тощо. Ми маємо гарантії лише в 1-вимірі.

Переглядаючи кінцеві елементи (c, f, k), ми можемо зробити своєрідний фільтр: чи N <c? пошук (): наступний (). Таким чином, у нас є n ітерацій по рядках, при цьому кожен рядок приймає або O (log (n)) для двійкового пошуку, або O (1), якщо відфільтровано.

Дайте мені ПРИКЛАД, де N = j,

1) Перевірте рядок 1. j <c? (ні, йти далі)

2) Перевірте рядок 2. j <f? (так, пошук у сміттєвих ящиках нічого не дає)

3) Перевірте рядок 3. j <k? (так, пошук по коду знаходить)

Спробуйте ще раз з N = q,

1) Перевірте рядок 1. q <c? (ні, йти далі)

2) Перевірте рядок 2. q <f? (ні, йти далі)

3) Перевірте рядок 3. q <k? (ні, йти далі)

Напевно, є краще рішення, але це легко пояснити .. :)


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