钢条切割
矩阵链乘法
伪代码如下:
// 矩阵链乘法,p为矩阵的规模下标数组
Matrix_chain_order(p)
n = p.length - 1 // 矩阵的个数
let m and s be new tables, scale is n * n
for i = 1 to n // 下标从1开始
m[i,i] = 0 // 单一矩阵无计算代价
for l = 2 to n // length从2到n的矩阵链的计算代价
for i = 1 to n-l+1 // 计算m[i,j]的最小代价
j = i+l-1 // 定义矩阵A(i,j)链
m[i,j] = inf //inf表示无穷
for k = i to j-1 // k为分割点A[i,k] * A[k+1,j]
q = m[i,k]+ m[k+1,j]+ p[i]p[k]p[j]
if q < m[i,j]
m[i, j] = q
s[i,j] = k // 记录分割点,进而可以重构解
return m[1, n] and s // m[1,n] 为A[1,n]矩阵链的计算代价, s用于重构解
Get_Optimal_parens(s,i,j)
if i == j
print "A[i]"; //s[i,i]
else
print"("
Get_Optimal_parens(s,i,s[i,j])
Get_Optimal_parens(s,s[i,j]+1,j)
")"
C++代码实现如下:
最大子序合
- 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
例如 nums = [-2,1,-3,4,-1,2,1,-5,4]
常数空间
nums_max[i] 为至位置为i处的连续子数组最大值
则[4, -1 2, 1]即为nums_max[6]
nums_max[i] = max{nums_max[i-1], nums[i], nums[i] + nums_max[i-1]} i from 1 to n
最大连续子数组max_arry即为 max(nums_max)
nums_max = [-2, 1, -2, 4, 3, 5, 6, 1, 5];
max_arry = 6
if nums_max[i-1] > 0, nums[i] + nums_max[i-1] 最大
else nums[i] 最大
故 nums_max[i] = nums_max[i-1] > 0 ? nums[i] + nums_max[i-1] : nums[i]
伪代码如下:
int maxSubArry(nums)
len = len(nums)
let nums_max be a n*1 array
nums_max = nums
for x = 1 to len
if nums_max[x-1] > 0
nums_max[x] += nums[x-1]
max_arry = max(nums_max)
return max_arry
C++实现如下
int max_sum = nums[0];
int len = nums.size();
for(int i =1; i< len; ++i)
{
if (nums[i-1]>0) nums[i] += nums[i-1];
max_sum = max(nums[i], max_sum);
}
return max_sum;
LCS
最优二叉搜索树
最长回文子串
- 给定一个字符串s,找到s的最长的回文子串
- 例如:
- babad
- ans : bab or aba
- 暴力法:
对于长度为n的字符串按顺序一共有n*n种可能的字符串(len :1 -> n; 起始点: 1 -> n)
然后再对每个字符串判断其是否为回文字符串n种可能2 - 动态规划:
- 回文字符串有如下特性
如果S[i, j]为最长回文字符串,当S[i] = S[j], 故若S[i+1,j-1]为S的回文字符串,S[i] = S[j]则S[i,j]为回文字符串 - 定义P[i, j]存储S[i]与S[j]的关系
- 从上述分析可知划分问题的是字符串长度的多少因此先初始化一字母,二字母回文然后找到所有三字母回文然后按长度递增
- 复杂度
- 时间:O(N*N)
- 空间:O(N*N)
伪代码如下:
- 回文字符串有如下特性
string longestPalindrome(string s) {
n = len(s) - 1
let p be a len * len matrix;
// 初始化一字母,二字母回文
for i = 0 to n
p[i][i] = 1
for j = i + 1 to n
if s[i] == s[j]
p[i][j] = 1
else p[i][j] = 0 // p是一个上三角矩阵,即i < j
// 寻找所有三字母回文
for i = 0 to n-2
if s[i] = s[i+2]
p[i][i+2] = 1
else
P[i][i+2] = 0
// 寻找最长回文字符串
for l = 3 to n
for i = 0 to n -l
j = i + l;
if p[i+1][j-1] and s[i] = s[j]
p[i][j] = 1
else
p[i][j] = 0
// 存储所有长度字符串的信息
tag = false
for l = n downto 1
if !tag
for i = 0 to n-l
if p[i][i+l]
tag = true // 也可以直接在这里return
rst = l+1;
else
break
return rst
}
- 改进
- 可以提前退出:没必要判断所有长度的回文字符串情况
改进伪代码如下:
- 可以提前退出:没必要判断所有长度的回文字符串情况
string longestPalindrome(string s) {
n = len(s) - 1
let p be a len * len matrix;
// 初始化一字母,二字母回文
for i = 0 to n
p[i][i] = 1
tag = false
for j = i + 1 to n
if s[i] == s[j]
p[i][j] = 1
tag = true
else p[i][j] = 0 // p是一个上三角矩阵,即i < j
if ! tag
return 1
// 寻找所有三字母回文
for i = 0 to n-2
tag = flase
if s[i] = s[i+2]
p[i][i+2] = 1
tag = true
else
P[i][i+2] = 0
if !tag
return 2
// 寻找最长回文字符串
for l = 3 to n
tag = false
for i = 0 to n -l
j = i + l;
if p[i+1][j-1] and s[i] = s[j]
p[i][j] = 1
tag = true
else
p[i][j] = 0
if !tag
return l // 该回合没有增加长度
return n + 1 // 循环执行完毕整个字符串均为回文
}
- 空间复杂度可以为O(n)
O(n)空间复杂度,回文字符串为镜像,如果字符串长度len%2 = 0 则中点在两个字符串中间(n-1个中心点),如果len%2 = 1,则有n个中心点。用P[2n-1]记录这2n-1个中心点的回文子串长度。
伪代码如下:
string longestPalindrome(string s) {
n = len(s) - 1
let p be a array length is 2n + 1 (2 * len - 1)
// 对称点索引值: 0 0.5 1 1.5 ............. n-1 n-0.5 n 对应p[i + j]
// 初始化一字母,二字母,三字母回文 p[0]为上一轮循环后回文字符串最大长度,p[2n]为当前字符串最大长度
p[0] = p[2n] = 1
for i = 0 to n-2
if s[i] == s[i+1]
p[2i+1] = 2 // 中心点为i与i+1
p[2n] = 2
else
p[2i+1] = 1
if s[i] == s[i+2]
p[2i+2] = 3 // 中心点为i+1 1 -> n-1
p[2n] = 3
else
p[2i+2] = 1
if s[n-1] = s[n]
p[2n-1] = 2
if p[2n] == 1
p[2n] = 2
if p[2n] == p[0] // 回文字符串最大长度为
return 1
else
p[0] = p[2n] // 更新
// 动态规划
for l = 3 to n // l为长度
for i = 0 to n-l
j = i+l
if s[i] == s[j] and p[i+j] == l-1
p[i+j] += 2
p[2n] = p[i+j]
if p[0] == p[2n]
return l
return n + 1
}
C++实现代码如下
不同的二叉搜索树
需要知识:
- 二叉搜索树
- 树的数据结构
- 动态规划思想
爬楼梯
- 题目描述:
- 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
- 每次你可以爬 1 或 2 个台阶。你有多少种不 同的方法可以爬到楼顶呢?
- 注意:给定 n 是一个正整数。
- 本质是一个斐波那契额数列
- p[n] = p[n-1] + p[n-2]
- p[1] = 1 p[2] = 2
C++代码如下
int climbStairs(int n) {
if(n==1)
return 1;
else if(n == 2)
return 2;
else
{
vector<int> p;
p.push_back(1); p.push_back(2);
for(int i=2; i< n; ++i)
{
p.push_back(p[i-1] + p[i-2]);
}
return p[n-1];
}
买卖股票的最佳时间
- 题目描述:给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。注意你不能在买入股票前卖出股票。
题目来源-Leetcode - 本质即变形求最大子序合即给定一个数组求连续子数组的最大和是多少
- 给定数据[7,1,5,3,6,4]与[7, 6, 5, 4, 3]
- 分别求较前一天差值
- [0,-6,4,-2,3,-2] 与 [0,-1,-1,-1,-1]
- 最大子序和
- [0,-6, 4, 2, 5, 1]与[0, -1, -1, -1, -1]
- max:5 与 0 分别为第2天买入第5天卖出和不买入(利润:5和0)
int maxProfit(vector<int>& prices) {
int len = prices.size();
vector<int> prices_sub = prices;
if (len <= 1)
return 0;
for(int i =1; i< len; ++i)
{
prices_sub[i] -= prices[i-1];
}
prices_sub[0] = 0;
for(int i=1; i< len; ++i)
{
if (prices_sub[i-1] >= 0)
{
prices_sub[i] += prices_sub[i-1];
}
prices_sub[0] = max(prices_sub[0], prices_sub[i]);
}
return prices_sub[0];
}
打家劫舍
- 定义一个数组为P,P[i]代表掠夺到第i户人家(包括i户)带来的最大收益 (1< i < n) P[n]为所求值
- 对于第n户人家是否掠夺面临一个选择
- P[n] = max{p[n-1], p[n-2] + nums[n]}
C++代码如下
int rob(vector<int>& nums)
{
vector<int> price = nums;
int len = nums.size();
if(len==0)
return 0;
else if(len == 1)
return nums[0];
else if(len == 2)
return max(nums[0],nums[1]);
else
{
price[1] = max(price[0],price[1]);
for(int i=2; i<len; ++i)
{
price[i] = max(price[i] + price[i-2], price[i-1]);
}
}
return price[len-1];
}
粉刷房子
- 本质与打家劫舍相同,打家劫舍在面对一家的时候有两种选择,打劫或者不打劫进而产生两种选择结果
- 粉刷房子三种选择即选择什么颜色,这三种选择会带来不同的结果,选择其中代价最少的
- 对于第n家
- 如果选择打劫,则n-1家就不可以被打劫prices[n-2]
- 选择不打劫,prices[n-1]
- 故prices[n] = max{nums[n] + prices[n-2], prices[n-1]}
- 同理对于粉刷房子我们可以分为三种情况
- 蓝绿,绿红,红蓝,每种情况都缺少一种颜色
- 那么第n个房子如果涂红色,那么我们就不可以从绿红和红蓝两种情况选择 故 costs[n][0] + prices[n-1][0]
- 同理costs[n][1] + prices[n-1][1] ; costs[n][2] + prices[n-1][2]
- prices[n][0] = min{cost[n][1] + prices[n-1][1], costs[n][2] + prices[n-1][2]}
- 余下同理
- rst = min{prices[n][0] , prices[n][1], prices[n][2]}
- [[17,2,17],[16,16,5],[14,3,19]]
- [[2,17,2],[7,7,18],[10,21,10]]
- prices[1][0] = min{cost[1][1] + prices[0][1], cost[1][2] + prices[0][2]} = min{16+17, 5+2} = 7
- 同理
- rst = min{10,21,10} = 10 = 2 蓝 1 绿 0 蓝
- 元素最大的即为该房子所涂颜色
int minCost(vector<vector<int>>& costs) {
int len = costs.size(); // 有多少个房子
if(len == 0)
return 0;
else if(len == 1)
{
int rst = costs[0][0];
rst = min(rst,min(costs[0][1],costs[0][2]));
return rst;
}
else
{
vector<vector<int>> prices = costs;
prices[0][0] = min(costs[0][1], costs[0][2]);
prices[0][1] = min(costs[0][0], costs[0][2]);
prices[0][2] = min(costs[0][0], costs[0][1]);
for(int i=1; i<len; ++i)
{
prices[i][0] = min(prices[i-1][1] + costs[i][1], prices[i-1][2]+ costs[i][2]);
prices[i][1] = min(prices[i-1][0] + costs[i][0], prices[i-1][2]+ costs[i][2]);
prices[i][2] = min(prices[i-1][0] + costs[i][0], prices[i-1][1]+ costs[i][1]);
}
int n =len-1;
int rst = min(prices[n][0],min(prices[n][1], prices[n][2]));
return rst;
}
}
栅栏涂色
- 要求的是涂色的方案数,也就是说根据涂色顺序的不同,最后会有数目不相同的方案个数。
- 即求最大方案数
- 最多连续两个颜色,思考前面的爬楼梯,仅可以上一层或者两层那么有多少种不同方法可以爬上n阶楼梯,此题本质是与其一样的,最多连续涂两种颜色,那么也就是说对于n个栅栏也就是相当于要上n层台阶,假设k=1,涂的颜色连续涂一个和连续涂两个一样的颜色就类似于上一层或两层台阶。现在增加了一个条件可以用不同的方式上台阶:跑,跳,爬,走 即k = 4,每种方式最多上两层台阶那么变换方式也就类似于粉刷房子(打家劫舍,只不过平均主义每家钱都是相同的),不可以用相同的连续。也就是说我们每一步面临着两种选择
- 涂颜色的数目 2中选择
- 涂什么颜色 (第一次k种选择,后续k-1种选择)
- 回忆上楼梯本质就是斐波那契数列 只不过fib(0)与fib(1)值不同
- 涂颜色如果选择涂了一种颜色那么剩下就只有k-1种颜色可以选,而后续也是k-1
C++实现代码如下:
int numWays(int n, int k) {
if(n == 0 or k == 0) return 0;
if(n==1) return k;
if(n==2) return k*(k-1) + k;
else
{
vector<int> ways;
int mulp = k-1;
ways.push_back(k); // 第一次涂一次一种颜色有k种涂色方案
ways.push_back(k + k*mulp); // 第一次连续涂两次同种颜色有k种涂色方案 + 涂两种不同颜色
for(int i=2; i<n; ++i)
{
ways.push_back(mulp*ways[i-1] + mulp*ways[i-2]);
}
return ways[n-1];
}
}
区域和检索-数组不变
- 实不相瞒这道题一开始没有看懂,因为我C++是个半吊子,于是我们切换成python吧
- 题目里面强调了多次调用其本质就是解决一个重叠子问题的问题
- 如果就用暴力循环法的话:会导致多次调用sumrange使得运行超时,然而如果把每次sumrange的结果存储起来的话,对于一个长度为n的数组任意[i,j]对就为n*n的内存空间,很明显我们并不需要这么多的内存空间
- 如果求sumrange(2,5) = sumrange(0,5) - sumrange(0,2)那么也就是说我们可以只需要O(n)的内存空间就可以解决此问题
- 每次运行的时候将从0到n的值保存下来然后执行sumRange的时候进行减就可以了
- 时空复杂度都为线性的
python代码如下
def __init__(self, nums: List[int]):
length = len(nums);
if length == 0:
return None
else:
self.nums = nums;
for x_cur in range(length):
if x_cur > 0:
nums[x_cur] += nums[x_cur - 1] # 求前n项和
def sumRange(self, i: int, j: int) -> int:
if i>0:
return self.nums[j] - self.nums[i-1]
else:
return self.nums[j]
- 考虑长度为0的情况直接返回None
判断子序列
- s为短字符串,t为长字符串
- 暴力搜索法
- i_cur,j_cur两个游标分别指示s和t由于不要求有序因此最大时间复杂度为O(len(t))
- 然而既然s要比t短很多那一定存在更快的算法
暴力搜索法代码如下
bool isSubsequence(string s, string t) {
int i_cur = 0;
int j_cur = 0;
int len = s.size();
int len_t = t.size();
while(j_cur < len_t)
{
if(s[i_cur] == t[j_cur])
++ i_cur;
++j_cur;
if(i_cur == len) return true;
}
return false;
}
注意题目的描述t很长但均为英文小写字母
后续挑战
使用最小花费爬楼梯
int minCostClimbingStairs(vector<int>& cost) {
int len = cost.size();
cost.push_back(0);
if(len == 2)
return cost[1];
else
{
for(int i=2; i<=len; ++i)
cost[i] = min(cost[i] + cost[i-1], cost[i] + cost[i-2]);
return cost[len];
}
}
除数博弈
- 看起来很复杂的样子其实很简单
- N%x == 0 就代表可以被整除如果不能被整除那么就输掉了游戏 0 < x < N 每次用N-x代替N
- 也就是说到谁那里N = 1,对方就赢得了比赛
- 一个质数仅可以由1*本身得到
- 而一个素数可以由1*若干质数得到
- 例如93 = 1 * 3 * 31
- 例以下叫喊顺序(有许多叫喊顺序)
- 93 爱丽丝喊31 -> 62 鲍勃喊31 -> 31 爱丽丝喊1 -> 30 鲍勃喊15 -> 15 爱丽丝喊3 -> 12 鲍勃喊4 -> 8 爱丽丝喊4 -> 4 鲍勃喊 2 ->2 爱丽丝喊1 ->1 鲍勃无法进行 爱丽丝胜
通过推导发现了一个奇特的现象:
class Solution {
public:
bool divisorGame(int N) {
if(N%2 == 0)
return true;
else return false;
}
};
hhhhh 是不是很奇怪但结果恰恰是正确的
那么接下来给出C++的动态规划思想和实现方法
再看看能不能用数学方法证明
动态规划
首先我们知道N = 1时 谁先喊谁就输了,因为N没有在1和N之间的公约数
那我们在考虑N=2时 2 = 1 * 2 故只能喊X= 1那么N-X = 1 由N = 1 我们可以知道谁喊谁就输了
我们定义数组nums[length]
- nums[i] = 1 表示喊这个数得就获得了胜利
- nums[i] = -1 表示喊这个数就失败了
由于每个人都处于最佳状态因此对于n种选择,其中只要有一种选择可以使自己获胜那么nums[i] = 1
同时我们还知道一个数由小于该数得所有质数相乘得到
对于每个数N得到其相乘字典 - mulp (0 -> k)
因此我们以下用如下状态表明状态迁移方程
nums[n] = -1 if all nums[i] != 1 (i in mulp) (所有可以喊得数带来的结果全是输)
1 (只要有一个数喊得结果可以赢)
最小路径和
- 用窗口为2的窗口对全体路径进行探测O(n^2)
- max_matrix[m][n] = max{max_matrix[m-1][n-1], max_matrix[m][n-1], max_matrix[m-1][n]}
int minPathSum(vector<vector<int>>& grid) {
int m = grid.size(); // col
int n = grid[0].size(); // row
int rst = 0;
if(m==0||n==0);
else if(m==1||n==1)
{
if(n==1)
for(int i=0; i<m; ++i) rst += grid[i][0];
else
for(int i=0; i<n; ++i) rst += grid[0][i];
}
else
{
for(int i=1; i<n; ++i)
{
grid[0][i] += grid[0][i-1];
}
for(int i=1; i<m; ++i)
{
grid[i][0] += grid[i-1][0];
} // 边界初始化
for(int i=1; i<m; ++i)
{
for(int j=1; j<n; ++j) grid[i][j] += min(grid[i-1][j], grid[i][j-1]);
}
for(int i=0; i<m; ++i)
{
for(int j=0; j<n; ++j) cout << grid[i][j] << endl;
}
rst = grid[m-1][n-1];
}
return rst;
}
最佳买卖股票时机
- 与买卖股票相比较
- 可以多次买卖
- 有冷冻期
- 仔细分析一下可以发现其是买卖股票和打架劫舍得结合版
- 需要我们变形得求连续最大子数组
- 不能连续求解,即每段数组之间至少间隔1我们可以认为每段数组所获取得值就是打劫每家房子所收获得利益。我们的目的是在不可以连续打劫
- 我们如何才能获得最大收益呢
- 我们当然希望在低点买入高点卖出,在高点卖出后等待下一个低点再次买入在再高点卖出,即我们希望可以在上升时持有,在下降时空仓。
- 但通过前面的简单买卖股票我们就可以发现如果仅考虑最低点和最高点的话是不一定的,因为最高点可能在最低点的前面出现。所以我们用动态规划的思想将其转化为了求最大连续子数组
- 那么这道题同样的道理应该也可以利用动态规划转化为求一个数组可以分段的情况下连续子数组的最大和
- 比如股票得价格为:[1,2,3,0,2]计算差值
- [0, 1, 1, -3, 2]
- [0, 1, 2, -1, 2] 可知在该数组中连续最大子数组的和为2
- 那么如何计算有间断的连续子数组的最大和呢。
- 我们假设price[i][j] 为第i天买入第j天卖出股票可以收获的最高利益。由于有冷冻期也就是说我们最早也只能在第i+2天买入。price[i+2][k] (第i+2天买入第k天卖出)
- 以同样的思想推算前面的卖出时间距离卖出的这天最早的卖出时间就只能是price[l][i-2]
- 可以得到price是一个n*n的上三角矩阵
- 故 price[i][j] = max{price[i][j], price[i][k] + price[k+2][j]}(i < k < j-2) 我们知道后面的由于k项可变实际是许多项,再看这个递归式像不像钢条切割的案例。
- 当我们需要考虑多种情况时候,可以先确定一面是固定的分割另外一面
- 我们可以认为左面price[i][k]为固定长度右面price[k+2][j]需要求其各个参数
- 可以证明左面的price[i][k]不需要分割即为最优解
- k < j 故若存在另外的解使得该解优于price[i][k]则不符合最优子结构
int maxProfit(vector<int>& prices) {
int len = prices.size();
int rst = 0;
if(len > 1)
{
vector<vector<int>> w;
for(int i=0; i<len; ++i)
{
vector<int> temp = {0}; // 生成临时容器第一个数据存储0
int j=i; int k=i; ++j; // 指针k为指针j的前一个指针
for(int cur = 0; j<len; ++j,++k,++cur)
{
int sub = prices[j] - prices[k]; //与前一个数的差值
if(temp[cur] < 0)
temp.push_back(sub);
else
temp.push_back(sub + temp[cur]);
}
w.push_back(temp);
}
for(int i = 0; i<len; ++i)
{
int max_now_circle = w[i][0];
for(int j=1; j<w[i].size(); ++j)
{
max_now_circle = w[i][j] = max(max_now_circle, w[i][j]); //存储差值
}
}// A[i.j]的单一连续数组的最大值
for(int l = 4; l<len; ++l) // 以l作为区分问题的参量
{
int j= l;
for(int i=0; j<w[i].size(); ++i) // w为上三角矩阵要特别注意索引(i表示pricesA[i,j],j表示距离i的距离)
{
int max_now_circle = w[i][j];
int pre_len = 1; int mid_tail=i+3; int tail_len = l-3;// 分割点
for(; tail_len>0; ++pre_len, ++mid_tail, --tail_len)
max_now_circle = max(max_now_circle, w[i][pre_len]+w[mid_tail][tail_len]);
w[i][j] = max_now_circle;
}
}
rst = w[0][len-1];
}
return rst;
}
int maxProfit(vector<int>& prices) {
int dp[5] = {0,0,0,0,0};
int len = prices.size();
int rst = 0;
if(len >1)
{
dp[1] -= prices[0];
dp[3] = -99999999;
for(int i=1; i<len; ++i)
{
dp[0] = 0;
dp[1] = max(dp[1], dp[0] - prices[i]); // 第一次买入
dp[2] = max(dp[2], dp[1] + prices[i]); // 第一次卖出
dp[3] = max(dp[3], dp[2] - prices[i]); // 第二次买入
dp[4] = max(dp[4], dp[3] + prices[i]); // 第二次卖出
}
rst = max(dp[2],dp[4]);
}
return rst;
}
在这里插入代码片