[LeetCode Select Lectures, número 11] "Generación de soportes" (búsqueda DFS, programación dinámica DP)

Generación de brackets T22

Enlace del tema: leetcode-cn.com/problems/ge…

Idea 1: Búsqueda DFS

solución ingenua

Este es un problema típico de "búsqueda DFS", similar a las "Combinaciones de letras de números de teléfono" que estudiamos anteriormente , pero aquí lo analizaremos con más detalle.

Para tales temas, a fin de facilitar el inicio, es mejor que no consideremos ninguna optimización, sino que solo consideremos los siguientes problemas:

  1. ¿Dónde empezó la búsqueda?
  2. ¿Cómo buscar?
  3. ¿Cuántas ramas hay debajo de cada nodo de búsqueda?
  4. ¿Cómo saber si una ruta de búsqueda termina?

Para el problema (1), sabemos por el significado de la pregunta que si queremos formar una combinación de corchetes cerrados, el corchete más a la izquierda debe ser (, y este es nuestro punto de partida.

Para el problema (2), solo necesitamos comenzar desde el extremo izquierdo y buscar (en realidad enumerar) todos los casos posibles poco a poco.

Para el problema (3), dado que aquí no consideramos la optimización, solo hay dos ramas (es decir, dos posibilidades) debajo de cada nodo de búsqueda: (, ).

Para la pregunta (4), de acuerdo con el significado de esta pregunta, sabemos que si queremos finalizar una ruta de búsqueda, solo hay dos situaciones: ① Se encuentra que se ha buscado la respuesta correcta; ② Se encuentra que es imposible seguir buscando para obtener la respuesta correcta.

Consideremos primero lo primero.

Para facilitar la expresión, también podríamos registrar el (número de búsquedas como leftNum, y las búsquedas )como rightNum.

Obviamente, fue solo leftNum == rightNum == nentonces se encontró la respuesta correcta.

"¡Espera! No es suficiente asegurarse de que el número de paréntesis izquierdo y derecho sea igual, ¡también debes asegurarte de que se puedan cerrar uno por uno! Si tienes esta duda en tu corazón ahora, es genial. Pero no te preocupes, analicemos esto último primero.

¿Cuándo es imposible obtener la respuesta correcta más abajo?

首先,题目已经规定了答案中括号对的个数n,也就是说,倘若我们对每个答案枚举的字符个数超过n * 2(下记作totalNum)却发现其不是正确答案,那么该路径必然需要结束。

其次,为了使得所有的括号对都有闭合的机会,必须保证搜索过程中始终有leftNum ≥ rightNum

这个条件稍稍有点难度,如果有同学想不明白的话,不妨用「反证法」:

假设leftNum < rightNum,表明已搜索到的左侧区域必定有多余的)存在,而此时无论我们如何在右侧区域添加括号(即继续往下搜索),都不可能使前面多余的)闭合。因此此时搜索到的结果肯定不是正确答案!

现在相信你已经解开了刚才的疑惑。是的,我们只要在搜索的中途将括号无法闭合的情况予以排除,就不需要在校验最终正确答案的时候考虑这个问题了!

现在我们已经分析清楚了这四个问题,下面直接运用我们的老朋友「递归」来实现「DFS搜索」就行了。由于代码比较简单,这里不再仔细讲解。如果你是初次接触此类题目的同学,请尝试找一找刚才的四个问题分别对应代码的哪一部分。

代码如下:

function generateParenthesis(n) {
    const ansArr = [];
    dfs(n * 2, 1, 0, '(', ansArr);
    return ansArr;
}
function dfs(totalNum, leftNum, rightNum, ans, ansArr) {
    if (totalNum === leftNum + rightNum) {
        leftNum === rightNum && ansArr.push(ans);
    }
    else if (leftNum >= rightNum) {
        dfs(totalNum, leftNum + 1, rightNum, ans + '(', ansArr);
        dfs(totalNum, leftNum, rightNum + 1, ans + ')', ansArr);
    }
}

剪枝优化

我们刚才已经编写出了完全不带任何优化的版本,下面我们尝试对其进行「剪枝优化」。

什么是「剪枝优化」呢,从字面上理解,就是在搜索的过程中及时发现并"剪去"搜索树上不可能抵达正确答案的"枝条"。

换句话说,就是我们需要在搜索过程中对一些具体情况进行研判,使得我们尽可能走可能抵达正确答案的搜索路径,规避不可能抵达正确答案的路径。

首先是我们很容易想到的两种情况:

  • leftNum = rightNum时,答案中下一个字符必须是(
  • leftNum + rightNum = totalNum - 1时,答案中下一个字符必须是).

还要其他可以进行剪枝的情况吗?

答案是肯定的。别忘了我们之前分析过,如果要找到正确答案,必须保证搜索过程中不等式leftNum ≥ rightNum始终成立。那么反过来说,如果走某一条路径会打破这个不等式,那么搜这个路径一定不会得到正确答案!

根据上面的分析,我们优化的代码如下:

