DP

163 阅读4分钟
  1. 最小删除距离(只允许删除)
/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
//两字符串最小编辑距离
//二维dp数组,行和列大小为length + 1
var minDistance = function(word1, word2) {
  let dp = [];
  for (let i = 0; i <= word1.length; i++) {
      dp.push(new Array(word2.length + 1));
  }

  for (let i = 0; i <= word1.length; i++) {
      for (let j = 0; j <= word2.length; j++) {
          //i为0,编辑距离为j
          if (i == 0) {
              dp[i][j] = j;
          //j为0编辑距离为i
          } else if (j == 0) {
              dp[i][j] = i;
          } else {
              //两字母相同
              if (word1[i-1] == word2[j-1]) {
                  dp[i][j] = dp[i-1][j-1];
              } else {
                  dp[i][j] = Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1);
              }
          }
      }
  }

  return dp[word1.length][word2.length];
};
  1. 序列s中有多少子序列与t相同
/**
 * @param {string} s
 * @param {string} t
 * @return {number}
 */
//dp[i][j]代表序列s(0, i)中有多少个序列t(0, j)的子序列
var numDistinct = function(s, t) {
  let dp = [];
  
  for (let i = 0; i <= s.length; i++) {
      dp.push(new Array(t.length + 1).fill(0));
  }
  
  //相当于不断加入s序列中的字符,看子序列个数是否会增加(尾部元素相同才会增加)
  dp[0][0] = 1;
  for (let i = 1; i <= s.length; i++) {
      dp[i][0] = 1;
      for (let j = 1; j <= Math.min(i, t.length); j++) {
          //尾字符相同,以新字符结尾的子序列
          if (s[i - 1] == t[j - 1]) {
              dp[i][j] = dp[i-1][j-1];
          }
          //不以新字符结尾的子序列
          if (i > j) {
              dp[i][j] += dp[i-1][j];
          }
      }
  }

  return dp[s.length][t.length];
};
  1. 最小编辑距离
/**
 * @param {string} word1
 * @param {string} word2
 * @return {number}
 */
var minDistance = function(word1, word2) {
  if ((!word1 && !word2) || word1 == word2) return 0;
  let dis = [];
  for (let i = 0; i <= word1.length; i++) {
      dis.push(new Array(word2.length + 1));
  }
  for (let i = 0; i <= word1.length; i++) {
      for (let j = 0; j <= word2.length; j++) {
          if (i == 0) {
              dis[i][j] = j;
          } else if (j == 0) {
              dis[i][j] = i;
          } else {
              dis[i][j] 
               = Math.min(dis[i][j-1] + 1, dis[i-1][j] + 1, dis[i-1][j-1] + (word1[i-1] == word2[j-1] ? 0 : 1));
          }
      }
  }
  return dis[word1.length][word2.length];
};

最优子结构: dp不是number就是boolean.

  1. 最长回文子串:更短的区间字符串是否回文

  2. 分割等和数组:更少括号组合数量

  3. 字符串字典匹配:前置更短字符串能否匹配

  4. 是否可以找出和为m的子序列:前置是否可以找出和为m或者前置是否可以找出和为m-nums[i]的子序列。

  5. 组成和为amount的最小数目:组成和为amount-nums[i]的最小数目

  6. 最长回文子串 dp[i][j] = true/false. 长度为i,以j开头的字符串是否是回文子串

dp数组有3行,每次新增加一个长度,shift第一行,push([])到第三行

dp[i][j] == s[j] == s[j + i - 1] && dp[i-1][j+1]

  1. 分割等和数组 dp[i][j] = true/false. 从0~i是否可以选出和为j的子序列(不一定连续).

规划的tip是,是否选择nums[i]

dp[i][j] = dp[i-1][j] || dp[i-1][j - nums[i]]

dp数组其实可以缩短为1行,因为dp[i][j]只依赖于dp[i-1][j]或者dp[i-1][j - nums[i]]。为避免提前被重写,第二层从大到小。

dp[j] = dp[j] || (j > nums[i] && dp[j-nums[i]]);

