Алгоритм преобразования 1-D-градиента в специальную форму 2-D-градиента

Предполагая, что существует одномерный массив/список, который определяет цветовой градиент, я хотел бы использовать его для создания двумерного цветового градиента следующим образом:

Давайте для простоты заменим информацию о цвете одним числовым значением для примера одномерного массива/списка:

[ 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?

Упорядочен ли 1d вектор? Это с последовательными номерами, начиная с 1?

AndrewBloom 10.06.2024 22:53

@AndrewBloom: вектор 1d может быть чем угодно... числами, кортежами, строками...

oOosys 10.06.2024 23:10
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
5
2
525
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

Следующий подход использует 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 16.06.2024 07:07

@greybeard Это очень круто! чему-то научился.

Neil 16.06.2024 18:37

См. мой «ответ», чтобы узнать время сравнения кода, представленного в других ответах на ваш код. Не стесняйтесь предоставить просто ради этого попытку ускорить код сверх скорости самого быстрого подхода (предоставленного Vitalizzare).

oOosys 17.06.2024 20:28
Ответ принят как подходящий

Главная идея

Насколько я вижу, это можно сделать в три этапа:

  1. Разбейте последовательность на фрагменты, как если бы они были диагоналями массива заданной формы.
  2. Разделите элементы каждого фрагмента по четности их индекса.
  3. Соберите новый массив из модифицированных диагональных фрагментов.

Шаг 1. Разбейте последовательность на диагональные фрагменты

Я думаю, будет достаточно найти точки остановки, чтобы мы могли нарезать последовательность с их помощью. Для этого мы можем применить накопительную сумму к последовательности диагональных длин:

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()

Шаг 2. Разделите элементы по четности индексов

Преобразование последовательности, подобное этому:

(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

Шаг 3. Собираем фрагменты в диагонали нового массива.

Для этого мы можем создать плоское представление требуемого результата и работать с ним, разрезая диагональные позиции:

@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)

P.S. Сложите данные прямо по антидиагоналям.

Давайте специализируем функцию сборки для работы непосредственно с антидиагоналями, чтобы не путаться с трюками с переворотом-транспонированием. В данном случае шаг нарезки у нас короче, а начальная точка будет вдоль верхнего и правого краев. Все остальное остается неизменным:

@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 - это довольно неприятно. Было бы хорошо, если бы вы это исправили, заставили функцию правильно выполнять всю работу.

no comment 18.06.2024 01:08

@nocomment Да, ты прав. Специализация функции не помешала бы.

Vitalizzare 18.06.2024 23:30

Спасибо. Новая версия работает как есть в сценарии тестирования моего нового ответа, проходя все тесты. Это немного медленнее (~30 мс), но у меня нет числа, поэтому я измерял только без njit.

no comment 18.06.2024 23:43

Рассматривайте массив как три раздела: верхний левый треугольник, включающий самую длинную диагональ (желтый), нижний правый треугольник (зеленый), исключающий длинную диагональ, и простое правило, увеличивающее на более короткое измерение, чтобы заполнить пробел. Как уже заметили другие, здесь существует сильная связь с треугольными числами.

Если я понял спецификацию, то думаю, что следующие правила дадут требуемый результат и могут быть записаны в виде короткой процедуры на языке 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 часть напрямую и добиться максимально возможной скорости, верно?

oOosys 14.06.2024 06:54

Почему эта форма? Чтобы добиться эффекта прогресса 2D-градиента в изображении RGB по «линии» между верхним левым и нижним правым пикселями изображения, при этом края изображения «ближе» к цвету левого верхнего пикселя и Центр высоты изображения ближе к цвету правого нижнего пикселя.

oOosys 14.06.2024 07:04

Я решил показать его в виде верхнего левого треугольника, включая диагональ и нижний правый треугольник, не подчеркивая структуру массива при Jmax != Kmax и его связь с квадратным массивом размера NxN. Это должно быть достаточно легко перевести в функцию Python. Я до сих пор не понимаю, что это преобразование делает для вас в пространстве RGB, чем гораздо более простое правило определения верхней линии таким же образом и установки x[k+1,j-1] = x[k,j]+1 для остальные.

Martin Brown 14.06.2024 10:18

При размере NxN схема деления на два треугольника и «параллелограмм» больше не работает, если вы решите сделать один треугольник «полноразмерным», потому что «параллелограмма» в этом случае не будет. Выбрав меньшие треугольники, составьте схему: верхний треугольник, параллелограмм, нижний треугольник всегда имеют там параллелограмм, сохраняя там своеобразную «симметрию». Вот почему я предпочитаю этот подход для обработки трех случаев: ширина > высота, ширина < высота, ширина = высота. Можете ли вы с этим согласиться, или я что-то здесь просмотрел?

oOosys 14.06.2024 10:29

В любом случае все должно быть в порядке, поскольку оба способа построения центральной диагонали должны давать одинаковый результат и быть согласованными с остальной частью шаблона. То, что два пути дадут один и тот же ответ, не влияет на алгоритм. Первое обнаруженное допустимое правило может вернуть правильный индекс. Вам необходимо полностью определить значения вдоль края самой короткой стороны, поэтому я выбрал шаблон именно так. Наличие одинаковых углов немного снижает сложность кода, поэтому может быть предпочтительнее. Я опубликовал код того, как я визуализировал решение проблемы. Улучшения возможны.

Martin Brown 14.06.2024 10:51

Да… дело сводится к заботе о «красоте» кода (симметрия обычно считается красивее, чем ее отсутствие). Результат тот же, поэтому, если «красота» не принимается во внимание, выбор размера треугольника не имеет значения и может осуществляться путем любой сознательной комбинации.

oOosys 14.06.2024 13:07

Чтобы быть полезным для быстрого создания 2D-массива numpy, код необходимо превратить в модуль Python, принимающий 1D-массив numpy любого типа, возвращающий 2D-массив numpy. Узким местом здесь является скорость выполнения цикла по всем индексам, присваивающим значения из 1D в 2D-массив, а не вычисление целевого индекса.

oOosys 16.06.2024 22:25

Это по-прежнему канонический ответ на ваш исходный вопрос, который обеспечивает краткую вычислительную стоимость O (1) для сопоставления 2D-индексации для массива J x K с длиной 1D линейного массива J*K. Вы запросили для этого код Python или C.

Martin Brown 16.06.2024 23:19

Хммм... вопрос запрашивал код для получения 2D-массива с учетом 1D-массива. Предоставленный код не создает запрошенный 2D-массив, необходимый для сравнения эффективности алгоритма с другими, представленными в других ответах.

oOosys 17.06.2024 11:20

Вам просто нужно создать 2D-массив numpy, а затем заполнить его, пройдя по индексам массива, чтобы заполнить диагональные линии j+k=2,3,4,.. в месте назначения, начиная с верхнего правого угла, чтобы шаблон доступа к памяти остается локальным и почти линейным при доступе к одномерному массиву. Вам придется сделать это в несколько полос, если количество строк в месте назначения велико, чтобы все действия происходили в кеше.

Martin Brown 17.06.2024 18:44

Я чувствую, что создание 2D-массива — неправильный путь, а создание 1D-массива — правильный. Изменение формы в 2D является тогда последним шагом без каких-либо затрат, кроме установки значений для шагов и формы массива numpy. Другими словами, с этой точки зрения на самом деле необходимо отображение 1D-массива в другой 1D-массив, который при изменении формы дает ожидаемый результат.

oOosys 17.06.2024 18:58

Решение

Вычисление индексов входного массива для желаемой формы и их последующее применение. Можно подготовить их один раз и применить ко многим массивам, если вы хотите сделать одну и ту же форму много раз.

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 16.06.2024 07:17

@Vitalizzare Готово, хотя на этот раз без вычурных рисунков пальцами :-)