function generateParenthesis(n) {
    const ansArr = [];
    //注意:现在这里若写dfs(n * 2, 0, 0, '', ansArr);也是可以的
    //思考:为什么?
    dfs(n * 2, 1, 0, '(', ansArr);
    return ansArr;
}
function dfs(totalNum, leftNum, rightNum, ans, ansArr) {
    if (totalNum === leftNum + rightNum) {
        leftNum === rightNum && ansArr.push(ans);
    }
    //思考:为什么先前代码中的条件leftNum >= rightNum已不再需要?
    else {
        if (leftNum === rightNum) {
            dfs(totalNum, leftNum + 1, rightNum, ans + '(', ansArr);
        } else if (leftNum + rightNum === totalNum - 1) {
            dfs(totalNum, leftNum, rightNum + 1, ans + ')', ansArr);
        } else {
            leftNum + 1 >= rightNum && dfs(totalNum, leftNum + 1, rightNum, ans + '(', ansArr);
            leftNum >= rightNum + 1 && dfs(totalNum, leftNum, rightNum + 1, ans + ')', ansArr);
        }
    }
}

拓展:从代数角度理解

下面我们来提一下如何从代数角度来理解本题中「DFS搜索」的实现。

我们可以记出现一个(为得+1分,出现一个)为得-1分。那么对于符合题意的括号组合,都有:

  • 当搜索到最终结果时,有总分score = 0,即leftNum = rightNum. 且搜索深度deep = n * 2,即leftNum + rightNum = totalNum
  • 在搜索过程中,总分始终满足0 ≤ score ≤ n,即leftNum ≥ rightNumleftNum ≤ n

这两条结论与我们上文中对题意的分析是完全吻合的。

代码如下:

function generateParenthesis(n) {
    const ansArr = [];
    dfs(n * 2, 1, 1, '(', ansArr);
    return ansArr;
}
function dfs(maxDeep, deep, score, ans, ansArr) {
    if (deep === maxDeep) {
        score === 0 && ansArr.push(ans);
    } else {
        if (score === 0) {
            dfs(maxDeep, deep + 1, score + 1, ans + '(', ansArr);
        } else if (deep === maxDeep - 1) {
            dfs(maxDeep, deep + 1, score - 1, ans + ')', ansArr);
        } else {
            score + 1 <= maxDeep/2 && dfs(maxDeep, deep + 1, score + 1, ans + '(', ansArr);
            score - 1 >= 0 && dfs(maxDeep, deep + 1, score - 1, ans + ')', ansArr);
        }
    }
}

思路二:DP动态规划

从观察开始

下面我们思考一个问题:这道题可以用「动态规划」进行解答吗?

我们知道,如果一道题可以用「动态规划」进行解答,那么题目中必须蕴含着某种递推关系

如果你还没有任何头绪的话,我们可以先来观察一下使用前面「DFS搜索」获得的几组正确答案:

1.png

请留意:是不是当n比较大时的答案,看上去像n比较小时的答案组装起来的

为了进一步发掘其中的规律,我们来举一个具体的例子分析一下。

比如当n = 4时的一个答案'(()())()'。为了更好地观察,我们拆开来看:'( ()() ) ()'

Ahora que todo es obvio, parece que esta respuesta debería ser una respuesta dentro de un nuevo conjunto de paréntesis n = 2y una respuesta ()()fuera de él n = 1. ¡Sabemos que de 1 + 2 + 1 = 4ahí viene esta n = 4respuesta!

relación de recurrencia

Hay muchos más ejemplos como el anterior. Al analizarlos, la relación recursiva que estamos buscando está lista para emerger:

Si la especificación dp[n]representa cualquier respuesta cuando el nnúmero ,

Hay dp[n] = "(" + dp[p] + ")" + dp[q], de los cuales p + q + 1 = n.

Código

¡ Usando la relación de recurrencia, solo decorrectasrespuestaslascontar1necesitamosn - 1ndp[n]dp[n - 1]

el código se muestra a continuación:

function generateParenthesis(n) {
    /* 初始化dp数组 */
    const dp = [['']];
    for(let i = 1; i <= n; i++) dp[i] = [];
    /* 动归核心部分 */
    for(let curN = 1; curN <= n; curN++) {
        for(let p = 0; p < curN; p++) {
            let q = curN - p - 1;
            //由于dp[n]中可能包含了多个答案,
            //所以别忘了需要对其逐个进行遍历。
            for(let inside of dp[p]) {
                for(let outside of dp[q]) {
                    dp[curN].push("(" + inside + ")" + outside);
                }
            }
        }
    }
    /* 返回所求结果 */
    return dp[n];
}

escribir al final

Soy PAK Sunflower del Club de desarrollo de juegos de Jiangnan , un grupo de interés de programación estudiantil en la escuela . Actualmente estamos trabajando en el desarrollo de nuestro juego para fanáticos de la web sin fines de lucro desarrollado por nosotros mismos "Plants vs. Zombies: Travel" para ejercitar nuestra interfaz capacidades de desarrollo de aplicaciones.

Le invitamos sinceramente a experimentar nuestro excelente trabajo. Si le gusta TA, puede recomendarlo a sus colegas y amigos. Si tiene preguntas técnicas que le gustaría discutir con nosotros, contácteme directamente. ¡Tu apoyo es nuestra mayor motivación!

QQ图片20220701165008.png

Supongo que te gusta

Origin juejin.im/post/7118297882922319908
Recomendado
Clasificación