/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canPartition = function(nums) {
    if (nums.length == 0) return true;
    if (nums.length == 1) return false;
    let max = 0, sum = 0;
    for (let i = 0; i < nums.length; i++) {
        sum += nums[i];
        max = Math.max(nums[i], max);
    }

    if (sum % 2 == 1 || max > sum / 2) return false;

    sum = sum / 2;
    let dp = new Array(sum + 1).fill(false);
    dp[0] = true;

    //0~i中选数
    for (let i = 1; i < nums.length; i++) {
        for (let j = sum; j >= 0; j--) {
            dp[j] = dp[j] || (j >= nums[i] && dp[j - nums[i]]);
            if (j == sum && dp[j]) return true;
        }
    }

    return false;
    
};
  1. 给定数字n,生成n对有效括号种类的组合方式

tip: 以最左边左括号及其对应的右括号为标志,之哟啊该对括号内包含的括号数目不同,则在括号对数相同的情况下,一定会生成不同的串。

解法:考虑新加的第n对括号, arr[n] = "(" + arr[p] + ")" + arr[q] p + q = n - 1。

/**
 * @param {number} n
 * @return {string[]}
 */
var generateParenthesis = function(n) {
   let arr = [[""]];
   
   //迭代n次
   for (let i = 1; i <= n; i++) {
       let arri = [];
      
       for (let p = 0; p < i; p++) {
           
           for (let m = 0; m < arr[p].length; m++) {
               
               for (let k = 0; k < arr[i-p-1].length; k++) {
                   arri.push('(' + arr[p][m] + ')' + arr[i-p-1][k]);
               }
           }
       }
       arr.push(arri);
   }

   return arr[n];
    
};
  1. word break. 题目:给定字符串,判断是否能够用字典中的单词拼出来。

tip: 感觉上只能用暴力的题,都可以换成逆思路用dp

/**
 * @param {string} s
 * @param {string[]} wordDict
 * @return {boolean}
 */
var wordBreak = function(s, wordDict) {
    let maxLen = 0;
    for (let i = 0; i < wordDict.length; i++) {
        if (wordDict[i].length > maxLen) {
            maxLen = wordDict[i].length;
        }
    }

    let dp = new Array(s.length);
    for (let i = 0; i < s.length; i++) {
        for (k = 1; k <= maxLen; k++) {
            if ((i == k - 1 && wordDict.indexOf(s.substr(0, k)) > -1 ) || 
            (i >= k && dp[i - k] && wordDict.indexOf(s.substr(i - k + 1, k)) > -1)) {
                dp[i] = true;
            }
        }
        dp[i] = !!dp[i];
    }

    return dp[s.length - 1];
};
  1. 最少硬币

题目:给定硬币coins,求组成amount的最小硬币数量。

解法:最优子结构为更小面值的最少硬币数量。

dp[amount] = Math.min(dp[amount - coins[i]]) + 1;

  1. 最大连续乘积

tip:由于正负特性,维护两个数组

/**
 * @param {number[]} nums
 * @return {number}
 */
 

 
var maxProduct = function(nums) {
   if (nums.length == 1) return nums[0];

    let max = [], min = [], result;
    max[0] = min[0] = nums[0];
    result = max[0];
    for (let i = 1; i < nums.length; i++) {
        if (nums[i] == 0) {
            max[i] = min[0] = 0;
        } else if (nums[i] > 0) {
            if (max[i - 1] > 0) {
                max[i] = nums[i] * max[i-1];
            } else {
                max[i] = nums[i];
            }
            if (min[i - 1] <= 0) {
                min[i] = min[i - 1] * nums[i];
            } else {
                min[i] = nums[i];
            }
        } else {
            if (min[i - 1] <= 0) {
                max[i] = min[i - 1] * nums[i];
            } else {
                max[i] = nums[i];
            } 
            if (max[i - 1] > 0) {
                min[i] = max[i-1] * nums[i];
            } else {
                min[i] = nums[i];
            }
        }

        if (max[i] > result) {
            result = max[i];
        }
    }

    return result;
};
  1. 字符串编解码

