算法学习记录(四十九)

126 阅读1分钟

问:

  1. 最长回文子序列
  2. 让字符串成为回文串的最少插入次数
  3. 分割回文串 II

解:

  1. 解法一: 反转字符串,求反转字符串和原字符串的最长公共子序列;解法二:范围上的尝试模型,找str在i~j范围上的最长回文子序列长度
function longestPalindromeSubseq { 
    const str2 = [...str].reverse().join('') 
    return getMaxLength(str, str2) 
    function getMaxLength(str1, str2) { 
        const dp = [] 
        for (let i = 0; i < str1.length; i++) { 
            dp[i] = [] 
            for (let j = 0; j < str2.length; j++) { 
                if (i === 0) { 
                    dp[i][j] = str1[i] === str2[j] ? 1 : dp[i][j - 1] ?? 0 
                    continue 
                } 
                if (j === 0) { 
                    dp[i][j] = str1[i] === str2[j] ? 1 : dp[i - 1][j] ?? 0 
                    continue 
                } 
                let p1 = 0 
                let p2 = 0 
                // 回文串可能性分析
                // p1.以i结尾   以j结尾
                // p2.不以i结尾 不以j结尾
                // p3.不以i结尾  以j结尾
                // p4.以i结尾   不以j结尾
                if (str1[i] === str2[j]) { 
                    p1 = dp[i - 1][j - 1] + 1 
                } 
                p2 = dp[i - 1][j - 1] 
                const p3 = dp[i - 1][j] 
                const p4 = dp[i][j - 1] 
                dp[i][j] = Math.max(p1, p2, p3, p4)
            } 
        } 
        return dp[str1.length - 1][str2.length - 1]
    } 
};
function longestPalindromeSubseq(str) {
    const dp = []
    for (let i = 0; i < str.length; i++) {
        dp[i] = []
        for (let j = 0; j < str.length; j++) {
            if (i > j) dp[i][j] = 0
            if (i === j) dp[i][j] = 1
            if (j === i + 1) dp[i][j] = str[i] === str[j] ? 2 : 1
        }
    }
    for (let i = str.length - 3; i >= 0; i--) {
        for (let j = i + 2; j < str.length; j++) {
            if (str[i] === str[j]) {
                dp[i][j] = (dp[i + 1][j - 1] ?? 0) + 2
                continue
            }
            dp[i][j] = Math.max(dp[i + 1][j] ?? 0, dp[i][j - 1] ?? 0, dp[i + 1][j - 1] ?? 0)
        }
    }
    return dp[0][str.length - 1]
}
// 与上一题思路一样,范围上的尝试模型。 求str在i~j范围上的需要增加几个字符
// 暴力递归
function minInsertions(str) {
   function getRes(left, right) {
        if (left > right) return Infinity
        if (right === left) return 0
        if (right === left + 1) return str[left] === str[right] ? 0 : 1
        // left和right位置字符相等,不用管这两个位置
        if (str[left] === str[right]) return getRes(left + 1, right - 1)
        // 剩下两种可能:把left + 1 ~ right范围变成回文,再把left字符添加一个。 把left到right-1范围变成回文,再把right字符添加一个。
        return Math.min(getRes(left + 1, right) + 1, getRes(left, right - 1) + 1)
    }
    return getRes(0, str.length - 1)
};
// 递归改dp
function minInsertions(str) {
    const dp = []
    for (let i = 0; i < str.length; i++) {
        dp[i] = []
        for (let j = 0; j < str.length; j++) {
            if (i > j) {
                dp[i][j] = Infinity
                continue
            }
            if (i === j) {
                dp[i][j] = 0
                continue
            }
            if (i === j - 1) {
                dp[i][j] = str[i] === str[j] ? 0 : 1
            }
        }
    }
    for (let i = str.length - 3; i >= 0; i--) {
        for (let j = i + 2; j < str.length; j++) {
            if (str[i] === str[j]) {
                dp[i][j] = dp[i + 1][j - 1]
                continue
            }
            dp[i][j] = Math.min(dp[i + 1][j] + 1 , dp[i][j - 1] + 1)
        }
    }
    return dp[0][str.length - 1]
}
// 暴力递归
function minCut(str) {
    if (str.length <= 1) return 0

    function getRes(curIdx) {
        if (curIdx === str.length) return 0
        let res = Infinity
        for (let i = curIdx; i < str.length; i++) {
            // 判断 当前是否是回文
            const flag = isPalindrome(str.slice(curIdx, i + 1))
            if (flag) {
                res = Math.min(getRes(i + 1) + 1, res)
            }
        }
        return res
    }
    return getRes(0) - 1
    function isPalindrome(s) {
        // 去除异常的字符,先全部转成小写
        const str = s.toLocaleLowerCase().replace(/[\W_]/ig, '')
        // 提前获取处理后字符串的长度,省的在下边重复获取,用空间换时间
        const l = str.length
        // 循环次数,如果处理后字符串长度为偶数,/2是整数就不用处理,但是考虑会有基数(12321)的情况,所以加个Math.ceil 取整,当然你开心的话l/2%1 === 0 判断处理也行
        for (let i = 0;i < Math.ceil(l / 2);i ++) {
            // 判断首位对应的索引不一致直接返回 fase
            if (str[i] !== str[l - i - 1]) {
                return false
            }
        }
        // 循环走完了证明没问题,返回true
        return true
    }
}
// 递归改dp。 按理来说判断是否回文这个环节要预处理生成一个dp表优化判断回文的遍历过程。但是coding大同小异就不改了。
function minCut(str) {
    if (str.length <= 1) return 0
    const dp = []
    dp[str.length] = 0
    for (let i = str.length - 1; i >= 0; i--) {
        dp[i] = Infinity
        for (let j = i; j < str.length; j++) {
            // 判断 当前是否是回文
            const flag = isPalindrome(str.slice(i, j + 1))
            if (flag) {
                dp[i] = Math.min(dp[j + 1] + 1, dp[i])
            }
        }
    }
    return dp[0] - 1

    // return getRes(0) - 1
    function isPalindrome(s) {
        const str = s.toLocaleLowerCase().replace(/[\W_]/ig, '')
        const l = str.length
        for (let i = 0;i < Math.ceil(l / 2);i ++) {
            if (str[i] !== str[l - i - 1]) {
                return false
            }
        }
        return true
    }
}