Самый дешевый путь в матрице

Средняя

Описание задачи #

Матрица

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

Входные данные: [][]int (матрица m на n в которой нужно искать путь)

Выходные данные: int (стоимость самого дешевого пути)


Ограничения #


Примеры #

  • Входные данные

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

    Ответ: 7

  • Входные данные

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

    Ответ: 12

  • Входные данные

    [
        [1, 2, 3],
    ]
    

    Ответ: 6

  • Входные данные

    [
        [1],
        [2],
        [3],
    ]
    

    Ответ: 6


Брутфорс решение #

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

type node struct {
	price    int
	children []*node
}

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

nodeGrid := make([][]node, 0, len(grid))

for i := 0; i < len(grid); i++ {
    nodeGrid = append(nodeGrid, make([]node, 0, len(grid[i])))
	
    for j := 0; j < len(grid[i]); j++ {
        nodeGrid[i] = append(nodeGrid[i], node{
            price:    grid[i][j],
            children: make([]*node, 0),
        })

        if i == 0 && j == 0 {
            continue
        }

        if i == 0 {
            nodeGrid[i][j-1].children = append(nodeGrid[i][j-1].children, &nodeGrid[i][j])
            continue
        }

        if j == 0 {
            nodeGrid[i-1][j].children = append(nodeGrid[i-1][j].children, &nodeGrid[i][j])
            continue
        }

        nodeGrid[i][j-1].children = append(nodeGrid[i][j-1].children, &nodeGrid[i][j])
        nodeGrid[i-1][j].children = append(nodeGrid[i-1][j].children, &nodeGrid[i][j])
    }
}

Теперь, имея граф с проставленными связями, мы можем с помощью обхода в глубину обойти все возможные пути от элемента nodeGrid[0][0] до элемента nodeGrid[m - 1][n - 1], сравнив стоимость путей.

func minPathSumTraverse(root node) int {
	if len(root.children) == 0 {
		return root.price
	}
	
	minPathPrice := math.MaxInt
	
	for _, child := range root.children {
		childPathPrice := minPathSumTraverse(*child)
		
		if childPathPrice < minPathPrice {
			minPathPrice = childPathPrice
		}
	}

	return root.price + minPathPrice
}

Оценка сложности #

По времени

Для того чтобы найти минимальный путь, нам нужно обойти все возможные пути в матрице. Каждый путь состоит из n+m-1 элементов. Длина каждого пути m+n-1, Не считая элементов первой/последней строки и первого/последнего столбца, из каждого элемента есть 2 пути. Суммарное количество возможных путей и сложность получившегося алгоритма можно оценить как O(2(n+m)).

По памяти

Мы ввели дополнительный граф размером m * n, поэтому дополнительную память мы можем оценить в O(m * n).


Оптимальное решение #

Оптимальное решение может получиться с использованием методов динамического программирования: мы разобьем задачу на более маленькие подзадачи.

Давайте предположим, что мы знаем самые дешевые пути от элемента матрицы grid[0][0] до элементов grid[n - 2][m - 1] (соседнего слева от правого нижнего элемента) и grid[n - 1][m - 2] (соседнего сверху от правого нижнего элемента).

Так как мы можем перемещаться либо вправо, либо вниз. Самый дешевый путь до правого нижнего элемента:

$$ grid[n-1][m-1] = min(grid[n-2][m-1], grid[n-1][m-2]) + grid[n-1][m-1] $$

Точно по такому же принципу мы можем вычислить самый короткий путь до элементов grid[n - 2][m - 1] и grid[n - 1][m - 2]. Таким образом, мы можем вычислить стоимость пути до каждого из элементов, зная стоимость пути до соседа слева и соседа сверху.

Давайте модифицируем исходную матрицу (так как нет требований к иммутабельности для экономии памяти будем менять значения в матрице in-place), преобразовав значение каждого из элементов в стоимости пути до этого элемента.

Для этого нам достаточно воспользоваться стандартным обходом матрицы в цикле по i и j. Первый элемент матрицы оставим без изменений, так как он является нашей отправной точкой.

Стоимость пути до любого элемента на первой строчке — стоимость пути до предыдущего элемента и значение текущего (так как к любому элементу на первой строке можно прийти только из элемента слева).

if i == 0 {
    grid[i][j] = grid[i][j-1] + grid[i][j]

    continue
}

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

if j == 0 {
    grid[i][j] = grid[i-1][j] + grid[i][j]

    continue
}

Для всех остальных элементов мы высчитываем стоимость, выбирая меньший из путей (либо сверху, либо слева).

minPrices := grid[i][j-1]
if grid[i-1][j] < minPrices {
    minPrices = grid[i-1][j]
}

grid[i][j] = minPrices + grid[i][j]

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

  • package main
    
    func minPathSum(grid [][]int) int {
    	var i, j int
    	for i = 0; i < len(grid); i++ {
    		for j = 0; j < len(grid[i]); j++ {
    			if i == 0 && j == 0 {
    				continue
    			}
    
    			if i == 0 {
    				grid[i][j] = grid[i][j-1] + grid[i][j]
    
    				continue
    			}
    
    			if j == 0 {
    				grid[i][j] = grid[i-1][j] + grid[i][j]
    
    				continue
    			}
    
    			minPrices := grid[i][j-1]
    			if grid[i-1][j] < minPrices {
    				minPrices = grid[i-1][j]
    			}
    
    			grid[i][j] = minPrices + grid[i][j]
    		}
    	}
    
    	return grid[i-1][j-1]
    }

  • export const minPathSum = (grid: number[][]): number => {
        let i = 0;
        let j = 0;
    
        for (i; i < grid.length; i++) {
            for (j = 0; j < grid[i].length; j++) {
                if (i == 0 && j == 0) {
                    continue
                }
    
                if (i == 0) {
                    grid[i][j] = grid[i][j - 1] + grid[i][j]
                    continue
                }
    
                if (j == 0) {
                    grid[i][j] = grid[i - 1][j] + grid[i][j]
                    continue
                }
    
                let minPrices = grid[i][j - 1]
                if (grid[i - 1][j] < minPrices) {
                    minPrices = grid[i - 1][j]
                }
    
                grid[i][j] = minPrices + grid[i][j]
            }
        }
    
        return grid[i - 1][j - 1]
    }

Оценка сложности #

По времени

Для того чтобы высчитать самый дешевый путь, нам достаточно один раз пройтись по всей матрице. То есть итоговая сложность — O(m * n).

По памяти

O(1) — дополнительная память константна.

Оригинал задачи на Leetcode
< Предыдущая задача
Следующая задача >