最优子结构:dp[i]代表以i结尾的字符串的解码方式数目

题目:11106有几种编解码方式?(比如AAJF)

/**
 * @param {string} s
 * @return {number}
 */
var numDecodings = function(s) {
   if (s[0] == '0') return 0; 
   
   let dp = new Array(s.length);
   dp[0] = 1;
   dp[1] = 1;
   
   for (let i = 1; i < s.length; i++) {
       if (s[i] == '0') {
           if (s[i-1] == '1' || s[i-1] == '2') {
               dp[i + 1] = dp[i - 1];
           } else {
               return 0;
           }
       } else {
           if (s[i - 1] == '1' || (s[i-1] == '2' && s[i] <= '6')) {
               dp[i + 1] = dp[i - 1] + dp[i];
           } else {
               dp[i + 1] = dp[i];
           }
       }
   }
   
   return dp[s.length];
};
  1. 最长回文子序列

最优子结构:

dp[x][y] = Math.max(dp[x][y], dp[x][y - 1], dp[x + 1][y]);

max函数中dp[x][y]的计算是如果两端相等,直接中间+2

  1. 是否是interleaving String

题目:判断s3是否能够为s1和s2分别分割为子串后交错构成。

解法:最优子结构: dp[i][j] = (s1[i] == s3[i+j-1] && dp[i-1][j] || (s2[j] == s3[i+j-1] && dp[i][j-1]);

/**
 * @param {string} s1
 * @param {string} s2
 * @param {string} s3
 * @return {boolean}
 */
var isInterleave = function(s1, s2, s3) {
    if (s1.length + s2.length != s3.length) return false;
    let dp = new Array(s2.length + 1);
    
    dp[0] = true;
    for (let i = 0; i <= s1.length; i++) {
        for (j = 0; j <= s2.length; j++) {
            if (i == 0 && j == 0) continue;
            dp[j] = dp[j] && i > 0 && s1[i - 1] == s3[i + j - 1];
            if (!dp[j]) {
                dp[j] = j > 0 && s2[j - 1] == s3[i + j - 1] && dp[j-1];
            }
        }
    }

    return !!dp[s2.length]
};
  1. 金币凑齐金额amount的方式

最优子结构:dp[i][j] = dp[i-1][j] + dp[i][j-amount];

/**
 * @param {number} amount
 * @param {number[]} coins
 * @return {number}
 */
var change = function(amount, coins) {
    let dp = new Array(amount + 1).fill(0);
    
    dp[0] = 1;
    //dp[j]凑齐到j金币的方式
    for (let i = 0; i < coins.length; i++) {
        for (let j = 0; j <= amount; j++) {
            if (coins[i] <= j){
                dp[j] += dp[j - coins[i]];
            }
        }
    }

    return dp[amount];
};
  1. 障碍矩阵路径个数

题目:给定m * n矩阵,中有障碍,求从top-left到bottom-right的方法

解法:二维dp. 上一步只可能来自左边或者上面。如果有障碍则作废。

/**
 * @param {number[][]} obstacleGrid
 * @return {number}
 */
var uniquePathsWithObstacles = function(obstacleGrid) {
    let m = obstacleGrid.length, n = obstacleGrid[0].length;
    if (m == 1 && n == 1) return (obstacleGrid[0][0] + 1) % 2;
    if (obstacleGrid[m-1][n-1] == 1) return 0;

    let paths = [];
    for (let i = 0; i < m; i++) {
        paths.push(new Array(n));
    }

    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            paths[i][j] = 0;
            if(i == 0 && j == 0) {
                paths[i][j] = 1;
            } else {
                if (i > 0 && obstacleGrid[i-1][j] == 0){
                    paths[i][j] += paths[i-1][j];
                } 
                if (j > 0 && obstacleGrid[i][j-1] == 0) {
                    paths[i][j] += paths[i][j-1];
                }
            }
        }
    }

    return paths[m-1][n-1];

};
  1. 回文串个数

