Минимальная сумма вертикального среза

1

Я хочу ввести размер матрицы N x N и вырезать срез таким образом, чтобы каждый элемент находился непосредственно ниже, слева-ниже или справа от него над ним. И стоимость - это сумма всех элементов в срезе. Как написать программу для этого?

Например. матрица указана в виде списка

[[1,2,3],
 [4,5,6],
 [7,8,9]]

который имеет следующие фрагменты:

(1,4,7), (1,4,8), (1,5,7), (1,5,8), (1,5,9), 
(2,4,7), (2,4,8), (2,5,7), (2,5,8), (2,5,9), (2,6,8), (2,6,9), 
(3,5,7), (3,5,8), (3,5,9), (3,6,8), (3,6,9)

Тогда срез наименьшего веса равен (1,4,7), который имеет сумму 12.

  • 0
    Это проблема dynamic programming . Что вы пробовали?
  • 0
    Пожалуйста, не просто копируйте формулировку проблемы из другого места и помещайте ее здесь; показать некоторые усилия, мысли, попытки ...
Теги:
algorithm

3 ответа

1

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

Тогда мы можем использовать, например, алгоритм Беллмана-Форда для нахождения кратчайшего пути в этих условиях. Ниже приведен пример реализации:

import numpy as np


m, n = 10, 10
M = np.arange(m*n).reshape(m, n) + 1
for i in range(1, m):
    M[i:] = np.roll(M[i:], 1 if i <= m // 2 else -1, axis=1)
print('Matrix:')
print(M, end='\n\n')


def edges():
    for i in range(m - 1):
        yield [(i, 0), (i + 1, 0)]
        yield [(i, 0), (i + 1, 1)]
        for j in range(1, n - 1):
            yield [(i, j), (i + 1, j - 1)]
            yield [(i, j), (i + 1, j)]
            yield [(i, j), (i + 1, j + 1)]
        yield [(i, n - 1), (i + 1, n - 1)]
        yield [(i, n - 1), (i + 1, n - 2)]


def compute_path(start):
    distance = {index: np.inf for index in np.ndindex(m, n)}
    predecessor = {index: None for index in np.ndindex(m, n)}

    distance[start] = M[start]
    for __ in range(M.size - 1):
        for u, v in edges():
            weight = M[v]
            if distance[u] + weight < distance[v]:
                distance[v] = distance[u] + weight
                predecessor[v] = u
    stop = min(filter(lambda x: x[0] == n - 1, distance), key=lambda y: distance[y])
    path = [stop]
    while predecessor[path[-1]] is not None:
        path.append(predecessor[path[-1]])
    return path[::-1], distance[stop]


paths = [compute_path((0, c)) for c in range(n)]
opt = min(paths, key=lambda x: x[1])
print('Optimal path: {}, with weight: {}'.format(*opt))
print('Vertices: ', M[list(zip(*opt[0]))])

Что дает в качестве результата:

Matrix:
[[  1   2   3   4   5   6   7   8   9  10]
 [ 20  11  12  13  14  15  16  17  18  19]
 [ 29  30  21  22  23  24  25  26  27  28]
 [ 38  39  40  31  32  33  34  35  36  37]
 [ 47  48  49  50  41  42  43  44  45  46]
 [ 56  57  58  59  60  51  52  53  54  55]
 [ 67  68  69  70  61  62  63  64  65  66]
 [ 78  79  80  71  72  73  74  75  76  77]
 [ 89  90  81  82  83  84  85  86  87  88]
 [100  91  92  93  94  95  96  97  98  99]]

Optimal path: [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 4), (7, 3), (8, 2), (9, 1)], with weight: 460
Vertices:  [ 1 11 21 31 41 51 61 71 81 91]
1

Как отметил Вивек, вы можете решить эту проблему с помощью динамической программы:

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

Вы можете легко инициализировать таблицу затрат. Просто скопируйте первую строку входной матрицы в таблицу. Затем мы собираемся заполнить остальную часть таблицы по строкам. Пусть C - матрица затрат, а M - входная матрица. Затем:

//Initialize cost table
for col = 0 to N - 1
    C(0, col) = M(0, col)
