目录:
递推公式:
进阶过程:
存在大量重复计算
def f(n):
if n <= 1: return 1
return f(n - 1) + f(n - 2)
存储每次的计算结果,计算时判断是否被计算过,如果已经计算过就无需再计算
def f(n):
if n <= 1: return 1
if n not in mem:
mem[n] = f(n - 1) + f(n - 2)
return mem[n]
def f(n):
dp = [1] * (n + 1)
for i in range(2, n + 1):
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
当前状态只与保存的上一次状态有关
def f(n):
x, y = 1, 1
for i in range(1, n):
y, x = x + y, y
return y
总结如下图:
总结如下图:
总结如下图:
状态转移图:
- i 为天数
- k 为最多交易次数
- [0,1] 为是否持有股票
总状态数: n * K * 2 种状态
for 0 <= i < n:
for 1 <= k <= K:
for s in {0, 1}:
dp[i][k][s] = max(buy, sell, rest)
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能:
- - 我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
- - 我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能:
- - 我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
- - 我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了
初始状态:
状态转移方程:
复杂度来源
- 状态拥有更多维度(二维、三维、或者更多、甚至需要压缩)
- 状态方程更加复杂
本质:内功、逻辑思维、数学
• 1、2、3
• x1, x2, …, xm 步
• 前后不能走相同的步伐
数组的每个索引作为一个阶梯,第
i
个阶梯对应着一个非负数的体力花费值cost[i]
(索引从0开始)。每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。
您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。
示例 1:
输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。
示例 2:
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。
注意:
cost
的长度将会在[2, 1000]
。- 每一个
cost[i]
将会是一个Integer类型,范围为[0, 999]
。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
vector<int> dp(cost.size());
dp[0] = cost[0];
dp[1] = cost[1];
for (int i = 2; i < cost.size(); i++) {
dp[i] = min(dp[i - 2], dp[i - 1]) + cost[i];
}
return min(dp[cost.size() - 2], dp[cost.size() - 1]);
}
};
• 如果 word1[i] 与 word2[j] 相同,显然 dp[i][j]=dp[i-1][j-1]
• 如果 word1[i] 与 word2[j] 不同,那么 dp[i][j] 可以通过
- 在 dp[i-1][j-1] 的基础上做 replace 操作达到目的
- 在 dp[i-1][j] 的基础上做 insert 操作达到目的
- 在 dp[i][j-1] 的基础上做 delete 操作达到目的
取三者最小情况即可
实现函数 ToLowerCase(),该函数接收一个字符串参数 str,并将该字符串中的大写字母转换成小写字母,之后返回新的字符串。
示例 1:
输入: "Hello"
输出: "hello"
示例 2:
输入: "here"
输出: "here"
/*
利用ASCII值大小写之间相差32的性质,遇到大写的字母,加上32就是相应的小写字母。
*/
class Solution {
public:
string toLowerCase(string str) {
int len = str.size();
for(int i = 0; i < len; ++ i){
if(str[i] <= 'Z' && str[i] >= 'A'){
str[i] = str[i] + 32;
}
}
return str;
}
};
给定一个仅包含大小写字母和空格
' '
的字符串s
,返回其最后一个单词的长度。如果字符串从左向右滚动显示,那么最后一个单词就是最后出现的单词。如果不存在最后一个单词,请返回 0 。
**说明:**一个单词是指仅由字母组成、不包含任何空格字符的 最大子字符串。
示例:
输入: "Hello World"
输出: 5
class Solution {
public:
int lengthOfLastWord(string s) {
int res = 0;
for(int i = s.length()-1; i >=0 ; i--)
{
if(res != 0 && s[i] == ' ') break;
if((s[i] >= 'a' && s[i] <= 'z') || (s[i] >= 'A' && s[i] <= 'Z'))
res++;
}
return res;
}
};
给定字符串
J
代表石头中宝石的类型,和字符串S
代表你拥有的石头。S
中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。
J
中的字母不重复,J
和S
中的所有字符都是字母。字母区分大小写,因此"a"
和"A"
是不同类型的石头。示例 1:
输入: J = "aA", S = "aAAbbbb"
输出: 3
示例 2:
输入: J = "z", S = "ZZ"
输出: 0
注意:
S
和J
最多含有50个字母。J
中的字符不重复。
class Solution {
public:
int numJewelsInStones(string J, string S)
{
unordered_map<char, bool> mp;
int sum = 0;
for (auto j : J)
mp[j] = 1;
for (auto s : S)
if (mp[s]) sum++;
return sum;
}
};
编写一个函数来查找字符串数组中的最长公共前缀。
如果不存在公共前缀,返回空字符串
""
。示例 1:
输入: ["flower","flow","flight"]
输出: "fl"
示例 2:
输入: ["dog","racecar","car"]
输出: ""
解释: 输入不存在公共前缀。
说明:
所有输入只包含小写字母
a-z
。
class Solution {
public:
string longestCommonPrefix(vector<string>& strs) {
if (!strs.size()) {
return "";
}
string prefix = strs[0];
int count = strs.size();
for (int i = 1; i < count; ++i) {
prefix = longestCommonPrefix(prefix, strs[i]);
if (!prefix.size()) {
break;
}
}
return prefix;
}
string longestCommonPrefix(const string& str1, const string& str2) {
int length = min(str1.size(), str2.size());
int index = 0;
while (index < length && str1[index] == str2[index]) {
++index;
}
return str1.substr(0, index);
}
};
- 暴力 O(n^3)
- 中间向两边扩张法 O(n^2)
- 动态规划
首先定义 P(i, j):
true s[i, j] 是回文串
P(i, j) =
false s[i, j] 不是a回文串
接下来
P(i, j) = (P(i+1, j-1) && S[i] == S[j])
- 暴力递归
- 动态规划
dp [i] [j] 代表 T 前 i 字符串可以由 s 前 j 字符串组成最多个数。
所以动态方程:
- 暴力法(brute force) - O(mn)
- Rabin-Karp 算法
- KMP 算法
在朴素算法中,我们需要挨个比较所有字符,才知道目标字符串中是否包含子串。那么, 是否有别的方法可以用来判断目标字符串是否包含子串呢?
答案是肯定的,确实存在一种更快的方法。为了避免挨个字符对目标字符串和子串进行比较, 我们可以尝试一次性判断两者是否相等。因此,我们要一个好的哈希函数(hash function)。 通过哈希函数,我们可以算出子串的哈希值,然后将它和目标字符串中的子串的哈希值进行比较。 这个新方法在速度上比暴力法有显著提升。
Rabin-Karp 算法的思想:
- 假设子串的长度为 M (pat),目标字符串的长度为 N (txt)
- 计算子串的 hash 值 hash_pat
- 计算目标字符串txt中每个长度为 M 的子串的 hash 值(共需要计算 N-M+1次)
- 比较 hash 值:如果 hash 值不同,字符串必然不匹配; 如果 hash 值相同,
还需要使用朴素算法再次判断
public final static int D = 256;
public final static int Q = 9997;
static int RabinKarpSerach(String txt, String pat) {
int M = pat.length();
int N = txt.length();
int i, j;
int patHash = 0, txtHash = 0;
for (i = 0; i < M; i++) {
patHash = (D * patHash + pat.charAt(i)) % Q;
txtHash = (D * txtHash + txt.charAt(i)) % Q;
}
int highestPow = 1; // pow(256, M-1)
for (i = 0; i < M - 1; i++)
highestPow = (highestPow * D) % Q;
for (i = 0; i <= N - M; i++) { // 枚举起点
if (patHash == txtHash) {
for (j = 0; j < M; j++) {
if (txt.charAt(i + j) != pat.charAt(j))
break;
}
if (j == M)
return i;
}
if (i < N - M) {
txtHash = (D * (txtHash - txt.charAt(i) * highestPow) + txt.charAt(i + M)) % Q;
if (txtHash < 0)
txtHash += Q;
}
}
return -1;
}
KMP算法(Knuth-Morris-Pratt)的思想就是,当子串与目标字符串不匹配时,其实你已经知道了前面已经匹配成功那 一部分的字符(包括子串与目标字符串)。
以阮一峰的文章为例,当空格与 D 不匹配时,你其实知道前面六个字符是“ABCDAB”。
KMP 算法的想法是,设法利用这个已知信息,不要把“搜索位置” 移回已经比较过的位置,继续把它向后移,这样就提高了效率。