Медиана матрицы c отсортированными строками

Я не могу решить следующую проблему оптимально, ни найти подход, чтобы сделать это в любом месте.

учитывая матрицу N × M, в которой сортируется каждая строка, найдите общую медиану матрицы. Предположим, что N * M нечетно.

например,

Матрица =
[1, 3, 5]
[2, 6, 9]
[3, 6, 9]

A = [1, 2, 3, 3, 5, 6, 6, 9, 9]

медиана-5. Итак, мы возвращаемся 5.
Отмечать: Дополнительная память не допускается.

любая помощь будет оценили.

6 ответов


рассмотрим следующий процесс.

  • если рассматривать матрицу N*M как 1-D массив, то медиана является элементом 1+N*M/2 th элемент.

  • тогда рассмотрим x будет медианой, если x является элементом матрицы и количество элементов матрицы ≤ x равно 1 + N*M/2.

  • поскольку элементы матрицы в каждой строке отсортированы, вы можете легко найти количество элементов в каждой строке less than or equals x. Для нахождения в вся матрица, сложность N*log M С помощью бинарного поиска.

  • затем сначала найдите минимальный и максимальный элемент из матрицы N*M. Примените двоичный поиск в этом диапазоне и запустите вышеуказанную функцию для каждого x.

  • если количество элементов в матрице ≤ x is 1 + N*M/2 и x содержит в этой матрице тогда x - это медиана.

вы можете рассмотреть это ниже кода C++:

int median(vector<vector<int> > &A) {
    int min = A[0][0], max = A[0][0];
    int n = A.size(), m = A[0].size();
    for (int i = 0; i < n; ++i) {
        if (A[i][0] < min) min = A[i][0];
        if (A[i][m-1] > max) max = A[i][m-1];
    }

    int element = (n * m + 1) / 2;
    while (min < max) {
        int mid = min + (max - min) / 2;
        int cnt = 0;
        for (int i = 0; i < n; ++i)
            cnt += upper_bound(&A[i][0], &A[i][m], mid) - &A[i][0];
        if (cnt < element)
            min = mid + 1;
        else
            max = mid;
    }
    return min;
}

простое решение o (1) memory-проверить, каждый ли отдельный элемент z - это медиана. Для этого находим позицию z во всех строках, просто накапливая количество элементов меньше, чем z. Это не использует тот факт, что каждая строка сортируется, кроме нахождения позиции z на каждой строке O (log M) времени. Для каждого элемента нам нужно сделать N * log m сравнения, и есть N * M элементы, так это N2M журнал м.


если элементы матрицы являются целыми числами, можно двоично искать медиану, начиная с диапазона матрицы для hi и low. O (n log M log (hi-low)).

в противном случае один из способов, который имеет o(n2log2m) временную сложность wost-case, - это двоичный поиск, O(log m), для каждой строки в свою очередь, O(n), ближайший элемент к общей медиане матрицы слева и ближайший справа, O(N log m), обновляя лучшее до сих пор. Мы знаем, что общая медиана имеет не более floor(m * n / 2) элементы строго меньше, чем он, и что добавление количества элементов меньше, чем он, и количество раз, когда это происходит, может быть не менее floor(m * n / 2) + 1. Мы используем стандартный двоичный поиск в строке и, как указал седая борода, пропускаем тест для элементов за пределами нашего "лучшего" диапазона. Тест на то, насколько близок элемент к общей медиане, включает подсчет того, сколько элементов в каждой строке строго меньше и сколько равно, что достигается в O(n log m) времени n бинарный поиск. После строки сортированные, мы знаем, что большие элементы будут больше " справа "и меньшие элементы больше" слева " по отношению к общей медиане.

если разрешено переставлять матрицу, o (MN log (mn)) временная сложность возможна путем сортировки матрицы на месте (например, с помощью сортировки блоков) и возврата среднего элемента.


существует рандомизированный алгоритм, который решает эту проблему за O (n (log n) (log m)) время. Это алгоритм Лас-Вегаса, что означает, что он всегда дает правильные результаты, но может занять больше времени, чем ожидалось. В этом случае вероятность того, что это займет гораздо больше времени, чем ожидалось, крайне мала.