解法:dp[i][j]代表substring(i, j)是否回文。

dp[i][j] = i == 1 || (s[j] == s[j + i - 1] && (i <= 3 || dp[i-2][j+1]))

/**
 * @param {string} s
 * @return {number}
 */
var countSubstrings = function(s) {
    let ans = 0, dp = [];

    for (let i = 0; i <= s.length; i++) {
        dp.push(new Array(s.length));
    }

    for (let i = 1; i <= s.length; i++) {
        for (let j = 0; j < s.length - i + 1; j++) {
            if (i == 1 || (s[j] == s[j + i - 1] && (i <= 3 || dp[i-2][j+1]))) {
                dp[i][j] = true;
                ans++;
            } 
        }
    }

    return ans;
};
  1. 求n个结点的BST组成方式

tip: 左右子树的排列组合,可以用dp来做

/**
 * @param {number} n
 * @return {number}
 */
var numTrees = function(n) {
    if (n <= 2) return n;
    let dp = new Array(n + 1).fill(0);
    dp[1] = 1;
    dp[2] = 2;

    for (let i = 3; i <= n; i++) {
        dp[i] = 2 * dp[i-1];
        let j = 1, k = i - 2;
        while (j < k) {
            dp[i] += 2 * dp[j] * dp[k];
            j++;
            k--;
        }
        if (j == k) {
            dp[i] += dp[j] * dp[j];
        }
    }

    return dp[n];
};

12、判断s3是否为s1和s2交织而成

tip: 首先证明只要判断最后一个字符相同以及子串满足条件,则整体最优子结构满足条件 dp[i]只依赖dp[i-1]时,可以只用一维数组

/**
 * @param {string} s1
 * @param {string} s2
 * @param {string} s3
 * @return {boolean}
 */
var isInterleave = function(s1, s2, s3) {
    if (s1.length + s2.length != s3.length) return false;
    let dp = new Array(s2.length + 1);
    
    dp[0] = true;
    for (let i = 0; i <= s1.length; i++) {
        for (j = 0; j <= s2.length; j++) {
            if (i == 0 && j == 0) continue;
            dp[j] = dp[j] && i > 0 && s1[i - 1] == s3[i + j - 1];
            if (!dp[j]) {
                dp[j] = j > 0 && s2[j - 1] == s3[i + j - 1] && dp[j-1];
            }
        }
    }

    return !!dp[s2.length]
};
  1. 0-1背包问题

题目:有限背包容量能够拿到的最大价值




function KnapsackProblem01(vs, ws, w) {
    let dp = [];
    for (let i = 0; i <= vs.length; i++) {
        dp.push(new Array(w + 1));
    }
    
    dp[0][0] = 0;
    
    //可选物品个数
    for(let i=0; i<=vs.length; i++) {
      //可承受重量
      for(let j=w; j>=0;j--) {
        if (i==0 || j == 0) {
            dp[j] = 0;
        } else if (j > ws[i-1]) {
            dp[j] = Math.max(dp[j-ws[i-1]] + vs[i-1], dp[j]);
        }
      }
    
    }

}
  1. 0-X背包问题

0i的商品总价值相当于,0i选商品w-weight[i], 或者0~i-1选总价值w

function KnapsackProblem(vs, ws, w) {
    let dp = new Array(vs.length + 1);
    for (let i = 0; i <= vs.length; i++) {
        for (let j = 0; j <= w; j++) {
            if (i == 0 || j == 0) {
                dp[j] = 0;
            } else if (ws[i-1] <= w){
                dp[j] = Math.max(dp[j-ws[i-1]] + vs[i-1], dp[j]);
            
            }
        }
    }
}
  1. 整数拆分

数n能拆分出的乘积最大的序列

/**
 * @param {number} n
 * @return {number}
 */
