Найти все подмассивы фиксированной длины с заданным рейтингом

У меня есть массив чисел, например:

A = [1, 5, 2, 4, 3]

и массив, который определяет ранг, например:

B = [0, 2, 1]

Моя цель состоит в том, чтобы найти все подмассивы A, которые "подчиняются" рангу B. Если подмассив подчиняется рангу, это означает, что i-й наименьший элемент подмассива должен иметь B[i] качестве (подмассивного) индекса. Таким образом, для соответствия подмассива наименьший элемент в нем должен быть в позиции 0, второй наименьший элемент должен быть в позиции 2, а самый большой элемент должен быть в позиции 1.

Так, например, здесь, есть два подмассива A, которые соответствуют ранжированию: [1, 5, 2] (потому что A [0] <A [2] <A [1]) и [2, 4, 3].

До сих пор мне удалось найти решение, которое имеет временную сложность O(mn) (m is len (A) и n is len (B)), оно перебирает все подмассивы длины 3 и проверяет, являются ли они правильно заказано:

A = [1, 5, 2, 4, 3]
B = [0, 2, 1]
m = len(A)
n = len(B)
for i in range(m - n + 1):
    current_subarray = A[i:i + n]
    # we now do n - 1 comparaisons to check whether the subarray is correctly ordered.
    for B_index in range(n - 1):
        if current_subarray[B[B_index]] > current_subarray[B[B_index + 1]]:
            break
    else:
        print("Subarray found:", current_subarray)

Результат:

Subarray found: [1, 5, 2]
Subarray found: [2, 4, 3]

Это работает, но мне было интересно, был ли лучше оптимизированный алгоритм (лучше, чем O(mn)) для выполнения этой задачи.

Ответы

Ответ 1

Вместо того, чтобы перебирать B для сравнения рангов, вы можете использовать scipy.stats.rankdata чтобы получить ранги напрямую:

from scipy.stats import rankdata

A = [1, 5, 2, 4, 3]
B = [0, 2, 1]

m = len(A)
n = len(B)

for i in range(m - n + 1):
    current_subarray = A[i:i + n]

    ranked_numbers = (rankdata(current_subarray).astype(int) - 1).tolist()
    if ranked_numbers == B:
        print("Subarray found:", current_subarray)

# Subarray found: [1, 5, 2]
# Subarray found: [2, 4, 3]

Примечание: rankdata() начинает ранги с 1 вместо 0, поэтому вышеупомянутые минусы 1 от каждого элемента в массиве numpy.

Ответ 2

Здесь решение numpy основано на некоторой линейной алгебре.

Сначала конвертируем B в основу:

import numpy as np
A = [1, 5, 2, 4, 3]
B = [0, 2, 1]

b = np.eye(len(B))[B]
print(b)
#array([[1, 0, 0],
#       [0, 0, 1],
#       [0, 1, 0]])

Теперь мы можем пройти через каждый массив A и спроецировать его в это пространство. Если результирующий вектор отсортирован, это означает, что подрешетка следовала за ранжированием.

for i in range(0, (len(A) - len(B))+1):
    a = np.array(A[i:i+len(B)])
    if (np.diff(a.dot(b))>0).all():
        print(a)
#[1 5 2]
#[2 4 3]

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


Обновление, здесь более чистая версия:

def get_ranked_subarrays(A, B):
    m = len(A)
    n = len(B)
    b = np.eye(n)[B]
    a = np.array([A[i:i+n] for i in range(0, m - n+1)])
    return a[(np.diff(a.dot(b))>0).all(1)].tolist()

A = [1, 5, 2, 4, 3]
B = [0, 2, 1]
get_ranked_subarrays(A, B)
#[[1, 5, 2], [2, 4, 3]]

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

Ваше решение очень хорошо для малых n, но решение с клочками выигрывает, когда размер A становится большим:

Вот ваш код, который я превратил в функцию, которая возвращает нужные подмассивы (вместо печати):

def get_ranked_subarrays_op(A, B):
    m = len(A)
    n = len(B)
    out = []
    for i in range(m - n + 1):
        current_subarray = A[i:i + n]
        # we now do n - 1 comparisons to check whether the subarray is correctly ordered.
        for B_index in range(n - 1):
            if current_subarray[B[B_index]] > current_subarray[B[B_index + 1]]:
                break
        else:
            out.append(current_subarray)
    return out

Сроки результаты для большого случайного A:

array_size = 1000000
A = np.random.randint(low=0, high=10, size=array_size)
B = [0, 2, 1]

%%timeit
get_ranked_subarrays_op(A, B)
#1 loop, best of 3: 1.57 s per loop

%%timeit
get_ranked_subarrays(A, B)
#1 loop, best of 3: 890 ms per loop

Однако, если m также становится большим, ваше решение будет немного лучше из-за короткого замыкания (вероятность короткого замыкания становится большой для больших m). Вот временные результаты того, что мы позволим m быть 100.

array_size = 1000000
basis_size = 100
A = np.random.randint(low=0, high=10, size=array_size)
B = range(basis_size)
np.random.shuffle(B)

%%timeit
get_ranked_subarrays_op(A, B)
#1 loop, best of 3: 1.9 s per loop

%%timeit
get_ranked_subarrays(A, B)
#1 loop, best of 3: 2.79 s per loop

Ответ 3

Вы можете зациклить A и проверить получившиеся подмассивы:

A, B = [1, 5, 2, 4, 3], [0, 2, 1]
def results(a, b):
   _l = len(b)
   for c in range(len(a)-_l+1):
     _r = a[c:c+_l]
     new_r = [_r[i] for i in b]
     if all(new_r[i] < new_r[i+1] for i in range(len(new_r)-1)):
       yield _r

print(list(results(A, B)))

Выход:

[[1, 5, 2], [2, 4, 3]]

Ответ 4

По крайней мере, мы могли бы намного быстрее исключить окна-кандидаты, рассматривая (двоичное) отношение соседних элементов, что могло бы позволить параллельное исследование. Звоните less than 0 и greater than 1. Затем:

A = [1,  5,  2,  4,  3]
A'=   [0,  1,  0,  1]

B'=   [0,  1]
B = [0,  2,  1]

Очевидно, что любой кандидат должен соответствовать последовательности отношений. Также обратите внимание, что единственным типом раздела B который может допускать перекрытие, является восходящая или нисходящая последовательность (означает, что мы можем пропустить априори, если совпадение найдено).