Предполагая, что существует одномерный массив/список, который определяет цветовой градиент, я хотел бы использовать его для создания двумерного цветового градиента следующим образом:
Давайте для простоты заменим информацию о цвете одним числовым значением для примера одномерного массива/списка:
[ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ]
Чтобы градиент прогрессировал по диагонали с продвижением наибольшего следующего значения по диагонали по всему массиву, я хотел бы преобразовать 1-D последовательность в 2D-массив с намеренно выбранной формой (т. е. шириной/высотой, т. е. количеством строк x числом столбцы, где строка * столбцы == длина одномерного градиентного массива) следующим образом:
[[ 1 2 4 ]
[ 3 6 7 ]
[ 5 9 10 ]
[ 8 12 13 ]
[ 11 14 15 ]]
или
[[ 1 2 4 7 10 ]
[ 3 6 9 12 13 ]
[ 5 8 11 14 15 ]]
или начиная с последовательности:
[ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16]
к
[[ 1 2 4 7 ]
[ 3 6 9 11 ]
[ 5 10 13 14 ]
[ 8 12 15 16 ]]
Существует ли готовый к использованию готовый модуль Python или C-библиотека, способный выполнить такое изменение формы массива, или этот особый случай необходимо закодировать вручную? И если необходимо кодирование циклов вручную, какой способ сделать это будет наиболее эффективным, поскольку размер последовательности, которую я хотел бы преобразовать, составляет 256³? Может быть, я уже нашел готовый к использованию код для такого изменения/преобразования в глубоком пространстве Интернета, который мне не удалось найти, спрашивая ни поисковые системы, ни LLM?
@AndrewBloom: вектор 1d может быть чем угодно... числами, кортежами, строками...