var integerBreak = function(n) {
    let dp = new Array(n + 1);
    dp[0] = 0;
    dp[1] = 1;
    dp[2] = 1;

    for (let i = 3; i <= n; i++) {
        dp[i] = i - 1;
        for (let j = 1; j <= Math.floor(i / 2); j++) {
         //可选2个数或者多个数
           dp[i] = Math.max(dp[i], j * dp[i - j], j * (i - j));
        }
    }

    return dp[n];
};

4.打家劫舍2

首尾房子不能一起劫持。

结果是Math.max(subrob(arr[0, ...n-2]), subrob(arr[1, ...n-1]));

  1. 打家劫舍3

方法1: 考虑2个数组的dp 包含结点i和不包含结点i

var rob = function(root) {
    const f = new Map();
    const g = new Map();

    const dfs = (node) => {
        if (node === null) {
            return;
        }
        dfs(node.left);
        dfs(node.right);
        f.set(node, node.val + (g.get(node.left) || 0) + (g.get(node.right) || 0));
        g.set(node, Math.max(f.get(node.left) || 0, g.get(node.left) || 0) + Math.max(f.get(node.right) || 0, g.get(node.right) || 0));
    }
    
    dfs(root);
    return Math.max(f.get(root) || 0, g.get(root) || 0);
};

方法2: 中规中矩dp

/**
 * Definition for a binary tree node.
 * function TreeNode(val, left, right) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.left = (left===undefined ? null : left)
 *     this.right = (right===undefined ? null : right)
 * }
 */
/**
 * @param {TreeNode} root
 * @return {number}
 */
var rob = function(root) {
   let map = new Array(10 ** 4);

   var postorder = function(node, num) {
       if (!node.left && !node.right) {
           map[num] = node.val;
           return;
       }
       let a = 0, b = 0;
       if (node.left) {
           postorder(node.left, num * 2 + 1);
           a += map[num * 2 + 1];
           if (node.left.left) {
               b += map[2 * (2 * num + 1) + 1];
           }
           if (node.left.right) {
               b += map[2 * (2 * num + 1) + 2];
           }
       }
       if (node.right) {
           postorder(node.right, num * 2 + 2);
           a += map[num * 2 + 2];
           if (node.right.left) {
               b += map[2 * (2 * num + 2) + 1];
           }
           if (node.right.right) {
               b += map[2 * (2 * num + 2) + 2];
           }
       }

       map[num] = Math.max(node.val + b, a);
   }

   postorder(root, 0);
   return map[0];
};
  1. 分割等和子集(是否存在和为target的子序列)
/**
 * @param {number[]} nums
 * @return {boolean}
 */
var canPartition = function(nums) {
    if (nums.length == 0) return true;
    if (nums.length == 1) return false;
    let max = 0, sum = 0;
    for (let i = 0; i < nums.length; i++) {
        sum += nums[i];
        max = Math.max(nums[i], max);
    }

    if (sum % 2 == 1 || max > sum / 2) return false;

    sum = sum / 2;
    let dp = new Array(sum + 1).fill(false);
    dp[0] = true;

    //0~i中选数
    for (let i = 1; i < nums.length; i++) {
        for (let j = sum; j >= 0; j--) {
            dp[j] = dp[j] || (j >= nums[i] && dp[j - nums[i]]);
            if (j == sum && dp[j]) return true;
        }
    }

    return false;
    
};
  1. 求排列和组合的不同

如果求组合数就是外层for循环遍历物品,内层for遍历背包。

如果求排列数就是外层for遍历背包,内层for循环遍历物品。

  1. 硬币问题(求序列中和为target的排列数)
var combinationSum4 = function(nums, target) {
    let dp = new Array(target + 1).fill(0);

    dp[0] = 1;
    for (let i = 1; i <= target; i++) {
        for (let j = 0; j < nums.length; j++) {
            if (i >= nums[j]) {
                dp[i] += dp[i - nums[j]];
            }
        }
    }

    return dp[target];
};
  1. 买卖股票

双dp状态转移

  1. 最长递增子序列
for (int i = 1; i < nums.size(); i++) {
    for (int j = 0; j < i; j++) {
        if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1);
    }
    if (dp[i] > result) result = dp[i];
}