//Run dynamic program
for row = 1 to N - 1
    for col = 0 to N - 1
        //take the minimum of the three possible predecessors:
        //make sure that the entries exist (i.e., take care of the edges, not shown here)
        C(row, col) = M(row, col) 
                       + min(C(row - 1, col - 1)), C(row - 1, col), C(row - 1, col + 1))

После этого вам просто нужно найти минимум в последней строке C, что даст вам стоимость минимального фрагмента. Чтобы получить фактический фрагмент, пройдите указатели предшественника, которые вы настроили во время цикла (не показано в фрагменте псевдокода).

0

Эта проблема может быть представлена с использованием теории графов, а затем решена с использованием методов линейного программирования.

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

Мы можем использовать scipy.optimize.linprog (который использует алгоритм Simplex) для решения задачи линейного программирования cT @x. Каждый элемент решения представляет возможную связь между любым из N узлов (т.е. Вектор решения имеет размер N ** 2). Коэффициенты c определяют вес соединения: т.е. Вес узла, связанного с (значением матричного элемента), за исключением первого слоя, где нам нужно также добавить начальные веса.

Нам также необходимо применить несколько ограничений для получения правильного решения:

  1. Результат должен иметь ровно N - 1 ребра.
  2. Каждый узел не должен быть подключен к себе.
  3. Каждая строка должна подключаться к одному узлу, за исключением последней строки.
  4. Каждый узел может подключаться не более чем к одному другому узлу (за исключением тех, которые имеют последний уровень, который должен иметь нулевые соединения).
  5. Каждый узел должен уважать своих возможных преемников (которые задаются "формой" срезов).
  6. Для каждого из слоев с 1 по (N-1) любой подключенный узел должен подключиться к следующему слою.

Это довольно много частей, которые мы должны собрать вместе, и поэтому полученный код немного длинный и может показаться подавляющим вначале. Однако при более внимательном рассмотрении должно быть возможно определить отдельные фигуры и их структуру (я попытался как можно больше прокомментировать + добавил несколько отпечатков). Итак, вот пример кода:

import numpy as np
from scipy.optimize import linprog


M = np.arange(9).reshape(3, 3) + 1
print('Matrix:')
print(M)
print('\n\n')

N = len(M)

# Compute all possible connections between nodes (1: possible, 0: forbidden).
pc = np.zeros(shape=(N**2, N**2), dtype=int)

# Connect to nodes below (except the last layer).
i = np.arange(N**2 - N)
pc[i, i + N] = 1

# Connect to left nodes (except the last layer and leftmost column).
pc[i, i + N - 1] = 1
pc[i[::N], i[::N] + N - 1] = 0

# Connect to left nodes (except the last layer and rightmost column).
r = i + N + 1
mask = r < N**2
pc[i[mask], r[mask]] = 1
r = r[N-1::N]
mask = mask[N-1::N]
pc[i[N-1::N][mask], r[mask]] = 0

print('Possible connections:')
print(pc)
print('\n\n')

# Coefficients for linear programming problem represent the weight of connections.
c = np.zeros(shape=(N**2, N**2), dtype=int)
# Add weights for connections.
c = np.tile(M.ravel(), (N**2, 1))
# Add additional weights for first layer.
c[:N] += M[0, :][:, None]
print('Coefficient matrix:')
print(c)
print('\n\n')

# === Add constraints ===
A_eq_1 = np.concatenate((
    # Exactly N-1 connections.
    np.ones(N ** 4, dtype=int)[None, :],
    # No node can connect to itself.
    np.diag([1] * N**2).flatten()[None, :]
), axis=0)
b_eq_1 = np.asarray([N - 1, 0], dtype=int)
print('Exactly N-1 connections and no self-connecting nodes:')
print(A_eq_1)
print(b_eq_1)
print('\n\n')

# Each layer connects to exactly one other node (except the last layer).
A_eq_2 = np.zeros((N, N ** 4), dtype=int)
for j in range(N):
    A_eq_2[j, j * N**3:(j + 1)*N**3] = 1
b_eq_2 = np.ones(N, dtype=int)
b_eq_2[-1] = 0
print('Each layer connects to exactly one other node (except the last layer):')
print(A_eq_2)
print(b_eq_2)
print('\n\n')

