训练题:最长公共子串
string LCS( string str1, string str2 )
{
if( str1 == "" || str2 == "" ) { return ""; }
const auto size1 = str1.size();
const auto size2 = str2.size();
map<char, vector<int>> dataMap;
for( int idx1 = 0; idx1 < size1; ++idx1 ) { dataMap[str1[idx1]].push_back( idx1 ); }
int maxLen = 0, startIdx = -1;
for( int idx2 = 0, tmpCount = 0; idx2 < size2; ++idx2 )
{
if( dataMap.count( str2[idx2] ) )
{
for( auto &idx1 : dataMap[str2[idx2]] )
{
tmpCount = 1;
for( ; idx1 + tmpCount < size1 && idx2 + tmpCount < size2 && str1[idx1 + tmpCount] == str2[idx2 + tmpCount]; ++tmpCount );
if( tmpCount > maxLen )
{
startIdx = idx1;
maxLen = tmpCount;
}
}
}
}
if( !maxLen ) { return ""; }
return str1.substr( startIdx, maxLen );
}
训练题:最长回文子串
方法一:动态规划
- 空间复杂度:O(n2)
- 时间复杂度:O(n2)
dp[i][j]指子串[i~j]是否为回文串。有如下动态公式:
string longestPalindrome( string s )
{
if( s.size() < 2 ) { return s; }
const short size = s.size();
short maxLen = -1;
short maxBeginIdx = -1;
vector<vector<bool>> dataMap( size, vector<bool>( size ) );
// 根据动态规划公式,我们应该先找出端的回文串,再此基础上才能确定更长的串
// 因此遍历方式应该是:
// 1、遍历字符串中的所有子串
// 2、先遍历短的子串
for( short interval = 0; interval < size; ++interval ) // 遍历首尾间隔为interval的子串
{
// left,right为子串的首尾索引
for( short left = 0, right = -1; left < size; ++left )
{
right = left + interval;
if( right > size - 1 ) { break; }
if( left == right ) { dataMap[left][right] = true; } // 单个字符肯定是回文
else if( right - left == 1 ) // 2个字符串只有相同才是回文
{
dataMap[left][right] = s[left] == s[right];
}
else // 动态规划公式
{
dataMap[left][right] = dataMap[left + 1][right - 1] && s[left] == s[right];
}
// 保存当前最长的回文串
if( dataMap[left][right] && maxLen < right - left + 1 )
{
maxLen = right - left + 1;
maxBeginIdx = left;
}
}
}
return s.substr( maxBeginIdx, maxLen );
}
方法二:中心扩展算法
string longestPalindrome( string s )
{
if( s.size() < 2 ) { return s; }
const short size = s.size();
short maxLen = 0;
short maxLeftIdx = 0;
// 遍历找出以idx或者idx,idx+1为中心的最长回文长度。
for( short idx = 0, len1 = -1, len2 = -1, len3 = -1; idx < size; ++idx )
{
len3 = std::max( expand( s, size, idx, idx ), expand( s, size, idx, idx + 1 ) );
if( len3 > maxLen )
{
maxLen = len3;
maxLeftIdx = idx - ( ( len3 - 1 ) >> 1 );
}
}
return s.substr( maxLeftIdx, maxLen );
}
// left~right之间是回文,向两边扩展,找出最长的回文。
short expand( const string &s, const short &size, short left, short right )
{
for( ; left >= 0 && right < size && s[left] == s[right]; --left, ++right );
return right - left - 1;
}
训练题:N个骰子点数
题目:https://leetcode-cn.com/problems/nge-tou-zi-de-dian-shu-lcof/
- 1、定义dp[i]或者dp[i][j]
- 2、找出转移方程
- 第i个骰子投出点数为1的次数 =
- 第i个骰子投出点数为2的次数 =
- ……
- 第i个骰子投出点数为6的次数 =
这六种情况之和就是dp[i][j]的所有可能情况。
- 3、边界条件
1、递归实现
- 时间复杂度:O(6n),实现简单,但是非常低效,因为有大量重复计算。
- 空间复杂度:O(n2)
vector<double> dicesProbability(int n) {
vector<double> ret;
// 投n个骰子的全排列。
double totalCount = pow(6.0, n);
// 投n个骰子的总点数范围
for(int i = 1*n; i <= 6*n; ++i) {
ret.push_back( fuck(n, i) / totalCount );
}
return ret;
}
int fuck( int i, int j ) {
if(i == 1){
if(j >= 1 && j <= 6) return 1;
return 0;
}
return fuck(i-1, j-1) + fuck(i-1, j-2) + fuck(i-1, j-3) +
fuck(i-1, j-4) + fuck(i-1, j-5) + fuck(i-1, j-6);
}
2、优化计算
- 改成迭代
- 保存状态记录 ```cpp
vector
if(n < 1 || n > 11) return {0};
vector<double> ret;
double totalCount = pow(6, n);
// dp[i][j]
int dp[12][67] = {0};
for(char i = 1; i <= 6; ++i) dp[1][i] = 1;
// i,j就是指dp[i][j]的i、j
for(char i = 2; i <= n; ++i) {
for(char j = i; j <= 6*i; ++j) {
for(char cur = 1; cur <=6; ++cur){
if(j > cur) dp[i][j] += dp[i-1][j-cur];
}
}
}
ret.reserve(5*n+1);
for(char i = n; i <= 6*n; ++i) ret.push_back(dp[n][i] / totalCount);
return ret;
}
<a name="QMqj6"></a>
## 3、优化空间
```cpp
vector<double> dicesProbability( int n )
{
if( n < 1 || n > 11 ) return{ 0 };
vector<double> ret;
double totalCount = pow( 6, n );
// 如果当前循环是第n轮,则dp保存的是n-1轮的结果,这是可以通过dp的数据更新自己为n轮的数据。
int dp[67] = { 0 };
for( char i = 1; i <= 6; ++i ) { dp[i] = 1; }
for( char i = 2; i <= n; ++i )
{
// 从后往前更新,避免把还需要用到的数据覆盖。
for( char j = 6 * i; j >= i; --j )
{
// 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
// i=1 1 1 1 1 1 1
// i=2 1 2 3 4 5 6 5 4 3 2 1
// i=3 1 3 6 10 15 21 25 27 27 25 21 15 10 6 3 1
dp[j] = 0;
for(char cur = 1; cur <= 6; ++cur){
if(j - cur + 1 >= i) dp[j] += dp[j - cur];
}
}
}
ret.reserve( 5 * n + 1 );
for( char i = n; i <= 6 * n; ++i ) { ret.push_back( dp[i] / totalCount ); }
return ret;
}
训练题:股票最大利润
int maxProfit(vector<int>& prices) {
if(prices.size() < 2) return 0;
int profitMax = 0;
const auto days = prices.size();
for(int dayIdx = 1, lastProfitMax = 0; dayIdx < days; ++dayIdx)
{
lastProfitMax = max(prices[dayIdx] - prices[dayIdx - 1] + lastProfitMax, 0);
profitMax = max(profitMax, lastProfitMax);
}
return profitMax;
}
训练题:丑数
其中p2、p3、p5满足以下条件:
int nthUglyNumber( int n )
{
vector<int> nums( n + 1, 1 );
int p2 = 1, p3 = 1, p5 = 1;
for( int i = 2; i <= n; ++i )
{
int a1 = nums[p2] * 2;
int b1 = nums[p3] * 3;
int c1 = nums[p5] * 5;
nums[i] = std::min( std::min( a1, b1 ), c1 );
// 求出第i个丑数之后,要及时更新p2、p3、p5
nums[i] == a1 && ++p2;
nums[i] == b1 && ++p3;
nums[i] == c1 && ++p5;
}
return nums[n];
}
训练题:最多礼物(最优路径)
求最值,首先考虑动态规划。
动态规划的核心思想:
- 递归化(迭代化)
- dp[i][j],到坐标(i, j)的最大礼物值,这个提炼是否正确,就看下面这一条是否可行。
- 前一步的结果作为下一步的参考
- dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + val[i][j],坐标(i,j)的最大礼物值取决于左边和上方更大值者。
- 注意考虑i-1和j-1的边界情况。
- 选择遍历方式
- 使得可以先计算前面的dp,再计算后面的dp。
- 这一题,情况简单,从左往右,从上往下遍历就满足这个要求。 ```cpp
int maxValue(vector
if(grid.empty() || grid[0].empty()) return -1;
const auto m = grid.size();
const auto n = grid[0].size();
// 这两个循环先求出第一行/列的dp值,免得在下面的双重循环中多次判断
for(int col = 1; col < n; ++col) grid[0][col] += grid[0][col-1];
for(int row = 1; row < m; ++row) grid[row][0] += grid[row-1][0];
for(int row = 1; row < m; ++row){
for(int col = 1; col < n; ++col){
grid[row][col] += max(grid[row-1][col], grid[row][col-1]);
}
}
return grid[m-1][n-1];
}
<a name="yBvv3"></a>
# 训练题:跳台阶
题目:[http://t.cn/A6Vz29oQ](http://t.cn/A6Vz29oQ)
<a name="vS67K"></a>
## 方法一:最简单递归
- 时间复杂度:O(n2)
- 空间复杂度:递归栈空间。
- 极其简单,效率极其低下
```cpp
int jumpFloor( int number )
{
if( number < 3 ) { return number; }
return jumpFloor( number - 1 ) + jumpFloor( number - 2 );
}
方法二:记忆存储
保存递归过的结果,减少递归次数。
- 时间复杂度:O(n)
- 空间复杂度:O(n) + 递归栈空间。 ```cpp
int jumpFloor( int number ) { if( number < 4 ) { return number; }
// 45个数初始化为-1,超过45,int会溢出。
// index:对应number,value对应跳法数
vector<int> shit( 45, -1 );
return _jumpFloor( number - 1, shit ) + _jumpFloor( number - 2, shit );
}
int _jumpFloor( int number, vector
<a name="Cwdzt"></a>
## 方法三:自底向上
斐波那契数列从前往后叠加,直到目的位置。
- 时间复杂度:O(n)
- 空间复杂度:O(1)
```cpp
int jumpFloor( int number )
{
if( number < 4 ) { return std::max( number, 0 ); }
for( int a = 1, b = 2, tmp, step = 3;; step++ )
{
tmp = b;
b = a + b;
a = tmp;
if( step == number ) { return b; }
}
}
训练题:斐波那契数列求值
题目:http://t.cn/A6VziaJb
和上情形本质完全一样,这里就采用第三种方法:
int Fibonacci(int n) {
if(n < 2) return n;
int b = 1;
for(int a = 1, tmp; n > 2; n--){
tmp = b;
b = tmp + a;
a = tmp;
}
return b;
}
训练题:最大累加和
训练题:数字组合
训练题:最长递增子序列(LIS)
题目:https://leetcode-cn.com/problems/longest-increasing-subsequence/
动态规划(递归版)
int lengthOfLIS(vector<int>& nums) {
if(nums.size() < 2) return nums.size();
const auto size = nums.size();
int ret = 0;
// 定义dp:dp[i]为以nums[i]结尾的最长子序列长度
vector<int> dp(size, -1);
for(int idx = 0; idx < size; ++idx)
// ret = max(dp[i]),0 <= i <= idx。
ret = max(ret, maxLenOfLIS(nums, dp, idx));
return ret;
}
// 以idx索引处字符结尾的最长子序列长度,dp是为了保存迭代状态
// dp[i] = max(dp[j]+1),其中0 <= j < i,且dp[j] < dp[i](这样才能做到以nums[i]结尾)
int maxLenOfLIS(vector<int>& nums, vector<int>& dp, int idx){
if(idx == 0) return 1;
if(dp[idx] != -1) return dp[idx];
dp[idx] = 1;
for(int tmp = 0; tmp < idx; ++tmp){
// dp[idx] = max(dp[i]+1),其中0 <= i < idx,且nums[i] < nums[idx]。
// 因为必须要以nums[idx]结尾,则dp[i]的结尾字符值必须小于nums[idx]
if(nums[tmp] < nums[idx]) dp[idx] = max(dp[idx], maxLenOfLIS(nums, dp, tmp) + 1);
}
return dp[idx];
}
动态规划(迭代版)
// 迭代版相对于递归版,少了递归栈空间。
// 低索引的计算数据用于高索引的计算。
int lengthOfLIS(vector<int>& nums) {
if(nums.size() < 2) return nums.size();
const auto size = nums.size();
int ret = 1;
vector<int> dp(size, 1); // key:idx, value:maxLen
for(int idx = 1, tmp; idx < size; ++idx)
{
for(tmp = 0; tmp < idx; ++tmp){
if(nums[tmp] < nums[idx]){
dp[idx] = max(dp[idx], dp[tmp] + 1);
}
}
ret = max(ret, dp[idx]);
}
return ret;
}
动态规划(贪心)
考虑一
动态规划的本质就是穷举,将所有子问题解决进而得到问题的最优解。
贪心算法的思想是每一步取子问题的最优解,迭代进而得到问题的最优解。
上面动态规划中的转移方程并不是最优解:
转移方程:。
我们在找这个max值的时候是线性遍历的方式寻找(复杂度n),即从头到尾遍历。key值i为字符索引与序列长度大小没有任何关系。假设我们以序列长度作为key值,那最大key就是最大长度。
考虑二
我们要使上升子序列尽可能的长,则我们需要让序列上升得尽可能慢,因此我们希望每次在上升子序列最后加上的那个数尽可能的小。
int lengthOfLIS(vector<int>& nums) {
if(nums.size() < 2) return nums.size();
const auto size = nums.size();
vector<int> dp(size+1); // 长度为idx的子序列的最小结尾字符。
dp[1] = nums[0]; // 起始状态,长度为1的子序列的结尾字符是首字符。
int tmpLenIdx = 1; // tmpLenIdx表示minCharOfLen的有效长度,即
for(int idx = 1; idx < size; ++idx)
{
// 1 4 5 2 3 4 5 6
// ^
// curr dp: 1 4
// next dp: 1 4 5
if(nums[idx] > dp[tmpLenIdx]) {
minCharOfLen[++tmpLenIdx] = nums[idx];
}
else{
// 1 4 5 2 3 4 5 6
// ^
// curr dp: 1 4 5
// next dp: 1 2 5
// 二分叉查找法,查找到第一个大于2的val值,并更新它。
// 这里的实际意义指长度为2的递增子序列的最小结尾字符更新为2
int left = 1;
for(int right = tmpLenIdx, mid; left < right;){
mid = left + ((right - left) >> 1);
if(dp[mid] < nums[idx]) left = mid + 1;
else if(dp[mid] >= nums[idx]) right = mid;
}
dp[left] = nums[idx];
}
}
return tmpLenIdx;
}
训练题:最长递增子序列个数
题目:https://leetcode-cn.com/problems/number-of-longest-increasing-subsequence/
int findNumberOfLIS( vector<int> &nums )
{
if( nums.size() < 2 ) { return nums.size(); }
const auto size = nums.size();
vector<int> dp( size, 1 ); // 以nums[i]结尾的LIS长度
vector<int> count( size, 1 ); // 以nums[i]结尾的LIS个数
dp[0] = 1; // 初始状态
count[0] = 1; // 初始状态
int maxLen = 0;
int maxLenCount = 0;
for( int idx = 1; idx < size; ++idx )
{
for( int tmp = 0; tmp < idx; ++tmp )
{
if( nums[idx] > nums[tmp] ) { // 统计递增子序列。
if( dp[tmp] + 1 > dp[idx] ) { // 出现更长的最长递增子序列
dp[idx] = dp[tmp] + 1;
count[idx] = count[tmp];
}
else if( dp[tmp] + 1 == dp[idx] ) { // 找到同样长的最长递增子序列。
count[idx] += count[tmp];
}
}
}
maxLen = max(maxLen, dp[idx]);
}
for( int idx = 0; idx < size; ++idx )
if( maxLen == dp[idx] ) maxLenCount += count[idx];
return maxLenCount;
}
训练题:“俄罗斯套娃”信封
题目:https://leetcode-cn.com/problems/russian-doll-envelopes/
方法一:动态规划
时间复杂度:O(n)
int maxEnvelopes( vector<vector<int>> &envelopes )
{
if( envelopes.size() < 2 ) { return envelopes.size(); }
// 宽度升序排列,高度降序排列。升序排列好理解,降序排列的原因是考虑宽度相同的情况。
sort( envelopes.begin(), envelopes.end(), []( const vector<int> &a, const vector<int> &b )
{
return a[0] < b[0] || ( a[0] == b[0] && a[1] > b[1] );
} );
const auto size = envelopes.size();
vector<int> dp( size, 1 ); // dp[i],以第i个信封结尾的套娃数量最大长度。
int ret = 1;
for( int idx = 1; idx < size; ++idx )
{
for( int tmp = 0; tmp < idx; ++tmp)
{
// 这里只比较了高度,为什么就可以呢?
// 首先可以肯定,envelopes[idx] >= envelopes[tmp][0]的,分两种情况分析:
// 假设envelopes[tmp][0] == envelopes[idx],则envelopes[idx][1] <= envelopes[tmp][1],
// envelopes[idx][1] > envelopes[tmp][1]不可能成立。也即宽度相同时候,这个if不可能成立,显然宽度相同不能进行套娃,因此这天然帮我们过滤了宽度相同的情况。
// 假设envelopes[tmp][0] < envelopes[idx],显然只需要判断高度即可,即如下判断。
// 综上,宽度升序,高度降序时,以下条件判断即可过滤出idx可套娃tmp的情况。
if( envelopes[tmp][1] < envelopes[idx][1] )
{
dp[idx] = max( dp[idx], dp[tmp] + 1 ); // 找到套娃最大值。
ret = max( ret, dp[idx] );
}
}
}
return ret;
}
方法二:贪心算法
和最长递增子序列类似的贪心。
int maxEnvelopes(vector<vector<int>>& envelopes) {
if(envelopes.size() < 2) return envelopes.size();
sort(envelopes.begin(), envelopes.end(), [](const vector<int>& a, const vector<int>& b){
return a[0] < b[0] || (a[0] == b[0] && a[1] > b[1]);
});
const auto size = envelopes.size();
vector<int> dp = { envelopes[0][1] }; // dp[i],套娃信封数量为i+1的最小结尾信封索引
dp.reserve(envelopes.size());
for(int idx = 1; idx < size; ++idx){
int height = envelopes[idx][1];
if(height > dp.back()){ // 后面的height大于前面的height,则后面的width肯定是大于前面的width
dp.push_back(height);
}
else{
auto it = lower_bound(dp.begin(), dp.end(), height);
*it = height;
}
}
return dp.size();
}
训练题:最大乘积子数组
题目:https://leetcode-cn.com/problems/maximum-product-subarray/
- 时间复杂度:O(n)
- 空间复杂度:O(n)
由于乘法的负负得正的特性,越小的负数乘以一个负数结果会越大,越大的正数乘以一个正数结果也会越大。我们可以记录两个极值,最小乘积和最大乘积,这两个乘积因为负数的出现会调换位置,当然还有0。
int maxProduct(vector<int>& nums) {
if(nums.empty()) return 0;
if(nums.size() == 1) return nums[0];
const auto size = nums.size();
int ret = 0;
vector<int> dp_max(size, 0);
vector<int> dp_min(size, 0);
ret = dp_max[0] = dp_min[0] = nums[0];
for(int idx = 1; idx < size; ++idx){
dp_max[idx] = max(dp_max[idx-1]*nums[idx], max(nums[idx], dp_min[idx-1]*nums[idx]));
dp_min[idx] = min(dp_max[idx-1]*nums[idx], min(nums[idx], dp_min[idx-1]*nums[idx]));
ret = max(ret, dp_max[idx]);
}
return ret;
}
把O(n)空间优化为O(1),dp[i]只需要dp[i-1]的数据。
int maxProduct(vector<int>& nums) {
if(nums.empty()) return 0;
if(nums.size() == 1) return nums[0];
const auto size = nums.size();
int ret = nums[0];
int dp_max = nums[0];
int dp_min = nums[0];
for(int idx = 1, dp_tmp; idx < size; ++idx){
dp_tmp = dp_max;
dp_max = max(dp_max*nums[idx], max(nums[idx], dp_min*nums[idx]));
dp_min = min(dp_tmp*nums[idx], min(nums[idx], dp_min*nums[idx]));
ret = max(ret, dp_max);
}
return ret;
}
训练题:环形子数组的最大和
题目:https://leetcode-cn.com/problems/maximum-sum-circular-subarray/
共有两种情况:
蓝色区域为最大和的连续子数组。
情况一:该连续子数组没有同时包含首尾元素,经没有across的情况,就是求最大子序和的问题。
情况二:该连续子数组同时包含了首尾元素,此时该区域在数组上是在首尾两端两部分组成,我们只需求得中间连续部分的最小子序和,即可获得最大和。
int maxSubarraySumCircular(vector<int>& nums) {
if(nums.empty()) return 0;
if(nums.size() == 1) return nums[0];
const auto size = nums.size();
int sum = nums[0]; // 数组和
int sum_max = nums[0]; // 情况一的最大子序和
int sum_tmp = nums[0]; // 中间变量
for(int idx = 1; idx < size; ++idx){
sum += nums[idx];
sum_tmp = nums[idx] + max(sum_tmp, 0);
sum_max = max(sum_max, sum_tmp);
}
int sum_min = nums[1]; // 情况二的最小子序和
sum_tmp = nums[1];
// 情况二的最大子序和肯定包含了nums[0]和nums[size-1]
// 因此最小子序和肯定在nums[1]~nums[size-2]区间。
for(int idx = 2; idx < size - 1; ++idx){
sum_tmp = nums[idx] + min(sum_tmp, 0);
sum_min = min(sum_min, sum_tmp);
}
return max(sum_max, sum - sum_min);
}
训练题:最大子矩阵(待完成)
题目:https://leetcode-cn.com/problems/max-submatrix-lcci/
训练题:Nim游戏
题目:https://leetcode-cn.com/problems/nim-game/
bool canWinNim(int n) {
// 余子数量为n+1时,我们能不能赢。
vector<bool> dp(n+1, true);
for(int idx = 4; idx <= n; ++idx) dp[idx] = !(dp[idx-1] && dp[idx-2] && dp[idx-3]);
return dp[n];
}