no comment 16.06.2024 14:17

Я хочу помнить, что в ответах на другие вопросы этой серии одинаковых/похожих вопросов вы узнали, что работа с 1D-массивом даст самые быстрые результаты, так почему бы не создать соответствующий 1D-массив, а затем просто изменить форму это? Может быть, тогда это превзойдет скорость кода, предоставленного Vitalizzare?

oOosys 17.06.2024 11:25

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 17.06.2024 11:29

@oOosys Я работаю с одномерным массивом и одномерными индексами. Создаю ли я 2D-массив напрямую или через 1D-массив, как я и ожидал, разницы не имеет. Для m,n=1080,1920 я получаю 0,83 с для моего решения, как показано, и 0,15 с как для версии, которая просто выполняет return a[prepared_indices], так и для его версии через 1D.

no comment 17.06.2024 15:37

Я полагаю, что «хитрость» здесь заключается в использовании соответствующего среза при копировании значений из одного массива в другой. По какой-то еще недостаточно изученной причине это привело к более высокой скорости изменения диагоналей, чем при прохождении элемента за элементом. Я предполагаю, что выбор срезов, используемых для отображения, — это то, на чем необходимо сосредоточиться и детально разобраться, чтобы получить максимально быстрый результат в зависимости от формы массива. Как еще вы можете объяснить, что ваш код работает медленнее, чем тот, который опубликовал Vitalizzare?

