最优子结构: f(n-1)和f(n-2)
边界: f(1)=1,f(2)=2
状态转移函数: f(n)=f(n-1)+f(n-2)
问题细化为最后一次爬楼梯是爬1个台阶还是2个台阶。
时间复杂度o(n),空间复杂度o(n)
class Solution {
public int climbStairs(int n) {
int dp[] = new int[n + 1];
dp[1] = 1;
if (n == 1) return dp[1];
dp[2] = 2;
if (n == 2) return dp[2];
for (int i = 3; i < n + 1; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}
根据最优子结构和状态转移函数,对每个子状态,只需要前两个阶段的结果,因此其实不需要dp数组。
时间复杂度o(n),空间复杂度o(1)
class Solution {
public int climbStairs(int n) {
if (n == 1) return 1;
if (n == 2) return 2;
int p = 1;
int q = 2;
int r = 0;
for (int i = 3; i < n + 1; i++) {
r = p + q;
p = q;
q = r;
}
return r;
}
}
给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。如果不存在公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
输入:text1 = "abcde", text2 = "ace"
输出:3
解释:最长公共子序列是 "ace" ,它的长度为 3 。
使用dp[i][j]表示text1前i个字符和text2前j个字符最长公共子序列的长度
边界: dp[0][j]=0, dp[i][0]=0
状态转移函数: dp[i][j]=max(dp[i-1][j], dp[i][j-1]) 或 dp[i][i]=dp[i-1][j-1]+1
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] dp = new int[m + 1][n + 1];
if (m == 0 || n == 0) return 0;
for (int i = 1; i < m + 1; i++) {
for (int j = 1; j < n + 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[m][n];
}
}
给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
输入: "sea", "eat"
输出: 2
解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea"
设word1和word2的长度分别为m和n,最长子序列的长度为x,则答案为m-x+n-x
时间复杂度o(mn), 空间复杂度o(mn)
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m + 1][n + 1];
if (m == 0 || n == 0) return 0;
for (int i = 1; i < m + 1; i++) {
for (int j = 1; j < n + 1; j++) {
if (word1.charAt(i - 1) == word2.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 m + n - 2 * dp[m][n];
}
}
直接动态规划
时间复杂度o(mn), 空间复杂度o(mn)
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int dp[][] = new int[m + 1][n + 1];
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
if (i == 0) {
dp[i][j] = j;
}
else if (j == 0) {
dp[i][j] = i;
}
else if (word1.charAt(i - 1) == word2.charAt(j - 1)){
dp[i][j] = dp[i - 1][j - 1];
}
else {
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1;
}
}
}
return dp[m][n];
}
}
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
边界: dp[1] = nums[0], dp[2] = max(nums[0], nums[1])
状态转移函数: dp[i] = max(dp[i] + dp[i - 2], dp[i - 1])
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
int[] dp = new int[nums.length + 1];
dp[1] = nums[0];
dp[2] = Math.max(nums[0], nums[1]);
for (int i = 3; i < nums.length + 1; i++) {
dp[i] = Math.max(dp[i - 1], nums[i-1] + dp[i - 2]);
}
return dp[nums.length];
}
}
class Solution {
public int rob(int[] nums) {
if (nums.length == 1) return nums[0];
int pre1 = nums[0];
int pre2 = Math.max(nums[0], nums[1]);
int result = pre2;
for (int i = 2; i < nums.length; i++) {
result = Math.max(pre2, nums[i] + pre1);
pre1 = pre2;
pre2 = result;
}
return result;
}
}
你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。
给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。
输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
可分为两种 情况:
取这两种情况的最大值即为题解。
class Solution {
public int rob(int[] nums) {
if (nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
if (nums.length == 2) return Math.max(nums[0], nums[1]);
return Math.max(optimizedHelp(nums, 0, nums.length - 1), optimizedHelp(nums, 1, nums.length));
}
//动态规划传统解法
public int robHelp(int[] nums, int start, int end) {
int[] cache = new int[nums.length];
cache[start] = nums[start];
cache[start + 1] = Math.max(nums[start], nums[start + 1]);
for (int i = start + 2; i < end; i++) {
cache[i] = Math.max(nums[i] + cache[i - 2], cache[i - 1]);
}
return cache[end - 1];
}
//动态规划优化,不用数组
public int optimizedHelp(int[] nums, int start, int end) {
int pre1 = nums[start];
int pre2 = Math.max(nums[start], nums[start + 1]);
int res = pre2;
for (int i = start + 2; i < end; i++) {
res = Math.max(pre1 + nums[i], pre2);
pre1 = pre2;
pre2 = res;
}
return res;
}
}
给定两个字符串s1, s2,找到使两个字符串相等所需删除字符的ASCII值的最小和。
输入: s1 = "sea", s2 = "eat"
输出: 231
解释: 在 "sea" 中删除 "s" 并将 "s" 的值(115)加入总和。
在 "eat" 中删除 "t" 并将 116 加入总和。
结束时,两个字符串相等,115 + 116 = 231 就是符合条件的最小和。
class Solution {
public int minimumDeleteSum(String s1, String s2) {
int m = s1.length();
int n = s2.length();
int dp[][] = new int[m + 1][n + 1];
for (int i = 0; i < m + 1; i++) {
for (int j = 0; j < n + 1; j++) {
if (i == 0) {
int temp_len = 0;
for (int k = 0; k < j; k++) {
temp_len = temp_len + s2.codePointAt(k);
}
dp[i][j] = temp_len;
} else if (j == 0) {
int temp_len = 0;
for (int k = 0; k < i; k++) {
temp_len = temp_len + s1.codePointAt(k);
}
dp[i][j] = temp_len;
} else if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1];
} else {
dp[i][j] = Math.min(dp[i - 1][j] + s1.codePointAt(i - 1), dp[i][j - 1] + s2.codePointAt(j - 1));
}
}
}
return dp[m][n];
}
}
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')
class Solution {
public int minDistance(String word1, String word2) {
int m = word1.length();
int n = word2.length();
int[][] dp = new int[m+1][n+1];
for (int i = 0; i < m + 1; i++) {
for (int j = 0; j < n + 1; j++) {
if (i == 0) {
dp[i][j] = j;
}
else if (j == 0) {
dp[i][j] = i;
}
else {
char c1 = word1.charAt(i - 1);
char c2 = word2.charAt(j - 1);
if (c1 == c2) {
dp[i][j] = Math.min(Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1), dp[i-1][j-1]);
}
else {
dp[i][j] = Math.min(Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1), dp[i-1][j-1] + 1);
}
}
}
}
return dp[m][n];
}
}
给你一个字符串 s ,每一次操作你都可以在字符串的任意位置插入任意字符。
请你返回让 s 成为回文串的 最少操作次数 。
「回文串」是正读和反读都相同的字符串。
输入:s = "zzazz"
输出:0
解释:字符串 "zzazz" 已经是回文串了,所以不需要做任何插入操作
输入:s = "mbadm"
输出:2
解释:字符串可变为 "mbdadbm" 或者 "mdbabdm"
输入:s = "no"
输出:1
设原始字符串为s,回文串为s’。存在两种情况:
s’的字符数为奇数时,中心为一个字符,且这个字符一定是s中的,否则相当于多了一步插入操作。
s’的字符数为偶数时,中心为相等的两个字符,且这两个字符一定是s中的两个字符,否则相当于多了一步插入操作;且它们在s中连续,因为只能执行插入操作,不能执行删除操作。
这样,我们就可以找到在s中找到s’的中心c\cc,然后计算其左右两边的两个字符串的最长公共子序列r来确定插入值的个数。
但这样做的时间复杂度为o(n^3),会超时。
进一步考虑:上述过程实际得到的是 inv®+c\cc+r 的子序列,且都是原始序列中的,因此,上述过程相当于找到了s的最长回文子序列,即s和inv(s)的最长公共子序列,然后再插入剩余字符数量的字符即可将s变为s’。
class Solution {
public int minInsertions(String s) {
int m = s.length();
String sr = new StringBuffer(s).reverse().toString();
int[][] dp = new int[m + 1][m + 1];
for (int i = 1; i < m + 1; i++) {
for (int j = 1; j < m + 1; j++) {
char a = s.charAt(i - 1);
char b = sr.charAt(j - 1);
if (a == b) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return m - dp[m][m];
}
}