когда m = 1, эта проблема сводится к проблеме нахождения медианы в массиве только для чтения с использованием постоянного пространства. Эта проблема не имеет известного оптимального решения: см. " Поиск медианы в памяти только для чтения на целочисленном входе, Chan et al."

одна странная вещь об этом уменьшении проблемы, когда m = 1 заключается в том, что это subслучае тоже суперслучай, в котором алгоритм для m = 1 может быть применен к случаю m > 1. Идея состоит в том, чтобы просто забыть, что строки массива сортируются и обрабатывают всю область хранения как несортированный массив размером n * m. Так, например, тривиальный алгоритм для случая m = 1, в который каждый элемент проверяется, чтобы увидеть, является ли он медианой, принимает O (n2) времени. Применяя его, когда m > 1 принимает O (n2m2) времени.

возвращаясь к случаю m = 1, в модели сравнения (в которой элементами массива могут быть целые числа, строки, действительные числа или что-либо еще, что можно сравнить с операторами неравенства""), наиболее известное детерминированное решение, использующее пространство s (где s является константой, т. е. в O(1)) имеет время Θ (2ss!n1 + 1/s), и это сложнее, чем обычные алгоритмы, обсуждаемые на stackoverflow (хотя и не на https://cstheory.stackexchange.com или https://cs.stackexchange.com). Он использует цепную последовательность алгоритмов as, As-1, ... Таким образом,1, гдеs+1 называетs. Вы можете прочитать его в "выбор из памяти только для чтения и сортировки с минимальным движением данных", Munro и Raman.

существует простой рандомизированный алгоритм с меньшим временем выполнения с высокой вероятностью. Для любой константы c этот алгоритм выполняется во времени O (n log n) с вероятностью 1 - O(n - c). Когда массив является матрицей размера n*m, которая работает до O (n m log (n m)).

этот алгоритм очень похож на quickselect без перестановки элементов в разбиение.

import random

def index_range(needle, haystack):
  """The index range' of a value over an array is a pair
  consisting of the number of elements in the array less
  than that value and the number of elements in the array
  less than or equal to the value.
  """
  less = same = 0
  for x in haystack:
    if x < needle: less += 1
    elif x == needle: same += 1
  return less, less + same

def median(xs):
  """Finds the median of xs using O(1) extra space. Does not
  alter xs.
  """
  if not xs: return None
  # First, find the minimum and maximum of the array and
  # their index ranges:
  lo, hi = min(xs), max(xs)
  lo_begin, lo_end = index_range(lo, xs)
  hi_begin, hi_end = index_range(hi, xs)
  # Gradually we will move the lo and hi index ranges closer
  # to the median.
  mid_idx = len(xs)//2
  while True:
    print "range size", hi_begin - lo_end
    if lo_begin <= mid_idx < lo_end:
      return lo
    if hi_begin <= mid_idx < hi_end:
      return hi
    assert hi_begin - lo_end > 0
    # Loop over the array, inspecting each item between lo
    # and hi. This loops sole purpose is to reservoir sample
    # from that set. This makes res a randomly selected
    # element from among those strictly between lo and hi in
    # xs:
    res_size = 0
    res = None
    for x in xs:
      if lo < x < hi:
        res_size += 1
        if 1 == random.randint(1, res_size):
          res = x
    assert res is not None
    assert hi_begin - lo_end == res_size
    # Now find which size of the median res is on and
    # continue the search on the smaller region:
    res_begin, res_end = index_range(res, xs)
    if res_end > mid_idx:
      hi, hi_begin, hi_end = res, res_begin, res_end
    else:
      lo, lo_begin, lo_end = res, res_begin, res_end

он работает, поддерживая верхнюю и нижнюю границы значения медианы. Затем он обходит массив и случайным образом выбирает значение между границами. Это значение заменяет одну из границ и процесс начинается снова.

границы сопровождаются их диапазоном индексов, мера которых индексирует границу, если массив был отсортирован. Как только одна из границ появится в индексе ⌊n / 2⌋, это медиана и алгоритм прекращает.

когда элемент случайно выбран в промежутке между границами, это уменьшает разрыв на 50% в ожидании. Алгоритм завершается (не позднее), когда разрыв равен 0. Мы можем моделировать это как ряд случайных независимых равномерно распределенных переменных Xя из (0,1) такое, что Yk = X1 * X2 * ... * Xk где Xя это соотношение зазора, который остается после раунда Я. Например, если после 10-го раунда разрыв между диапазонами индексов lo и hi - 120, а после 11-го раунда разрыв составляет 90, затем X11 = 0.75. Алгоритм завершается, когда Yk

выберите постоянное положительное целое число k. Давайте ограничим вероятность того, что Yвойти к2n > = 1 / n с использованием границ Чернова. У нас есть Yвойти к2n = X1 * X2 * ... Xвойти к2n, так ln Yвойти к2n = ln X1 + ln X2 + ... + ln Xвойти к2n. Затем граница Чернова дает Pr(ln X1 + ln X2 + ... + ln Xвойти к2n >= ln (1 / n)) t > 0 e - t ln (1/n) (E[et ln X1] * E[et ln X2] * ... * E[et ln Xвойти к2 n]). После некоторого упрощения правая сторона mint > 0 nt (E[X1t] * E[X2t] * ... * E[Xвойти к2 nt]). Поскольку это минимум, и мы ищем верхнюю границу, мы можем ослабьте это, специализируясь на t = 1. Затем он упрощает до n1-k, так как E[Xя] = 1/2.

если мы выберем, например, k = 6, то это ограничивает вероятность того, что есть 6 log2N раундов или более n-5. Так что с вероятностью 1-O (n-5) алгоритм выполняет 6 log2N - 1 или меньше раундов. Это то, что я имею в виду под "с высокой вероятностью" выше.

С каждого раунда проверяет каждый член массива постоянное число раз, каждый раунд занимает линейное время, для общего времени работы O (N log n) с высокой вероятностью. Когда массив не просто массив, а матрица размера n * m, которая работает до O (n m log (n m)).

мы можем сделать значительно лучше, однако, воспользовавшись сортированностью строк. Когда мы работали в одном несортированном массиве, поиск элементов в зазоре, на который я ссылался выше, требовал проверки каждого элемента матрица. В матрице с отсортированными строками элементы зазора расположены в смежном сегменте каждой строки. Каждый сегмент может быть идентифицирован в o(log m) времени с помощью двоичного поиска, поэтому все они могут быть расположены в O(N log m) времени. Отбор проб резервуара теперь занимает O (N log m) времени на итерацию цикла.

другая основная работа, выполняемая в цикле, заключается в определении диапазона индексов элемента из случайно выбранного промежутка. Опять же, поскольку каждая строка отсортирована, диапазон индексов для случайно выбранного элемента в строке можно определить время O (log m). Суммы диапазонов индексов для каждой строки составляют диапазон индексов по всему массиву, поэтому эта часть каждой итерации цикла также занимает только O (n log m) времени.

по тому же аргументу, что и выше, с границей Чернова, есть o (log n) итерации с вероятностью не менее 1-O (n - k) для любой константы k. Таким образом, весь алгоритм занимает O (n (log n) (log m)) время с высоким вероятность.

import bisect
import random

def matrix_index_range(needle, haystack):
  """matrix_index_range calculates the index range of needle
  in a haystack that is a matrix (stored in row-major order)
  in which each row is sorted"""
  n, m = len(haystack), len(haystack[0])
  begin = end = 0;
  for x in haystack:
    begin += bisect.bisect_left(x, needle)
    end += bisect.bisect_right(x, needle)
  return begin, end

def matrix_median(xs):
  print "Starting"
  if not xs or not xs[0]: return None
  n, m = len(xs), len(xs[0])
  lo, hi = xs[0][0], xs[0][m-1]
  for x in xs:
    lo, hi = min(lo, x[0]), max(hi, x[m-1])
  lo_begin, lo_end = matrix_index_range(lo, xs)
  hi_begin, hi_end = matrix_index_range(hi, xs)
  mid_idx = (n * m) // 2
  while True:
    print "range size", hi_begin - lo_end
    if lo_begin <= mid_idx < lo_end:
      return lo
    if hi_begin <= mid_idx < hi_end:
      return hi
    assert hi_begin - lo_end > 0
    mid = None
    midth = random.randint(0, hi_begin - lo_end - 1)
    for x in xs:
      gap_begin = bisect.bisect_right(x, lo)
      gap_end = bisect.bisect_left(x, hi)
      gap_size = gap_end - gap_begin
      if midth < gap_size:
        mid = x[gap_begin + midth]
        break
      midth -= gap_size
    assert mid is not None
    mid_begin, mid_end = matrix_index_range(mid, xs)
    assert lo_end <= mid_begin and mid_end <= hi_begin
    if mid_end > mid_idx:
      hi, hi_begin, hi_end = mid, mid_begin, mid_end
    else:
      lo, lo_begin, lo_end = mid, mid_begin, mid_end

это решение существенно быстрее, чем первое, когда m непостоянно.


я закодировал O (n2 log2 m) временное решение גלררקן, но они попросили меня не добавлять код к их ответу, поэтому вот он как отдельный ответ:

import bisect

def MedianDistance(key, matrix):
  lo = hi = 0
  for row in matrix:
    lo += bisect.bisect_left(row, key)
    hi += bisect.bisect_right(row, key)
  mid = len(matrix) * len(matrix[0]) // 2;
  if hi - 1 < mid: return hi - 1 - mid
  if lo > mid: return lo - mid
  return 0

def ZeroInSorted(row, measure):
  lo, hi = -1, len(row)
  while hi - lo > 1:
    mid = (lo + hi) // 2
    ans = measure(row[mid])
    if ans < 0: lo = mid
    elif ans == 0: return mid
    else: hi = mid

def MatrixMedian(matrix):
  measure = lambda x: MedianDistance(x, matrix)
  for idx, row in enumerate(matrix):
    if not idx & idx-1: print(idx)
    ans = ZeroInSorted(row, measure)
    if ans is not None: return row[ans]

sunkuet02 это с уточнениями и кодом python:
Каждая строка матрицы n×M A занимает и имеет средний элемент, который является его медианой.
Есть, по крайней мере, N*(M+1)/2 элемента не больше, чем максимум привет этих медиан, и по крайней мере N*(M+1)/2 не меньше минимума Ло:
медиана всех элементов A должен быть между Ло и привет, включающий.
Как только известно, что более половины элементов ниже, чем текущий кандидат, последний известен как высокий. Как только остается слишком мало строк, чтобы количество элементов ниже текущего кандидата достигло половины общего числа, кандидат известен как низкий: в обоих случаях немедленно перейдите к следующему кандидату.

from bisect import bisect

def median(A):
    """ returns the median of all elements in A.
        Each row of A needs to be in ascending order. """
    # overall median is between min and max row median
    lo, hi = minimax(A)
    n = len(A)
    middle_row = n // 2
    columns = len(A[0])
    half = (n * columns + 1) // 2
    while lo < hi:
        mid = lo + (hi - lo) // 2
        lower = 0
        # first half can't decide median
        for a in A[:middle_row]:
            lower += bisect(a, mid)
        # break as soon as mid is known to be too high or low
        for r, a in enumerate(A[middle_row:n-1]):
            lower += bisect(a, mid)
            if half <= lower:
                hi = mid
                break
            if lower < r*columns:
                lo = mid + 1
                break
        else: # decision in last row
            lower += bisect(A[n-1], mid)
            if half <= lower:
                hi = mid
            else:
                lo = mid + 1

    return lo


def minmax(x, y):
    """return min(x, y), max(x, y)"""
    if x < y:
        return x, y
    return y, x


def minimax(A):
    """ return min(A[0..m][n//2]), max(A[0..m][n//2]):
        minimum and maximum of medians if A is a
        row major matrix with sorted rows."""
    n = len(A)
    half = n // 2
    if n % 2:
        lo = hi = A[0][half]
    else:
        lo, hi = minmax(A[0][half], A[1][half])
    for i in range(2-n % 2, len(A[0]), 2):
        l, h = minmax(A[i][half], A[i+1][half])
        if l < lo:
            lo = l
        if hi< h:
            hi = h
    return lo, hi


if __name__ =='__main__':
    print(median( [[1, 3, 5], [2, 6, 9], [3, 6, 9]] ))

(Я считаю std::upper_bound() и bisect.bisect() быть эквивалентным (bisect_right() - это псевдоним).)
Для второго кандидат медиана, последняя обработанная строка может быть ниже, чем на первой итерации. В следующих итерациях это число строк никогда не должно уменьшаться - слишком лениво для фактора это in ((переименовать и) увеличить middle_row при необходимости).