LeetCode探索(动态规划)
动态规划
动态规划的实质其实就是穷举,通过DP表的形式折叠一些重复的计算。dp其实没有什么好的模板,重点在于写出状态转移方程。但是即使写出了状态转移方程还是有很多细节需要注意。
总的来说DP的模板可以归结成如下:
//初始化
dp[0][0] = base;
//进行状态转移
for(状态1 in 状态1所有的取值)
for(状态2 in 状态2所有的取值)
dp[状态1][状态2] = 状态转移方程
不过这也太笼统了吧,还是来看几道经典的DP吧。
最长回文子串
题目:https://leetcode-cn.com/problems/longest-palindromic-substring/
题目大意:给定一个字符串 s,找到 s 中最长的回文子串。你可以假设 s 的最大长度为 1000。
分析:这道题是DP的经典问题了。
其实判断回文子串就是一个自己和自己比较的过程,我们初始化一个二维数组,比较每个位置的上的字符是否相等,如果相等的话那么向中间继续比较。
写成状态转移方程就是dp[i][j] = A[i] == A[j]? dp[i-1][j+1]:false
当然对于一些特殊情况还是需要进行考虑,对于回文子串来说一共有两种情况。第一种就是奇数长度,在这种情况下经过不断DP,最后会查找到i = j
的位置上,所以我们需要把i = j
位置的上的boolean初始化为true。
第二种就是偶数长度,这种情况下最后会归结到一个空的字符串上,对于这种情况,我们可以像以往一样采用dp[i][j]代表A[i-1]和A[j-1]的字符串进行比较,也可以直接进行判断,如果j-i<3&&A[i] == A[j]
那么dp[i][j] = true
。
class Solution {
public String longestPalindrome(String s) {
if(s == null || s.length() == 0)
return s;
char[] chars = s.toCharArray();
boolean[][] dp = new boolean[s.length()][s.length()];
//如果不设置成1的话
//会导致如果字符串没有回文子串会返回""
//但是实际上应该返回第一个字符
//因为一个字符就是一个回文串
int maxLen = 1;
//保存最长回文子串的起始位置
int begin = 0;
for(int i=0;i<s.length();i++)
dp[i][i] = true;
for(int i=1;i<s.length();i++){
//由于数组是对称的,所以计算一半就行
for(int j=0;j<i;j++){
if(chars[i] == chars[j]){
if(i-j<3){
dp[i][j] = true;
}else{
//向中间比较
dp[i][j] = dp[i-1][j+1];
}
}else{
dp[i][j] = false;
}
//是回文子串才能记录最大值
if(dp[i][j] && i-j+1>maxLen){
maxLen = i-j+1;
begin = j;
}
}
}
return s.substring(begin,begin+maxLen);
}
}
编辑距离
题目:https://leetcode-cn.com/problems/edit-distance/
题目大意:给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:插入一个字符、删除一个字符、替换一个字符。
分析:这道题看起来非常复杂,但是看了解答之后会有一种恍然大悟的感觉。
对于一个字符rose
有三种操作,如果要转化为ros
采用删除操作,操作数+1.
那么对于rorse
转化为rose
也是采用删除操作,操作数+1,。
通过这么倒推之后我们发现,前面的操作依赖于后面的操作,那么对于这种题目就可以采用动态规划来做。
我们创建一个数组int[][] dp
,对于dp[i][j]
代表的是字符串word1[:i-1]
和word2[:j-1]
。
如果word1[i-1] == word2[j-2]
,那么这一步不需要进行任何操作。
如果不相等,那么一共有三种操作。
例如,对于目标字符串a,假设目前字符串为ab,那么采用删除操作,然后比较的
是a和a,所以就是比较w[i-1]和w[j]
也就是dp[i-1][j]
。
假设目标字符串为ab,目标字符串为abc,那么采用添加操作,然后就是比较ab和ab,所以就是比较w1[i]
和w[j-1]
也就是dp[i][j-1]
。
假设目标字符串为abc,当前字符串为abd,那么采用替换操作,也就是比较w[i-1]
和w[j-1]
,也就是dp[i-1][j-1]
。
所以得出结论,替换依赖于dp[i-1][j-1]
,添加依赖于dp[i][j-1]
,删除依赖于dp[i-1][j]
。
由于需要求的是最小距离,所以我们需要在这三个数中取最小值,所以状态转移方程为dp[i][j] = w1[i]==w2[j]?dp[i-1][j-1]:Min(dp[i-1][j],dp[i][j-1],dp[i-1]][j-1])+1
class Solution {
public int minDistance(String word1, String word2) {
if(word1 == null && word2 == null)
return 0;
if(word1.length() == 0 && word2.length() == 0)
return 0;
if(word1 == null)
return word2.length();
if(word2 == null)
return word1.length();
char[] chars1 = word1.toCharArray();
char[] chars2 = word2.toCharArray();
int[][] dp = new int[word1.length()+1][word2.length()+1];
for(int i=0;i<word1.length()+1;i++){
dp[i][0] = i;
}
for(int i=0;i<word2.length()+1;i++){
dp[0][i] = i;
}
for(int i=1;i<word1.length()+1;i++){
for(int j=1;j<word2.length()+1;j++){
if(chars1[i-1] == chars2[j-1]){
dp[i][j] = dp[i-1][j-1];
}else{
//记得操作+1
dp[i][j] = Math.min(Math.min(dp[i-1][j-1],dp[i-1][j]),dp[i][j-1])+1;
}
}
}
return dp[word1.length()][word2.length()];
}
}
最长重复子数组
题目:https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/
题目大意:给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。
分析:这道题其实就是经典的最长公共子串的变形。dp[i][j] = nums[i] == nums[j]?dp[i-1][j-1]+1:0
class Solution {
public int findLength(int[] A, int[] B) {
if(A == null || B == null)
return 0;
int max = 0;
int[][] dp = new int[A.length+1][B.length+1];
for(int i=1;i<A.length+1;i++){
for(int j=1;j<B.length+1;j++){
if(A[i-1] == B[j-1])
dp[i][j] = dp[i-1][j-1]+1;
else
dp[i][j] = 0;
if(dp[i][j] != 0)
max = Math.max(max,dp[i][j]);
}
}
return max;
}
}
最长公共子序列
题目:https://leetcode-cn.com/problems/maximum-length-of-repeated-subarray/submissions/
题目大意:给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。
分析:其实这道题和上面那道差不多,但是区别在于这道题的序列是可以不连续的,但是顺序要相同。
那么对于字符abcde
和ace
来说。
假设我们以abcde
为主,逐个比较text2ace
,当text2为a
的时候此时公共子序列为1,为ac
的时候,子序列为1,为ace
的时候子序列为2。
对于ace
也是一样,所以可以得出状态转移方程:dp[i][j] = text1[i-1] == text[j-1]?dp[i-1][j-1]+1:Max(dp[i-1][j],dp[i][j-1])
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
if(text1 == null || text2 == null || text1.length() == 0 || text2.length() == 0)
return 0;
int[][] dp = new int[text1.length()+1][text2.length()+1];
int lengt1 = text1.length(),length2 = text2.length();
for(int i=1;i<lengt1+1;i++){
for(int j=1;j<length2+1;j++){
if(text1.charAt(i-1) == text2.charAt(j-1))
dp[i][j] = dp[i-1][j-1]+1;
else
dp[i][j] = Math.max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[lengt1][length2];
}
}
零钱兑换
题目:https://leetcode-cn.com/problems/coin-change/
题目大意:给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
分析:这道题其实算是完全背包问题,比起上面的题目代码上稍微简单一些。
状态转移方程是:dp[i] = i>coin?Min(dp[i-coin]+1,dp[i]):dp[i] coin:coins
class Solution {
public int coinChange(int[] coins, int amount) {
int[] dp = new int[amount+1];
Arrays.fill(dp,amount+1);
dp[0]=0;
for(int i=1;i<amount+1;i++){
for(int coin:coins){
if(i-coin>=0 && dp[i-coin]!=amount+1){
dp[i] = Math.min(dp[i],dp[i-coin]+1);
}
}
}
return dp[amount] == amount+1?-1:dp[amount];
}
}
上一篇: LeetCode——每日一题