oOosys 17.06.2024 19:03

Этот алгоритм дает вам необходимые элементы одномерного массива/списка в соответствии с индексом элемента массива, используя циклы и математические вычисления, поэтому он не использует слишком много памяти и разделен на 4 подалгоритма или шага для решения этой проблемы. :

  1. вычислить R, который возвращает количество элементов внутри диагональ, содержащая выделенный элемент по его индексу.

  2. Найдите порядок элементов внутри выбранной диагонали, используя элемент индекс 2D-массива (счёт начинается сверху вниз и слева по диагонали)

  3. изменение порядка элементов на требуемый порядок (счёт начинается с первого диагонального элемента, затем с последнего диагональный элемент влево вниз, затем возврат вверх и так далее)

  4. подсчет всех элементов до тех пор, пока элемент, который мы выбрали по индексу 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)

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

MRasheed 16.06.2024 17:27

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

oOosys 17.06.2024 20:08

Решение для 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)

Должен отметить, что схожие идеи есть и у следующих авторов:

  • @Neil - треугольные числа
  • @no comment — четные/нечетные элементы по диагонали
  • и возможно другие, я не все читал.

Приятно иметь C-код, предоставляющий массив для индексов. Чтобы сделать его полезным в контексте вопроса, потребуются некоторые дальнейшие шаги, которые затем решат, дает ли способ модуля C-расширения какое-либо преимущество перед чистым способом Python numpy.

oOosys 17.06.2024 20:25

Буду рад, если это поможет вам в работе.

freestyle 17.06.2024 20:53

Это скромная мотивация помочь мне, но я задаю здесь вопросы исключительно с целью обогатить stackoverflow интересными вопросами, дающими интересные ответы, которые, возможно, станут полезными для кого-то еще. Лично мне не нужны ответы в моей «работе», и то, над чем я на самом деле «работаю» (распространение информации о способе программирования oOo), — это совершенно другая пара обуви.

oOosys 17.06.2024 21:33

Ниже приведено сравнение времени предоставленных ответов, созданных с использованием приведенного ниже кода:

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 и наоборот, чтобы определить, насколько поведение кэша влияет на тайминги. Я до сих пор не могу понять, почему вам нужен именно этот диагональный доминирующий массив. Будет определенная оптимальная длина строки, соответствующая доступному кэшу.

Martin Brown 19.06.2024 09:29

Хммм... Присвоение значений новому массиву, полученному из другого массива, может производиться в любом произвольном порядке. Почему нельзя заранее определить порядок и упаковку срезов в зависимости от формы массива, чтобы минимизировать влияние формы массива на общее время?

oOosys 19.06.2024 14:28

Я не хочу ничего связанного с массивом... Я хочу понять, почему существуют различия в таймингах в зависимости от формы массива. При таком понимании должно быть возможно устранить или хотя бы минимизировать разницу во времени в зависимости от формы, верно?

oOosys 19.06.2024 14:30

Скорость зависит от того, как общий размер кэша на каждом уровне взаимодействует с размером перемещаемых объектов, размерами массива и размером кэша. В идеале при реструктуризации массива вы стремитесь сохранить его локальным кэшем, насколько это возможно. Обычно это предполагает укладку плитки. Каноническим примером такого рода оптимизации для алгоритма с поддержкой кэша является транспонирование большого массива. Обычно это делается в два этапа: перенесите небольшие квадратные плитки (возможно, на место), а затем переместите их в конечный пункт назначения. На современных процессорах с необычными правилами кэширования есть дополнительные особенности для некоторых магических длин.

Martin Brown 19.06.2024 15:07

Я подозреваю, что O(n) с полными 16Mx16M пикселями, одно ядро ​​будет работать медленно, несмотря ни на что. Некоторые решения действительно хороши для многоядерных процессоров или графических процессоров. Я кратко попробовал PyOpenCl, но мне даже не удалось его установить... Возможно, вы могли бы что-то попробовать, если не получите желаемой производительности.

Neil 20.06.2024 08:24

Копирование фрагментов из данного 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)

Попробуйте это онлайн!

Другие вопросы по теме