# Each node connects to at most one other node (except the ones in the last layer).
N = N ** 2
A_ub_1 = np.zeros((N, N ** 2), dtype=int)
for j in range(N):
    A_ub_1[j, j * N:j * N + N] = 1
b_ub_1 = np.ones(N, dtype=int)
b_ub_1[-1] = 0
print('Each node connects to at most one other node (except the ones in the last layer):')
print(A_ub_1)
print(b_ub_1)
print('\n\n')

# Each node respects its possible succesors (i.e. forbid all other connections).
A_eq_3 = np.zeros((N, N ** 2), dtype=int)
for j in range(N):
    A_eq_3[j, j * N:j * N + N] = 1 - pc[j, :]
b_eq_3 = np.zeros(N, dtype=int)
print('Each node respects its possible succesors (i.e. forbid all other connections):')
print(A_eq_3)
print(b_eq_3)
print('\n\n')

# For the layers 1 through (N-1) each node connected to must connect to the next layer.
A_eq_4 = np.zeros((N, N ** 2), dtype=int)
for j in range(len(M), N-len(M)):
    A_eq_4[j, j::N] = 1
    A_eq_4[j, j*N:(j+1)*N] = -1
b_eq_4 = np.zeros(N, dtype=int)
print('For the layers 1 through (N-1) each node connected to must connect to the next layer:')
print(A_eq_4)
print(b_eq_4)
print('\n\n')

# Concatenate all constraints.
A_eq = np.concatenate([A_eq_1, A_eq_2, A_eq_3, A_eq_4])
b_eq = np.concatenate([b_eq_1, b_eq_2, b_eq_3, b_eq_4])

A_ub = np.concatenate([A_ub_1])
b_ub = np.concatenate([b_ub_1])

res = linprog(c.ravel(), A_ub=A_ub, b_ub=b_ub, A_eq=A_eq, b_eq=b_eq, bounds=(0, 1))
print(res.success)
print(res.x.reshape(N, N))  # Edges.

Последний результат - результат, и он имеет вид:

[[0. 0. 0. 1. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0.]]

Что говорит нам, что для того, чтобы получить минимальный путь, мы должны подключить узел 0 (индекс строки) к узлу 3 (индекс столбца), а также узел 3 (индекс строки) к узлу 6 (индекс столбца). Это представляет собой путь (или "срез") (1, 4, 7). Мы можем вывести путь, начиная с первой строки, а затем прогуливаясь по графику в виде точки ребер:

edges = res.x.reshape(N, N)
for i, r in enumerate(edges):
    # Numerical instabilities can cause some elements to have very small values > 0.
    if r.sum() > 0.5:
        print('Connect {} -> {}'.format(i, r.argmax()))

path = [edges[:len(M)].ravel().argmax() // N]
while edges[path[-1]].max() > 0.5:
    path.append(edges[path[-1]].argmax())
print('Path: ', path)
print('Elements: ', M.ravel()[path])
print('Path weight: ', M.ravel()[path].sum())

Итоговая записка

Вышеприведенный пример кода оставляет много возможностей для повышения производительности. Например, он рассматривает все возможные связи между узлами как решения, масштабируемые как M.size**2. Хотя мы ограничиваем возможные соединения, количество вычислений все еще намного больше, чем если бы мы ограничивали его с самого начала, включив его в проблемную архитектуру. Это означает, что вместо M.size**2 coefficients мы могли бы идти только с 2*(M.shape[0] - 1) + 3*(M.shape[1] - 2)*(M.shape[0] - 1) который масштабируется только как M.size. Плюс мы можем использовать гораздо меньшую матрицу ограничений, так как мы уже создали эти ограничения в архитектуре проблемы. Принимая приведенный выше пример кода в качестве основы, его можно соответствующим образом адаптировать. Таким образом, я остановлюсь на этом этапе и оставлю возможное улучшение производительности заинтересованному читателю.

(Вышеупомянутая реализация также работает только с квадратными матрицами, обобщение на неквадратные матрицы должно быть простым).

Ещё вопросы

Сообщество Overcoder
Наверх
Меню