Следующий подход использует numpy для создания 2D-массива с индексами в 1D-массиве. Имейте в виду, что для больших массивов требуется значительный объем памяти.
import numpy as np
def create_diagonal_index(rows, cols):
# Index array for the diagonals. Needed to build more arrays with information about the diagonals
diag_index = np.arange(rows + cols - 1)
# Lookup table with number of items a diagonal has
diag_count = np.arange(1, rows + cols)
diag_count = np.fmin(diag_count, diag_count[::-1])
diag_count[diag_count > min(rows, cols)] = min(rows, cols)
# Index into 1D-array of first item in each diagonal
# (I guess there is a faster way to do this)
diag_start_index = np.zeros(rows + cols, dtype=int)
diag_start_index[1:] = np.cumsum(diag_count)
# First row for each diagonal
diag_startrow = np.where(diag_index < cols, 0, diag_index + 1 - cols)
# Row of the middle position of each diagonal
diag_midpos = (diag_count + 1) // 2 + diag_startrow
rowpos = np.arange(rows).reshape(rows, 1)
colpos = np.arange(cols).reshape(1, cols)
# Map each array item to the index of the diagonal it is contained in
diag = np.lib.stride_tricks.sliding_window_view(diag_index, cols)
# Calculate delta relative to diag_start_index for each array item
index_delta = np.fmin(np.fmin(rowpos, colpos), np.fmin(rows - rowpos - 1, cols - colpos - 1))
index_delta *= 2
index_delta = np.where(rowpos >= diag_midpos[diag], index_delta + 1, index_delta)
# Reuse array and add start index
index_delta += diag_start_index[diag]
return index_delta
print(create_diagonal_index(11, 6))
Выход:
[[ 0 1 3 6 10 15]
[ 2 5 8 12 17 21]
[ 4 9 14 19 23 27]
[ 7 13 20 25 29 33]
[11 18 26 31 35 39]
[16 24 32 37 41 45]
[22 30 38 43 47 51]
[28 36 44 49 53 56]
[34 42 50 55 58 60]
[40 48 54 59 62 63]
[46 52 57 61 64 65]]
Для точек на квадрате вы можете вычислить исходный индекс в списке, обратным преобразованием одной ячейки в O(1), даже не сохраняя ее. Один из способов — изменить основу, чтобы u представлялось треугольными цифрами. Я начал с четверти плоскости (Python3):
def inverse_diagonal(x: int, y: int) -> int:
u = x + y
return u * (u + 1) / 2 + y
Нужная вам форма более сложна, x+y=constant ее можно рассматривать как перестановку диапазона между бесконечной четвертьплоскостью с двумя конечными сторонами. Я использовал принцип включения-исключения, чтобы вычесть части диагонали, образовав прямоугольник. Порядок: минус верхняя часть, выходная середина, минус нижняя часть (отсюда и неравенства). Чересстрочная перестановка выполняется с помощью линейного преобразования, разделенного на нижнюю половину и верхнюю половину.
def inverse_diagonal(x: (int, int), max: (int, int)) -> int:
"""
Given a rectangle (0,0) -- [`max`], for point `x`, calculate the inverse
mapping of interlaced diagonal stripes, to a linear array.
"""
assert(x[0] < max[0] and x[1] < max[1])
# Change basis so 1st horizontally is triangular number.
u = x[0] + x[1]
area_x_axis = u * (u + 1) // 2
area_y_axis = (u + 1) * (u + 2) // 2
# Subtract the ends by the inclusion-exclusion principle.
subtract = 0;
if u >= max[0]:
subtract_side = u - max[0] + 1
subtract += subtract_side*(subtract_side + 1) // 2
if u > max[1]:
subtract_side = u - max[1]
subtract += subtract_side*(subtract_side + 1) // 2
area_x_axis -= subtract
area_y_axis -= subtract
# Interlace the diagonal.
range0 = area_x_axis + (u >= max[0]) * (u - max[0] + 1)
range1 = area_y_axis - (u >= max[1]) * (u - max[1] + 1)
without_interlace = area_x_axis + x[1]
if (without_interlace - range0 > (range1 - range0 - 1) // 2):
ray = (range1 - without_interlace - 1) * 2 + 1 + range0
else: # top half and diagonal
ray = (without_interlace - range0) * 2 + range0
return ray
list = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]
for max in ((3, 5), (5, 3), (4, 4)):
for y in range(max[1]):
for x in range(max[0]):
print("{:4d}, ".format(list[inverse_diagonal((x, y), max)]), end='')
print("")
print("")
Я предполагаю, что отсутствие смежности между ячейками на практике делает его медленнее, чем другие подходы, требующие больше памяти. Однако я чувствую, что этот подход будет хорошим кандидатом на распараллеливание , поскольку ячейки больше не зависят друг от друга. Таким образом, я думаю, что при больших размерах входных данных было бы полезно применить аппаратное ускорение, используя, например, PyOpenCl , PyCUDA или CuPy.
Помните help(sorted), пробуйте help(inverse_diagonal). Поместите /a строку документации в функцию.
@greybeard Это очень круто! чему-то научился.
См. мой «ответ», чтобы узнать время сравнения кода, представленного в других ответах на ваш код. Не стесняйтесь предоставить просто ради этого попытку ускорить код сверх скорости самого быстрого подхода (предоставленного Vitalizzare).
Насколько я вижу, это можно сделать в три этапа:
Я думаю, будет достаточно найти точки остановки, чтобы мы могли нарезать последовательность с их помощью. Для этого мы можем применить накопительную сумму к последовательности диагональных длин:
import numpy as np
from numba import njit
@njit
def flat_diagonal_stops(height, width):
'''Return a sequence of breakpoints separating a sequence
of length height*width into a sequence of matrix diagonals
of the shape (height, width)
'''
min_dim = min(height, width)
lengths = np.empty(height + width, dtype='int')
lengths[:min_dim] = [*range(min_dim)] # diagonal lengths in the lower triangle
lengths[min_dim:1-min_dim] = min_dim # diagonal lengths in the main body
lengths[:-min_dim:-1] = lengths[1:min_dim] # diagonal lengths in the upper triangle
return lengths.cumsum()
Преобразование последовательности, подобное этому:
(0, 1, 2, 3, 4, 5) >>> (0, 2, 4, 5, 3, 1)
на самом деле это разделение элементов по четности их позиционных индексов. Элементы с четным индексом сдвигаются влево, а остальные — вправо в обратном порядке:
@njit
def separate_by_index_parity(arr):
'''Return a numpy.ndarray filled with elements of arr,
first those in even-numbered positions,
then those in odd-numbered positions in reverse order
'''
out = np.empty_like(arr)
middle = sum(divmod(len(out), 2))
out[:middle] = arr[::2]
out[:middle-len(out)-1:-1] = arr[1::2]
return out
Для этого мы можем создать плоское представление требуемого результата и работать с ним, разрезая диагональные позиции:
@njit
def assemble_diagonals_separated_by_parity(arr, height, width):
'''Return a matrix of shape (height, width) with elements
of the given sequence arr arranged along diagonals,
where the elements on each diagonal are separated
by the parity of their index in them
'''
out = np.empty(height*width, dtype=arr.dtype)
stops = flat_diagonal_stops(height, width)
out_step = width + 1
for offset, (start, stop) in enumerate(zip(stops[:-1], stops[1:]), 1-height):
# out_from: the first element of an off-diagonal
# out_to : next after the last element of an off-diagonal
# out_step: a stride to get diagonal items
out_from = -offset*width if offset < 0 else offset
out_to = out_from + (stop-start)*out_step # stop - start is equal to the diagonal size
out[out_from:out_to:out_step] = separate_by_index_parity(arr[start:stop])
return out.reshape(height, width)
В результате получается укладка модифицированной последовательности по диагоналям снизу вверх и слева направо. Чтобы получить другие типы укладки, мы комбинируем переворачивание и транспонирование. Например, мы можем располагать элементы в порядке слева направо и сверху вниз по антидиагоналям следующим образом (обратите внимание на обратный порядок размеров (width, height) в вызове функции):
height, width = 6, 4
arr = np.arange(1, 1+height*width)
out = np.fliplr(assemble_diagonals_separated_by_parity(arr, width, height).T)
print(out)
[[ 1 2 4 7]
[ 3 6 9 11]
[ 5 10 13 15]
[ 8 14 17 19]
[12 18 21 22]
[16 20 23 24]]
import numpy as np
from numba import njit
@njit
def flat_diagonal_stops(height, width):
min_dim = min(height, width)
lengths = np.empty(height + width, dtype='int')
lengths[:min_dim] = [*range(min_dim)]
lengths[min_dim:1-min_dim] = min_dim
lengths[:-min_dim:-1] = lengths[1:min_dim]
return lengths.cumsum()
@njit
def separate_by_index_parity(arr):
out = np.empty_like(arr)
middle = sum(divmod(len(out), 2))
out[:middle] = arr[::2]
out[:middle-len(out)-1:-1] = arr[1::2]
return out
@njit
def assemble_diagonals_separated_by_parity(arr, height, width):
if height == 1 or width == 1:
return arr.reshape(height, width).copy()
out = np.empty(height*width, dtype=arr.dtype)
stops = flat_diagonal_stops(height, width)
out_step = width + 1
for offset, (start, stop) in enumerate(zip(stops[:-1], stops[1:]), 1-height):
out_from = -offset*width if offset < 0 else offset
out_to = out_from + (stop-start)*out_step
out[out_from:out_to:out_step] = separate_by_index_parity(arr[start:stop])
return out.reshape(height, width)
height, width = 6, 4
arr = np.arange(1, 1+height*width)
out = np.fliplr(assemble_diagonals_separated_by_parity(arr, width, height).T)
print(out)
Давайте специализируем функцию сборки для работы непосредственно с антидиагоналями, чтобы не путаться с трюками с переворотом-транспонированием. В данном случае шаг нарезки у нас короче, а начальная точка будет вдоль верхнего и правого краев. Все остальное остается неизменным:
@njit
def assemble_antidiagonals_separated_by_parity(arr, height, width):
if height == 1 or width == 1:
return arr.reshape(height, width).copy()
out = np.empty(height*width, dtype=arr.dtype)
stops = flat_diagonal_stops(height, width)
out_step = width - 1
for offset, (start, stop) in enumerate(zip(stops[:-1], stops[1:])):
out_from = offset if offset < width else (offset-width+2)*width-1
out_to = out_from + (stop-start)*out_step
out[out_from:out_to:out_step] = separate_by_index_parity(arr[start:stop])
return out.reshape(height, width)
>>> height, width = 8, 5
>>> arr = np.arange(1, 1+height*width)
>>> out = assemble_antidiagonals_separated_by_parity(arr, height, width)
>>> print(out)
[[ 1 2 4 7 11]
[ 3 6 9 13 16]
[ 5 10 15 18 21]
[ 8 14 20 23 26]
[12 19 25 28 31]
[17 24 30 33 35]
[22 29 34 37 38]
[27 32 36 39 40]]
Ага... Вызов def assemble...(arr, height, width) с помощью assemble...(arr, width, height) и необходимость внешних действий fliplr и T - это довольно неприятно. Было бы хорошо, если бы вы это исправили, заставили функцию правильно выполнять всю работу.
@nocomment Да, ты прав. Специализация функции не помешала бы.
Спасибо. Новая версия работает как есть в сценарии тестирования моего нового ответа, проходя все тесты. Это немного медленнее (~30 мс), но у меня нет числа, поэтому я измерял только без njit.
Рассматривайте массив как три раздела: верхний левый треугольник, включающий самую длинную диагональ (желтый), нижний правый треугольник (зеленый), исключающий длинную диагональ, и простое правило, увеличивающее на более короткое измерение, чтобы заполнить пробел. Как уже заметили другие, здесь существует сильная связь с треугольными числами.
Если я понял спецификацию, то думаю, что следующие правила дадут требуемый результат и могут быть записаны в виде короткой процедуры на языке C. Даже если это не совсем то, что вам нужно, возможно, это можно настроить.
We take j=1,2...Jmax and k = 1,2,...Kmax and define N = min(Jmax,Kmax)
result is an index into the linear array from 1 to Kmax*Jmax
caution everything has been done with 1 based indexing but coded in C (I don't speak Python)
Yellow zone j+k <= N+1
top line j<=N : x[1,j] = 1 +j*(j-1)/2
diagonal line : x[j,j] = j*(2*j-1)
otherwise
if k<j
x[k+1,j-1] = x[k,j]+2
else
x[k,j] = x[j,k]+1
White zone
if K>J x[k+1,j] = x[k,j]+J else x[k,j+1] = x[k,j]+K
Green zone (is just a version of the Yellow rules subtracted from Jmax*Kmax)
Мне любопытно, почему ОП хочет эту форму градиента с доминирующей диагональю.
Выводить правила для каждого значения ячейки немного утомительно, но я думаю, что полученная формула должна выполняться быстро. Я надеюсь, что выделение облегчит понимание закономерностей в каждом блоке. Если мы возьмем ту же индексацию на основе 1, что и в исходной поставленной задаче, тогда код C будет:
#include <stdio.h>
// based on original statement of problem with 1 based indexing linear gradient array
// a few constants will change by 1 if using 0 based indexing as would be normal in C
int tidy_jk_to_lin(int j, int k, int J, int K)
{
int N = (J < K ? J : K);
int core = (N - 1) * (N - 2) / 2;
int n = j + k;
if (n <= N + 1)
{
int upper = 1 + (n - 1) * (n - 2) / 2;
if (k <= j) // upper triangle of square NxN
return upper + 2 * (k - 1);
else
return 1 + upper + 2 * (j - 1);
}
if (J - j + K - k < N) // lower triangle of square NxN
{
int lower = J * K - (J + K + 1 - n) * (J + K + 2 - n) / 2;
if (K - k >= J - j)
return lower + 2 * (J + 1 - j) - 1;
else
return lower + 2 * (K + 1 - k);
}
if (K > J) // core to fill in the gap
if (j <= J / 2)
return core + 2 * j + J * (n - J) - 1;
else
return core + 2 * (J - j) + J * (n - J);
else
if (k <= K / 2)
return core + 2 * (k - 1) + K * (n - K);
else
return core + 2 * (K + 1 - k) + K * (n - K) - 1;
}
int main()
{
const int depth = 9;
for (int jmax = 2; jmax<depth; jmax++)
for (int kmax = 2; kmax<depth; kmax++)
{
printf("\nMatrix for %i x %i : \n", jmax, kmax);
for (int k = 1; k <= kmax; k++)
{
for (int j = 1; j <= jmax; j++)
printf(" %3i", tidy_jk_to_lin(j, k, jmax, kmax));
printf("\n");
}
}
}
Он реализует функцию, которая принимает j, k, Jmax и Kmax и возвращает индекс в одномерный линейный массив, предположительно имеющий размер не менее Jmax*Kmax, на основе индексации j,k в двумерный массив, который имеет столбцы Jmax и строки Kmax.
Чтобы быть «симметричным», имеет смысл разбить треугольники одинакового размера с обеих сторон, не так ли? Либо оба треугольника «полноразмерные», либо оба поменьше, но не смешанные. C-код — идеальный способ, но его необходимо интегрировать в Python как модуль Python, способный работать непосредственно с numpy .data частью массива, поэтому весь цикл происходит при чтении и записи C-кода в numpy data часть напрямую и добиться максимально возможной скорости, верно?
Почему эта форма? Чтобы добиться эффекта прогресса 2D-градиента в изображении RGB по «линии» между верхним левым и нижним правым пикселями изображения, при этом края изображения «ближе» к цвету левого верхнего пикселя и Центр высоты изображения ближе к цвету правого нижнего пикселя.
Я решил показать его в виде верхнего левого треугольника, включая диагональ и нижний правый треугольник, не подчеркивая структуру массива при Jmax != Kmax и его связь с квадратным массивом размера NxN. Это должно быть достаточно легко перевести в функцию Python. Я до сих пор не понимаю, что это преобразование делает для вас в пространстве RGB, чем гораздо более простое правило определения верхней линии таким же образом и установки x[k+1,j-1] = x[k,j]+1 для остальные.
При размере NxN схема деления на два треугольника и «параллелограмм» больше не работает, если вы решите сделать один треугольник «полноразмерным», потому что «параллелограмма» в этом случае не будет. Выбрав меньшие треугольники, составьте схему: верхний треугольник, параллелограмм, нижний треугольник всегда имеют там параллелограмм, сохраняя там своеобразную «симметрию». Вот почему я предпочитаю этот подход для обработки трех случаев: ширина > высота, ширина < высота, ширина = высота. Можете ли вы с этим согласиться, или я что-то здесь просмотрел?
В любом случае все должно быть в порядке, поскольку оба способа построения центральной диагонали должны давать одинаковый результат и быть согласованными с остальной частью шаблона. То, что два пути дадут один и тот же ответ, не влияет на алгоритм. Первое обнаруженное допустимое правило может вернуть правильный индекс. Вам необходимо полностью определить значения вдоль края самой короткой стороны, поэтому я выбрал шаблон именно так. Наличие одинаковых углов немного снижает сложность кода, поэтому может быть предпочтительнее. Я опубликовал код того, как я визуализировал решение проблемы. Улучшения возможны.
Да… дело сводится к заботе о «красоте» кода (симметрия обычно считается красивее, чем ее отсутствие). Результат тот же, поэтому, если «красота» не принимается во внимание, выбор размера треугольника не имеет значения и может осуществляться путем любой сознательной комбинации.
Чтобы быть полезным для быстрого создания 2D-массива numpy, код необходимо превратить в модуль Python, принимающий 1D-массив numpy любого типа, возвращающий 2D-массив numpy. Узким местом здесь является скорость выполнения цикла по всем индексам, присваивающим значения из 1D в 2D-массив, а не вычисление целевого индекса.
Это по-прежнему канонический ответ на ваш исходный вопрос, который обеспечивает краткую вычислительную стоимость O (1) для сопоставления 2D-индексации для массива J x K с длиной 1D линейного массива J*K. Вы запросили для этого код Python или C.
Хммм... вопрос запрашивал код для получения 2D-массива с учетом 1D-массива. Предоставленный код не создает запрошенный 2D-массив, необходимый для сравнения эффективности алгоритма с другими, представленными в других ответах.
Вам просто нужно создать 2D-массив numpy, а затем заполнить его, пройдя по индексам массива, чтобы заполнить диагональные линии j+k=2,3,4,.. в месте назначения, начиная с верхнего правого угла, чтобы шаблон доступа к памяти остается локальным и почти линейным при доступе к одномерному массиву. Вам придется сделать это в несколько полос, если количество строк в месте назначения велико, чтобы все действия происходили в кеше.
Я чувствую, что создание 2D-массива — неправильный путь, а создание 1D-массива — правильный. Изменение формы в 2D является тогда последним шагом без каких-либо затрат, кроме установки значений для шагов и формы массива numpy. Другими словами, с этой точки зрения на самом деле необходимо отображение 1D-массива в другой 1D-массив, который при изменении формы дает ожидаемый результат.
Вычисление индексов входного массива для желаемой формы и их последующее применение. Можно подготовить их один раз и применить ко многим массивам, если вы хотите сделать одну и ту же форму много раз.
def weird_from_1D(a, m, n):
i, j = np.indices((m, n))
d = i + j
imin = np.array(n * [0] + [*range(1, m)])
imax = np.array([*range(m-1)] + n * [m-1])
before = np.r_[0, np.cumsum(imax - imin + 1)]
indices = before[d] + np.fmin((i-imin[d])*2, (imax[d]-i)*2+1)
return a[indices]
import numpy as np
print(weird_from_1D(np.arange(1, 16), 5, 3))
Результат (Попробуйте это онлайн!):
[[ 1 2 4]
[ 3 6 7]
[ 5 9 10]
[ 8 12 13]
[11 14 15]]
i/j/d — обычные числа строки/столбца/диагонали:
i j d
[[0 0 0] [[0 1 2] [[0 1 2]
[1 1 1] [0 1 2] [1 2 3]
[2 2 2] [0 1 2] [2 3 4]
[3 3 3] [0 1 2] [3 4 5]
[4 4 4]] [0 1 2]] [4 5 6]]
Для каждой диагонали imin и imax сообщают минимальный/максимальный номер строки, которую включает диагональ. Это номера строк ячеек верхнего и правого края (для imin), а также ячеек левого и нижнего края (для imax):
imin: [0 0 0 1 2 3 4]
imax: [0 1 2 3 4 4 4]
Затем imax - imin + 1 сообщает размер каждой диагонали. А совокупные суммы этих размеров с добавленным нулем показывают для каждой диагонали, сколько ячеек было во всех предыдущих диагоналях, что является начальным числом в каждой диагонали:
before: [ 0 1 3 6 9 12 14 15]
before[d]:
[[ 0 1 3]
[ 1 3 6]
[ 3 6 9]
[ 6 9 12]
[ 9 12 14]]
К этому начальному номеру каждой диагонали добавьте 0, 2, 4 и т. д. из верхнего правого угла или 1, 3, 5 и т. д. из нижнего левого угла:
before[d] + before[d] +
(i-imin[d])*2 (imax[d]-i)*2+1
[[ 0 1 3] [[ 1 4 8]
[ 3 5 6] [ 2 6 11]
[ 7 8 9] [ 4 9 14]
[10 11 12] [ 7 12 15]
[13 14 14]] [10 13 15]]
Вместо того, чтобы объединять их, определяя середину каждой диагонали, я просто беру их поэлементный минимум, что имеет тот же эффект:
indices:
[[ 0 1 3]
[ 2 5 6]
[ 4 8 9]
[ 7 11 12]
[10 13 14]]
И теперь это можно использовать (или использовать повторно, если вы его сохраните) для захвата элементов из входного массива по желанию.
Я считаю, что этот ответ был бы намного лучше с небольшими пояснениями , как вы это делали раньше .
@Vitalizzare Готово, хотя на этот раз без вычурных рисунков пальцами :-)
Я хочу помнить, что в ответах на другие вопросы этой серии одинаковых/похожих вопросов вы узнали, что работа с 1D-массивом даст самые быстрые результаты, так почему бы не создать соответствующий 1D-массив, а затем просто изменить форму это? Может быть, тогда это превзойдет скорость кода, предоставленного Vitalizzare?
srcArr.shape=(6,) mRows=3 nCols=2 без комментариев: 0,0001 Vitalizzare: 0,0000 srcArr.shape=(2073600,) mRows=1080 nCols=1920 без комментариев: 0,1498 Vitalizzare : 0,0210 srcArr.shape=(207 3600,) mRows=19200 nCols=108 без комментариев: 0,1152 Изменение: 0,0762 srcArr.shape=(2073600,) mRows=108 nCols=19200 без комментариев: 0,0879 Изменение: 0,0750 srcArr.shape=(2073600,) m Rows=1440 nCols=1440 без комментариев : 0,0911 Обновление: 0,0192 Python 3.10.12: numpy 1.26.4
@oOosys Я работаю с одномерным массивом и одномерными индексами. Создаю ли я 2D-массив напрямую или через 1D-массив, как я и ожидал, разницы не имеет. Для m,n=1080,1920 я получаю 0,83 с для моего решения, как показано, и 0,15 с как для версии, которая просто выполняет return a[prepared_indices], так и для его версии через 1D.
Я полагаю, что «хитрость» здесь заключается в использовании соответствующего среза при копировании значений из одного массива в другой. По какой-то еще недостаточно изученной причине это привело к более высокой скорости изменения диагоналей, чем при прохождении элемента за элементом. Я предполагаю, что выбор срезов, используемых для отображения, — это то, на чем необходимо сосредоточиться и детально разобраться, чтобы получить максимально быстрый результат в зависимости от формы массива. Как еще вы можете объяснить, что ваш код работает медленнее, чем тот, который опубликовал Vitalizzare?
Этот алгоритм дает вам необходимые элементы одномерного массива/списка в соответствии с индексом элемента массива, используя циклы и математические вычисления, поэтому он не использует слишком много памяти и разделен на 4 подалгоритма или шага для решения этой проблемы. :
вычислить R, который возвращает количество элементов внутри диагональ, содержащая выделенный элемент по его индексу.
Найдите порядок элементов внутри выбранной диагонали, используя элемент индекс 2D-массива (счёт начинается сверху вниз и слева по диагонали)
изменение порядка элементов на требуемый порядок (счёт начинается с первого диагонального элемента, затем с последнего диагональный элемент влево вниз, затем возврат вверх и так далее)
подсчет всех элементов до тех пор, пока элемент, который мы выбрали по индексу 2D-матрицы, так что укажет элемент списка индексов, который нам нужен.
Код обновлен Разработаны подалгоритмы и выведены математические уравнения для решения проблемы повышения производительности предыдущего кода. теперь это лучше, чем раньше, и его легче закодировать на другом языке программирования, поэтому попробуйте закодировать его на компилируемом языке, таком как C, я думаю, это будет более эффективно.
import numpy as np
lst=np.arange(11,200) # 1-D array/list which defines a color gradient
A=np.empty([5,4], dtype = object) # define empty 2-D color gradient
m=np.shape(A)[0]
n=np.shape(A)[1]
def get_index(i,j,m,n): #first index 1,1
Rr= min(i-1,n-j)+ min(m-i,j-1)+1# 1- R
x=min(i-1,n-j)+1 # 2- e-order
B=int( Rr%2 !=0)
k=(Rr+B)/2
z=int(x>k)
v1, v2=i-n+j, j-m+i
segma=-((x-1)*2+1)*(z-1)+(4*k-2*x-2*B+2)*z # 3- e_new_order
S = i**2/2-i/2+j**2/2 -j/2+i*j-((v1-1+abs(v1-1))/4)*v1-(((v2-1+abs(v2-1))/4)*(v2)) # 4- e_calc
'''if x<=k: # segma calculated without using if condition to make code faster
#segma=(x-1)*2+1
segma=2*x-1 #After reducing the equation
elif x>k:
#segma=(x-k-1)*(-2)+1-2*B + (k-1)*2+1
segma=4*k-2*x-2*B+2 # After reducing the equation'''
dx=int(S - Rr + segma)-1
return dx
i , j = 0 , 0
while j < n:
while i < m:
A[ i , j ]=get_index(i+1,j+1,m,n)
i = i + 1
j=j+1
i=0
A
Выход
array([[11, 12, 14, 17], [13, 16, 19, 21], [15, 20, 23, 25], [18, 24, 27, 28], [22, 26, 29, 30]], dtype=object)
Этот алгоритм не присваивает переменной никаких срезов списков или массивов, поэтому он экономит оперативную память. Вам просто нужен индекс элемента в массиве, чтобы дать вам значение, соответствующее этому индексу в списке. Этот алгоритм больше зависит от процессора, чем от оперативной памяти.
Ознакомьтесь с моим «ответом» на код, указывающий время, и не стесняйтесь придумывать код, превосходящий другие алгоритмы. Теперь вы можете удалить свои комментарии здесь, поскольку они больше не нужны и только запутают будущих посетителей вашего ответа.
Решение для C
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
static unsigned min(unsigned x, unsigned y) { return x < y ? x : y; }
static unsigned triangle_number(unsigned n) { return n * (n + 1) / 2; }
static unsigned
linear_index(
unsigned rowIx, unsigned colIx,
unsigned nrows, unsigned ncols
)
{
unsigned diagIx;
unsigned diagBias;
unsigned elemIxFromTop, elemIxFromBottom;
assert(colIx < ncols && rowIx < nrows);
diagIx = rowIx + colIx;
diagBias = triangle_number(diagIx)
- triangle_number(diagIx - min(ncols, diagIx))
- triangle_number(diagIx - min(nrows, diagIx));
elemIxFromTop = min(diagIx, ncols - 1) - colIx;
elemIxFromBottom = min(diagIx, nrows - 1) - rowIx;
return diagBias + min(2 * elemIxFromTop, 2 * elemIxFromBottom + 1);
}
int main(int argc, char *argv[])
{
int nrows, ncols;
{
char *end;
if (3 != argc
|| (nrows = strtol(argv[1], &end, 0), '\0' != *end)
|| (ncols = strtol(argv[2], &end, 0), '\0' != *end))
{
fprintf(stderr, "usage: %s rows columns\n", argv[0]);
return 1;
}
}
for (int i = 0; i < nrows; ++i) {
for (int j = 0; j < ncols; ++j) {
printf("%4d", linear_index(i, j, nrows, ncols) + 1);
}
printf("\n");
}
return 0;
}
Тесты:
~ % ./test 5 3
1 2 4
3 6 7
5 9 10
8 12 13
11 14 15
~ % ./test 3 5
1 2 4 7 10
3 6 9 12 13
5 8 11 14 15
~ % ./test 4 4
1 2 4 7
3 6 9 11
5 10 13 14
8 12 15 16
Основная идея – получить линейный индекс. Для этого можно заметить, что двумерный массив заполняется по диагонали. Эти диагонали можно индексировать. Элемент массива (строка, столбец) будет расположен по диагонали:
diagIx = rowIx + colIx
Каждая диагональ начинает заполняться с верхнего правого элемента. Значение этого начального элемента равно количеству элементов выше этой диагонали. Для расчета вы можете использовать треугольное число. Однако следует учитывать, что элементы выше диагонали могут представлять собой усеченные треугольники. Например, массив размером 4 на 4, последняя диагональ:
+ + + + + + + + + + @ @ @ @ + + @ @ @ @
+ + + + + + + + + @ @ @ @ + @ @ @ @
+ + + + + + + + @ @ @ @ @ @ @ @
+ + + @ = + + + @ - @ @ @ @ - @ @ @ @
+ + + +
+ +
Чтобы это учесть, вам нужно вычесть количество элементов в дополнительных треугольниках (вверху справа и внизу слева):
diagBias = triangle_number(diagIx)
- triangle_number(diagIx - min(ncols, diagIx))
- triangle_number(diagIx - min(nrows, diagIx))
Далее можно обратить внимание на то, что по диагонали, учитывая ее начальное значение, заполнение происходит следующим образом. Начиная сверху до середины заполняется четными числами, а начиная снизу до середины заполняется нечетными числами. Рассмотрим в качестве примера несколько таких диагоналей.
0 0 0
2 2 2
4 4 4
6 5 3
5 3 1
3 1
1
Как видите, сверху четные числа, снизу нечетные, и они увеличиваются к середине диагонали. Таким образом, мы можем разложить наш элемент по диагонали под четное и нечетное число и взять меньшее из них. Сначала определяем номер элемента по диагонали сверху вниз:
elemIxFromTop = min(diagIx, ncols - 1) - colIx
Затем определяем номер элемента по диагонали снизу вверх:
elemIxFromBottom = min(diagIx, nrows - 1) - rowIx
Наконец, мы можем определить номер элемента как четное и нечетное число и выбрать меньшее из двух:
min(2 * elemIxFromTop, 2 * elemIxFromBottom + 1)
Должен отметить, что схожие идеи есть и у следующих авторов:
Приятно иметь C-код, предоставляющий массив для индексов. Чтобы сделать его полезным в контексте вопроса, потребуются некоторые дальнейшие шаги, которые затем решат, дает ли способ модуля C-расширения какое-либо преимущество перед чистым способом Python numpy.
Буду рад, если это поможет вам в работе.
Это скромная мотивация помочь мне, но я задаю здесь вопросы исключительно с целью обогатить stackoverflow интересными вопросами, дающими интересные ответы, которые, возможно, станут полезными для кого-то еще. Лично мне не нужны ответы в моей «работе», и то, над чем я на самом деле «работаю» (распространение информации о способе программирования oOo), — это совершенно другая пара обуви.
Ниже приведено сравнение времени предоставленных ответов, созданных с использованием приведенного ниже кода:
import numpy as np
from numba import njit
@njit
def flat_diagonal_stops(height, width):
'''Return a sequence of breakpoints separating a sequence of length height*width into
a sequence of matrix diagonals
of the shape (height, width)
'''
min_dim = min(height, width)
lengths = np.empty(height + width, dtype='int')
lengths[:min_dim] = [*range(min_dim)] # diagonal lengths in the lower triangle
lengths[min_dim:1-min_dim] = min_dim # diagonal lengths in the main body
lengths[:-min_dim:-1] = lengths[1:min_dim] # diagonal lengths in the upper triangle
return lengths.cumsum()
@njit
def separate_by_index_parity(arr):
''' Return a numpy.ndarray filled with elements of arr, first those in even-numbered
positions, then those in odd-numbered positions in reverse order
'''
out = np.empty_like(arr)
middle = sum(divmod(len(out), 2))
out[:middle] = arr[::2]
out[:middle-len(out)-1:-1] = arr[1::2]
return out
@njit
def assemble_diagonals_separated_by_parity(arr, height, width):
'''Return a matrix of shape (height, width) with elements
of the given sequence arr arranged along diagonals,
where the elements on each diagonal are separated
by the parity of their index in them
'''
out = np.empty(height*width, dtype=arr.dtype)
stops = flat_diagonal_stops(height, width)
out_step = width + 1
for offset, (start, stop) in enumerate(zip(stops[:-1], stops[1:]), 1-height):
# out_from: the first element of an off-diagonal
# out_to : next after the last element of an off-diagonal
# out_step: a stride to get diagonal items
out_from = -offset*width if offset < 0 else offset
out_to = out_from + (stop-start)*out_step # stop - start is equal to the diagonal size
out[out_from:out_to:out_step] = separate_by_index_parity(arr[start:stop])
return out.reshape(height, width)
def diagonal2DgradientArray_from_flatGradientArray(flatGradientArray, rows2Darray, cols2Darray):
m = rows2Darray ; n = cols2Darray ; a = flatGradientArray
i, j = np.indices((m, n))
d = i + j ## first 2D(m,n) array ( with rl-diagonal integerLabels from 0 to m+n-1 )
imin = np.array(n * [0] + [*range(1, m)])
imax = np.array([*range(m-1)] + n * [m-1])
before = np.r_[0, np.cumsum(imax - imin + 1)]
indices = before[d] + np.fmin((i-imin[d])*2, (imax[d]-i)*2+1) ## second 2D(m,n) array
return a[indices] ## third 2D(m,n) array
@njit
def inverse_diagonal(x: (int, int), max: (int, int)) -> int:
"""
Given a rectangle (0,0) -- [`max`], for point `x`, calculate the inverse
mapping of interlaced diagonal stripes, to a linear array.
"""
assert(x[0] < max[0] and x[1] < max[1])
# Change basis so 1st horizontally is triangular number.
u = x[0] + x[1]
area_x_axis = u * (u + 1) // 2
area_y_axis = (u + 1) * (u + 2) // 2
# Subtract the ends by the inclusion-exclusion principle.
subtract = 0;
if u >= max[0]:
subtract_side = u - max[0] + 1
subtract += subtract_side*(subtract_side + 1) // 2
if u > max[1]:
subtract_side = u - max[1]
subtract += subtract_side*(subtract_side + 1) // 2
area_x_axis -= subtract
area_y_axis -= subtract
# Interlace the diagonal.
range0 = area_x_axis + (u >= max[0]) * (u - max[0] + 1)
range1 = area_y_axis - (u >= max[1]) * (u - max[1] + 1)
without_interlace = area_x_axis + x[1]
if (without_interlace - range0 > (range1 - range0 - 1) // 2):
ray = (range1 - without_interlace - 1) * 2 + 1 + range0
else: # top half and diagonal
ray = (without_interlace - range0) * 2 + range0
return ray
@njit
def diagonal2Dgradient_Neil(flat1Darray, mRows, nCols):
tgtArr = np.empty( (mRows , nCols),dtype=flat1Darray.dtype)
for row in range(mRows):
for col in range(nCols):
tgtArr[row , col ] = flat1Darray[ inverse_diagonal( (col , row ), (nCols, mRows) ) ]
return tgtArr
'''
list = [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16 ]
for max in ((3, 5), (5, 3), (4, 4)):
for row in range(max[1]):
for col in range(max[0]):
print("{:4d}, ".format( list[inverse_diagonal((col, row), max)] ), end='')
print("")
print("")
'''
## MRasheed
# 1.calculate R which returns The number of elements within the diagonal containing the selected element according to its index
@njit
def R(i,j,m,n): #first index 1,1
if i+j-1 >= min(m,n) and (m+n)-(i+j-1) >= min(m,n):
R=min(m,n)
if i+j-1 < min(m,n) or m+n-i-j+1 < min(m,n):
R=min(i+j-1,m+n-i-j+1)
return R
# 2. calculate e_order which return Arrange the elements according to the specified diagonal
@njit
def e_order(i,j,m,n): #first index 1,1
res=0
while (i>0 and j<=n):
res=res+1
i-=1
j+=1
return res
# 3. calculate e_new_order which return New arrangement of elements as required
@njit
def e_new_order(i,j,m,n): #first index 1,1
Rr= R(i,j,m,n)
x=e_order(i,j,m,n)
if Rr%2 ==0:
if Rr==2:
segma=x
else:
if x<=Rr/2:
segma=(x-1)*2+1
else:
segma=(x-(Rr/2)-1)*(-2)+1+ ((Rr/2)-1)*2+1
else:
if x <=(Rr+1)/2:
segma=(x-1)*2 +1
else:
if Rr==3 and x==3:
segma=2
else:
segma=(x-((Rr+1)/2)-1)*(-2)-1+ (((Rr+1)/2)-1)*2+1
return segma
# 4. calculate elements number behind the diagonal containing the selected element according to its index
@njit
def e_calc(i,j,m,n): # first index 1,1
sums=0
while j>1:
j-=1
sums=R(i,j,m,n)+sums
while i>1:
i-=1
sums=R(i,1,m,n)+sums
return sums
@njit
def diagonal2Dgradient_MRashid(flat1Darray, mRows, nCols):
tgtArr = np.empty( (mRows , nCols),dtype=flat1Darray.dtype)
m=mRows ; n=nCols
i , j = 0 , 0
while j < n:
while i < m:
dx = int( e_new_order(i+1, j+1, m, n) + e_calc(i+1,j+1,m,n) ) - 1
#print(f"{i=} {j=} {dx=}", end = "")
#print(f" {flat1Darray[dx]=}")
tgtArr[ i , j ]=flat1Darray[dx ]
i = i + 1
j=j+1
i=0
return tgtArr
# exit()
mRows = 5 ; nCols = 3
arr = np.array( [ str(i) for i in range(11, 11+mRows*nCols) ] )
tstArr1 = np.fliplr(assemble_diagonals_separated_by_parity(arr, nCols, mRows).T)
tstArr2 = diagonal2DgradientArray_from_flatGradientArray(arr, mRows, nCols)
tstArr3 = diagonal2Dgradient_Neil(arr, mRows, nCols )
tstArr4 = diagonal2Dgradient_MRashid(arr, mRows, nCols )
assert np.all(tstArr1 == tstArr2 )
assert np.all(tstArr1 == tstArr3 )
assert np.all(tstArr1 == tstArr4 )
#exit()
from time import perf_counter as T
arrShapes = [ (16,8), (1080, 1920), (19200, 108), (108, 19200), (1440, 1440) ]
for arrShape in arrShapes:
mRows, nCols = arrShape
srcArr = np.arange(1, mRows*nCols + 1, dtype=np.uint32)
sT1=T()
tgtArr1 = diagonal2DgradientArray_from_flatGradientArray(srcArr, mRows, nCols)
dT1=T()-sT1
sT2=T()
tgtArr2 = np.fliplr(assemble_diagonals_separated_by_parity(srcArr, nCols, mRows).T)
dT2=T()-sT2
sT3=T()
tgtArr3 = diagonal2Dgradient_Neil(srcArr, mRows, nCols)
dT3=T()-sT3
sT4=T()
tgtArr4 = diagonal2Dgradient_MRashid(srcArr, mRows, nCols)
dT4=T()-sT4
print(f"{srcArr.shape=} {mRows=} {nCols=}")
print(f" no comment : {dT1:.4f}")
print(f" Vitalizarre : {dT2:.4f}")
print(f" Neil : {dT3:.4f}")
print(f" MRashid : {dT4:.4f}")
assert np.all(tgtArr1 == tgtArr2)
assert np.all(tgtArr1 == tgtArr3)
assert np.all(tgtArr1 == tgtArr4)
import os
os.system("echo $(python --version) : $(pip list | grep numpy)")
выход:
srcArr.shape=(128,) mRows=16 nCols=8
no comment : 0.0002
Vitalizarre : 1.1424
Neil : 0.2350
MRashid : 0.2498
srcArr.shape=(2073600,) mRows=1080 nCols=1920
no comment : 0.1600
Vitalizarre : 0.0106
Neil : 0.0376
MRashid : 1.9205
srcArr.shape=(2073600,) mRows=19200 nCols=108
no comment : 0.1117
Vitalizarre : 0.0073
Neil : 0.0243
MRashid : 10.5929
srcArr.shape=(2073600,) mRows=108 nCols=19200
no comment : 0.0875
Vitalizarre : 0.0068
Neil : 0.0520
MRashid : 12.2929
srcArr.shape=(2073600,) mRows=1440 nCols=1440
no comment : 0.0846
Vitalizarre : 0.0108
Neil : 0.0338
MRashid : 1.8087
Python 3.10.12 : numpy 1.26.4
Объяснением странного времени появления первой формы массива является декоратор @njit. Не стесняйтесь удалить его, а затем проверить еще раз, но будьте готовы, что, за исключением Vitalizzare и решения без комментариев, для достижения результата может потребоваться «бесконечное» время. Другими словами, первая форма массива присутствует лишь в качестве своеобразной «разминки» для того, чтобы сделать дальнейшие тайминги сопоставимыми.
Вопрос, который остается открытым в этом контексте: как получается, что сроки решений настолько различаются? Особенно эти два самых быстрых решения?
Если вы хотите знать ответ на такой вопрос, пожалуйста, задайте его сами... На данный момент я закончил с этой темой, общие затраты которой составили 1200 очков репутации (распределены на три вопроса, задающих более или менее одинаковые вопросы), не придя к объяснению поведение времени полезно для выбора соответствующего алгоритмического подхода для различных форм массива, что делает время более или менее независимым от формы.
Он никогда не будет независимым от формы, поскольку размер и емкость строки кэша на каждом уровне будут достигать некоторых длин намного быстрее, чем других. Из тех, что вы тестировали, «Нил» кажется наиболее стабильно быстрым. Было бы полезно попробовать использовать карту с размерами массива 1023, 1024, 1025 (или какой-либо другой хорошо выбранный 2^N+/-1), скажем, 512 и наоборот, чтобы определить, насколько поведение кэша влияет на тайминги. Я до сих пор не могу понять, почему вам нужен именно этот диагональный доминирующий массив. Будет определенная оптимальная длина строки, соответствующая доступному кэшу.
Хммм... Присвоение значений новому массиву, полученному из другого массива, может производиться в любом произвольном порядке. Почему нельзя заранее определить порядок и упаковку срезов в зависимости от формы массива, чтобы минимизировать влияние формы массива на общее время?
Я не хочу ничего связанного с массивом... Я хочу понять, почему существуют различия в таймингах в зависимости от формы массива. При таком понимании должно быть возможно устранить или хотя бы минимизировать разницу во времени в зависимости от формы, верно?
Скорость зависит от того, как общий размер кэша на каждом уровне взаимодействует с размером перемещаемых объектов, размерами массива и размером кэша. В идеале при реструктуризации массива вы стремитесь сохранить его локальным кэшем, насколько это возможно. Обычно это предполагает укладку плитки. Каноническим примером такого рода оптимизации для алгоритма с поддержкой кэша является транспонирование большого массива. Обычно это делается в два этапа: перенесите небольшие квадратные плитки (возможно, на место), а затем переместите их в конечный пункт назначения. На современных процессорах с необычными правилами кэширования есть дополнительные особенности для некоторых магических длин.
Я подозреваю, что O(n) с полными 16Mx16M пикселями, одно ядро будет работать медленно, несмотря ни на что. Некоторые решения действительно хороши для многоядерных процессоров или графических процессоров. Я кратко попробовал PyOpenCl, но мне даже не удалось его установить... Возможно, вы могли бы что-то попробовать, если не получите желаемой производительности.
Копирование фрагментов из данного 1D-массива в другой 1D-массив, который в конечном итоге преобразуется в 2D:
def via_slices(a, m, n):
if n == 1:
return a.reshape((m, n)).copy()
b = np.empty_like(a)
i = 0
for d in range(m+n-1):
jleft = 0 if d < m else d - (m-1)
jtop = d if d < n else n-1
jmid = (jleft + jtop + 1) // 2
left = (d-jleft)*n + jleft
mid = (d-jmid)*n + jmid
top = (d-jtop)*n + jtop
size = jtop - jleft + 1
b[left:mid:1-n] = a[i+1:i+size:2]
b[top:mid+1:n-1] = a[i:i+size:2]
i += size
return b.reshape((m, n))
Я действую по одной диагонали за раз, записывая ее нижнюю и верхнюю половины с одним назначением среза в каждой.
i — текущий индекс в данном массиве.d — текущий номер диагонали.jleft/jtop/jmid — номер столбца левой/верхней/средней ячейки текущей диагонали.left/top/mid — соответствующие им индексы 1D.(Я думаю, что это похоже на Витализзаре, не до конца прочитал.)
Время с вариациями и мое более раннее решение с помощью индексов:
14.8 ± 0.1 ms via_prepared_indices
22.0 ± 0.1 ms via_prepared_slices
22.9 ± 0.1 ms via_slices
24.3 ± 0.1 ms via_slices2
27.2 ± 0.2 ms via_prepared_views
78.1 ± 0.9 ms via_indices
Python: 3.12.2 (main, Jun 12 2024, 09:13:57) [GCC 14.1.1 20240522]
«Подготовленные» версии предназначены для случаев, когда вы хотите сделать это несколько раз с одной и той же формой. Тогда мы сможем подготовить индексы/срезы/представления только один раз, а затем просто применить их для каждого заданного массива этой формы.
Полный сценарий тестирования (также включает тесты на корректность):
import numpy as np
from timeit import timeit
from statistics import mean, stdev
import sys
import random
def via_indices(a, m, n):
i, j = np.indices((m, n))
d = i + j
imin = np.array(n * [0] + [*range(1, m)])
imax = np.array([*range(m-1)] + n * [m-1])
before = np.r_[0, np.cumsum(imax - imin + 1)]
indices = before[d] + np.fmin((i-imin[d])*2, (imax[d]-i)*2+1)
return a[indices]
def prepare_indices(a, m, n):
global indices
i, j = np.indices((m, n))
d = i + j
imin = np.array(n * [0] + [*range(1, m)])
imax = np.array([*range(m-1)] + n * [m-1])
before = np.r_[0, np.cumsum(imax - imin + 1)]
indices = before[d] + np.fmin((i-imin[d])*2, (imax[d]-i)*2+1)
def via_prepared_indices(a, m, n):
return a[indices]
def via_slices(a, m, n):
if n == 1:
return a.reshape((m, n)).copy()
b = np.empty_like(a)
i = 0
for d in range(m+n-1):
jleft = 0 if d < m else d - (m-1)
jtop = d if d < n else n-1
jmid = (jleft + jtop + 1) // 2
left = (d-jleft)*n + jleft
mid = (d-jmid)*n + jmid
top = (d-jtop)*n + jtop
size = jtop - jleft + 1
b[left:mid:1-n] = a[i+1:i+size:2]
b[top:mid+1:n-1] = a[i:i+size:2]
i += size
return b.reshape((m, n))
def via_slices2(a, m, n):
if n == 1:
return a.reshape((m, n)).copy()
b = np.empty_like(a)
i = 0
for d in range(m+n-1):
jleft = max(d - (m-1), 0)
jtop = min(d, n-1)
jmid = (jleft + jtop + 1) // 2
left = (d-jleft)*n + jleft
mid = (d-jmid)*n + jmid
top = (d-jtop)*n + jtop
size = jtop - jleft + 1
b[left:mid:1-n] = a[i+1:i+size:2]
b[top:mid+1:n-1] = a[i:i+size:2]
i += size
return b.reshape((m, n))
def prepare_slices(a, m, n):
global slices
slices = []
if n == 1:
slices = [(slice(None),) * 2]
return
i = 0
for d in range(m+n-1):
jleft = 0 if d < m else d - (m-1)
jtop = d if d < n else n-1
jmid = (jleft + jtop + 1) // 2
left = (d-jleft)*n + jleft
mid = (d-jmid)*n + jmid
top = (d-jtop)*n + jtop
size = jtop - jleft + 1
slices.append((slice(left, mid, 1-n), slice(i+1, i+size, 2)))
slices.append((slice(top, mid+1, n-1), slice(i, i+size, 2)))
i += size
def via_prepared_slices(a, m, n):
b = np.empty_like(a)
for s, t in slices:
b[s] = a[t]
return b.reshape((m, n))
def prepare_views(a, m, n):
global A, B, views
A = np.empty_like(a)
B = np.empty_like(a)
if n == 1:
views = [(B[:], A[:])]
return
views = []
i = 0
for d in range(m+n-1):
jleft = max(d - (m-1), 0)
jtop = min(d, n-1)
jmid = (jleft + jtop + 1) // 2
left = (d-jleft)*n + jleft
mid = (d-jmid)*n + jmid
top = (d-jtop)*n + jtop
size = jtop - jleft + 1
views.append((
B[left:mid:1-n],
A[i+1:i+size:2]
))
views.append((
B[top:mid+1:n-1],
A[i:i+size:2]
))
i += size
def via_prepared_views(a, m, n):
A[:] = a
for v, v[:] in views:
pass
return B.reshape((m, n)).copy()
def prep(start, m_, n_):
global a, m, n
m, n = m_, n_
a = np.arange(start, start+m*n)
prepare_indices(a, m, n)
prepare_slices(a, m, n)
prepare_views(a, m, n)
funcs = [via_indices, via_prepared_indices, via_slices, via_slices2, via_prepared_slices, via_prepared_views]
# Demo
prep(1, 5, 3)
for f in funcs:
print(f(a, m, n))
# Correctness
for m in range(1, 21):
for n in range(1, 21):
prep(0, m, n)
a0 = a.copy()
expect = None
for f in funcs:
result = f(a, m, n)
if expect is None:
expect = result
else:
assert np.array_equal(result, expect)
assert np.array_equal(a, a0)
print('done')
# Speed
prep(1, 1080, 1920)
times = {f: [] for f in funcs}
def stats(f):
ts = [t * 1e3 for t in sorted(times[f])[:5]]
return f'{mean(ts):5.1f} ± {stdev(ts):3.1f} ms '
for _ in range(25):
random.shuffle(funcs)
for f in funcs:
t = timeit(lambda: f(a, m, n), number=1)
times[f].append(t)
for f in sorted(funcs, key=stats):
print(stats(f), f.__name__)
print('\nPython:', sys.version)
Упорядочен ли 1d вектор? Это с последовательными номерами, начиная с 1?