Решение этой проблемы LeetCode для графиков
Учитесь ли вы в колледже или совершенствуете свои навыки для технических собеседований, у всех нас есть время в жизни, когда мы сталкиваемся с графиками. Графики — это удивительная структура данных для хранения сложных взаимосвязей между объектами, сущностями и даже людьми (как на Facebook!). Эта структура состоит из ребер и вершин и имеет различные способы реализации. У Geeksforgeeks есть отличный обзор графиков здесь, если вам нужно освежить в памяти.
Для целей этой статьи мы столкнемся с графами, реализованными с помощью матрицы смежности. Эта реализация использует вложенный массив и отслеживает ребра между двумя путями, используя координаты (i,j)
где i
и j
являются вершинами. Итак, если есть путь между i
и j
, значение по координате i, j
будет 1
, иначе будет 0
.
Проблема
Сегодня мы будем вместе решать общую задачу на собеседовании, известную на LeetCode как Число анклавов:
Вам дана бинарная матрица
m x n
grid
, где0
представляет ячейку моря, а1
представляет ячейку суши. Движение состоит в переходе из одной ячейки земли в другую соседнюю (в 4-х направлениях) ячейку земли или в выходе за границуgrid
. Возвращает количество ячеек земли вgrid
для которых мы не можем выйти за границу сетки ни за какое количество ходов.
Здесь матрица смежности используется для отслеживания того, есть ли между i,j
, и нам нужно подсчитать всю землю. Однако есть подвох: нам нужно исключить земли, у которых есть путь к границе матрицы. Мы будем решать эту проблему, используя алгоритм поиска в глубину (DFS). Идея состоит в том, чтобы перебрать каждую ячейку в сетке и, если ячейка является землей («1»), выполнить поиск в глубину, чтобы пометить все соединенные земли как посещенные и проверить, находится ли ячейка на границе сетки. Если он не на границе, увеличьте количество островов. Легче сказать, чем сделать, я знаю..
Прежде чем мы начнем, важно понять, что такое алгоритм поиска в глубину (DFS). Этот алгоритм, по сути, полностью исследует каждый путь, прежде чем проверять следующий возможный путь из заданной начальной точки. Так, например, если у вас есть начальная точка A с путями B и C, она сначала исследует путь B и все пути B, прежде чем проверит C. Подробное объяснение этого алгоритма читайте здесь .
Решение
Без дальнейших церемоний, давайте начнем с определения количества анклавов. Во-первых, давайте перечислим все, что нам нужно проверить:
- Проверить, находится ли позиция (r, c) в границах заданного графика. Когда мы проверяем каждую координату и ее пути, нам нужно убедиться, что мы находимся на допустимой координате, поскольку мы имеем дело с матрица (которая реализована под капотом с помощью массивов/списков). Если бы у нас был вспомогательный метод, который каждый раз справлялся бы с этим за нас, это сделало бы нашу жизнь намного проще.
- Проверить, находится ли позиция (r, c) рядом с позицией за пределами. Поскольку мы ищем пути за пределами границ, нам нужно будет проверить, какая земля имеет доступ к местоположению за пределами границ. Мы создадим этот помощник, чтобы изолировать эту логику при реализации более сложного алгоритма.
- Проверить, имеет ли позиция (r, c) путь к позиции за пределами поля, просматривая все пути в четырех направлениях (вверх, вниз, влево, вправо). Этот метод будет использовать два других наших метода. вспомогательные методы с рекурсивной DFS для поиска любой связанной земли, которая имеет доступ к положению вне границ.
- Отметьте посещенные позиции, особенно для всех позиций на пути к позиции за пределами поля, посетив все пути в четырех направлениях (вверх, вниз, влево, вправо). Это будет важно, чтобы помочь нам считать только те земли, которые не находятся на пути за пределы. Мы хотим убедиться, что земли, на которых есть такие пути, не учитываются, и мы можем сделать это, отмечая их как посещенные, когда мы проходим через них.
- Повторяйте по графику, отслеживая количество земель, которые не находятся на пути к выходу за пределы.
Реализация
Ниже приведены определения функций задач/вспомогательных методов, которые мы определили выше. Позже в этой статье мы рефакторим некоторые методы из нашей первой реализации, чтобы упростить пространство и время. Однако для начала давайте сосредоточимся на том, как работает каждый отдельный шаг, чтобы полностью понять проблему и решение. Прочитайте пояснения с комментариями во фрагменте кода ниже, чтобы понять шаги.
// return true if r,c is in bounds, else false const isInBounds = (grid, r, c) => {} // returns true if r,c is adjacent to boundary const canWalkOffBoundary = (grid, r, c) => {} // return true if r,c has a path to boundary and track visited coordinates const hasPathToBoundary = (grid, r, c, visited) => {} // mark all connected coordinates as visited by setting its value to 0 const floodFillDFS = (grid, r, c, color=1, newColor=0) => {} // Number of Enclaves: calculate number of lands that don't have a path to boundary const numEnclaves = (grid) => {}
Мы можем начать с реализации некоторых из наших более простых методов. Вот моя реализация метода isInBounds
method:
// Return true if r,c is in bounds, else false const isInBounds = (grid, r, c) => { // ensure our row and cols are not undefined if (!grid || !grid[r]) { return false } // ensure r is a valid row if (r < 0 || r > grid.length) { return false } // ensure c is a valid column for given row r if (c < 0 || c > grid[r].length) { return false } // if it passes all above checks, it must be a valid point return true }
Вот как проверить, находится ли координата на границе:
// returns true if point r,c is adjacent to boundary const canWalkOffBoundary = (grid, r, c) => { // check if r is a bordering row (first or last index) if (r === 0 || r === grid.length-1) { return true } // check if cis a bordering col of row r (first or last index) if (c === 0 || c === grid[r].length-1) { return true } // if it isn't, then it must not be on the border return false }
Теперь о более интересных методах. Метод hasPathToBoundary
требует, чтобы мы проверили все связанные земли с заданной координатой и вернули true, если какая-либо из связанных земель находится на границе. Здесь мы также хотим отслеживать наши посещенные точки, чтобы предотвратить бесконечный цикл.
const hasPathToBoundary = (grid, r, c, visited) => { // if we already visited this coordinate, exit method by returning null if (visited.has(`${r},${c}`)) { return } // add our coordinate to the visited Set visited.add(`${r},${c}`) // check if it is a valid coordinate (aka it is in bounds and it is land), // else return false since we have checked all paths if (!isInBounds(grid, r, c) || grid[r][c] !== 1) { return false } // check if this coordinate can walk off boundary using our helper, if so return true if (canWalkOffBoundary(grid, r, c)) { return true } // recursively call this method to check paths on adjacent lands return hasPathToBoundary(grid, r+1, c, visited) || hasPathToBoundary(grid, r-1, c, visited) || hasPathToBoundary(grid, r, c-1, visited) || hasPathToBoundary(grid, r, c+1, visited) }
Теперь давайте реализуем наш метод заливки, чтобы убедиться, что мы помечаем все наши земли на пути к границе как посещенные. Мы должны сделать это, чтобы не выполнять лишнюю работу и помочь нам учитывать только те земли, которые не находятся на пути за границей.
// mark all connected coordinates as visited by setting its value to 0 const floodFillDFS = (grid, r, c, color=1, newColor=0) => { // Make sure it is in bounds, it is a land, and not already visited if (!isInBounds(grid, r, c) || grid[r][c] !== color || grid[r][c] === newColor) { return 0 } // set as visited by setting it to 0 grid[r][c] = newColor // recursively call on all adjacent paths floodFillDFS(grid, r-1, c, color) floodFillDFS(grid, r+1, c) floodFillDFS(grid, r, c-1) floodFillDFS(grid, r, c+1) }
Теперь, наконец, о нашем основном методе. Здесь мы пройдем все пути в наших графах, используя вложенный цикл for, поскольку наш граф настроен как список смежности.
const numEnclaves = (grid) => { // track number of land cells without a path to boundary let numInvalidCells = 0 // loop over all rows for (let r = 0; r < grid.length; r++) { // loop over all columns in the row for (let c = 0; c < grid[r].length; c++) { // get value of current position const value = grid[r][c] // if it is land if (value === 1) { const visited = new Set() // check if it has a path to boundary const isTrue = hasPathToBoundary(grid, r, c, visited) if (isTrue) { // if it does, mark all as visited and DON'T increment count floodFillDFS(grid, r, c) } else { // else, it is a land cell without path to boundary // so let's add to our count numInvalidCells += 1 } } } } // return our count of land cells without path to boundary return numInvalidCells };
Рефакторинг
Ух ты! Мы сделали это. Но мне кажется, что мы здесь немного дублируем работу. Когда мы проверяем путь к границе и заливаем заливку, мы дважды проходим все пути, прилегающие к точке. Есть ли способ, которым мы могли бы реорганизовать это, чтобы сделать это вместе? (Попробуйте сами!)
Да, есть! Итак, для нашего окончательного, рефакторинга и чистого решения мы будем использовать следующие определения:
// return true if r,c is in bounds, else false const isInBounds = (grid, r, c) => {} // returns true if r,c is adjacent to boundary const canWalkOffBoundary = (grid, r, c) => {} // return true if r,c has a path to boundary and track visited coordinates // mark all connected coordinates as visited by setting its value to 0 // we will use the visited array to count visited lands if there is no path to boundary const floodFillDFSWithCheck = (grid, r, c, visited, color=1, newColor=0) => {} // Number of Enclaves: calculate number of lands that don't have a path to boundary const numEnclaves = (grid) => {}
Таким образом, мы все еще можем использовать наши методы isInBounds
и canWalkOffBoundary
, но здесь мы рефакторим наши методы hasPathToBoundary
и floodFillDFS
в метод floodFillDFSWithCheck
. Вот как:
// return true if r,c has a path to boundary and track visited coordinates // mark all connected coordinates as visited by setting its value to 0 // we will use the visited array to count visited lands if there is no path to boundary const floodFillDFSWithCheck = (grid, r, c, visited, color=1, newColor=0) => { // if out of bounds or not land or already checked (aka converted to newColor) if (!isInBounds(grid, r, c) || grid[r][c] !== color || grid[r][c] === newColor) { return false } // convert to new color to set visited grid[r][c] = newColor // add to visited array to help us keep count of what we changed // in case it is not a access to walk off boundary land visited.push(`${r},${c}`) // call each recursive call separately so we calculate each boolean result // we want to see if any of these paths return true for finding a boundary const isValid = canWalkOffBoundary(grid, r, c) const fill1 = floodFillDFSWithCheck(grid, r-1, c, visited) const fill2 = floodFillDFSWithCheck(grid, r+1, c, visited) const fill3 = floodFillDFSWithCheck(grid, r, c-1, visited) const fill4 = floodFillDFSWithCheck(grid, r, c+1, visited) // here return all using the || to return true if any of these are true return isValid || fill1 || fill2 || fill3 || fill4 }
Теперь нам нужно реорганизовать наш метод numEnclaves
, чтобы использовать эту новую реализацию:
const numEnclaves = (grid) => { let numInvalidCells = 0 for (let r = 0; r < grid.length; r++) { for (let c = 0; c < grid[r].length; c++) { const value = grid[r][c] // if we found land if (value === 1) { // track visited points let visited = [] // check if there is a path to boundary const isOffBounds = floodFillDFSWithCheck(grid, r, c, visited) // if there isn't, update our count of land without path to boundary // to all the lands we found and marked as visited on our DFS search if (!isOffBounds) { numInvalidCells += visited.length } } } } return numInvalidCells };
Спасибо за прочтение!! Пожалуйста, прокомментируйте ниже, если у вас есть какие-либо вопросы или предложения по улучшению этого алгоритма.
Подпишитесь на DDIntel Здесь.
Посетите наш сайт здесь: https://www.datadriveninvestor.com
Присоединяйтесь к нашей сети здесь: https://datadriveninvestor.com/collaborate