接下来最为核心的知识点目前杨某人思路清晰的整理好了,首先为大家展示几个核心算法的理论思想,只有掌握了这种基础才能更好的做题通过笔试锻炼面试时技术的询问,接下来会给大家一些质量比较高的笔试试题,帮助大家更好的理解算法的思想,完成理论到实践、逻辑到技术的转变,最后会为大家整理一些大厂关注的一些算法试题的套路和应对的套路,不保证百分之百,但是会保证60%以上的思想,话不多说,开始啦~~
额,多说一句:关于之前的项目和之后的一些比较重要的算法(整理了个人的最好的资源更容易学习和联系)都将会设置上一定的权限,望大家理解,不过粉丝可以一直免费观看
本人掌握动态规划的过程,有点钻牛角尖,这里用我的心历路程给各位同学们做个提醒。当我刚看到动态规划这个响亮的大名时,瞬间陷入了沉思,脑中浮想联翩,揣摩着这个算法应该很带感。
看一些大众的百度的解释:
动态规划在寻找有很多重叠子问题的情况的最佳解时有效。它将问题重新组合成子问题。为了避免多次解决这些子问题,它们的结果都逐渐被计算并被储存,从简单的问题直到整个问题都被解决。因此,动态规划储存递迴时的结果,因而不会在解决同样的问题时花费时间。 动态规划只能应用于有最佳子结构的问题。最佳子结构的意思是局部最佳解能决定全域最佳解(对有些问题这个要求并不能完全满足,故有时需要引入一定的近似)。简单地说,问题能够分解成子问题来解决
看了上面这一段之后,我完全懵逼了。看了一个最为基础的例子:斐波那契数列 之后,我的脑壳有点昏昏的赶脚;我个人询问了很多遍小灰灰之后,再加上一定量的试题练习,我总算是理解了,动态规划明明就是高中的数列试题嘛,非要起个名字说什么动态迷惑人。
如果-一个问题满足以下两点,那么它就能用动态规划解决。
第一点: 问题的答案依赖于问题的规模,也就是问题的所有答案构成了一个数列。举个简单的例子,1个人有2条腿,2个人有4条腿,… n个人有多少条腿?答案是2n条腿。这里的2n问题的答案,n则是问题的规模,显然问题的答案是依赖于问题的规模的。答案是因变量,问题规模是自效。因此,问题在所有规模下的答案可以构成- -个数列(f(1), f(2)…f(n,),比如刚刚“数腿”的例子就构成了间隔为2的等差数列(0,2,…,2n) 。
第二点: 大规模问题的答案可以由小规模问题的答案递推得到,也就是f(n)
的值可以由 f(i)|i < n 中的个别求得。还是刚刚“数腿” 的例子,显然f(n) 可以基于f(n- 1) 求得: f(n)= f(n-1)+2.
能用动态规划解决,不代表适合用。比如刚刚的“数腿” 例子,你可以写成f(n) = 2n的显式表达式形式,那么杀鸡就不必用牛刀了。但是,在许多场景,f(n) 的显式式子是不易得到的,大多数情况下甚至无法得到,动态规划的魅力就出来了。
当要应用动态规划来解决问题时,归根结底就是想办法完成以下三个关键目标。
这一步是最难的,大部分人都被卡在这里。这一步没太多的规律可说,只需抓住一个思维:当做经知道f(1)~ f(n- 1)的值,然后想办法利用它们求得f(n)。在“数腿”的例子中,状态转移方程即为f(n)= f(n- 1)+2。
这一步不难,但是很重要。如果没有合适地处理,很有可能就是指数和线性时间复杂度的区别。假设在“数腿”的例子中,我们不能用显式方程,只能用状态转移方程来解。如果现在f(100)未知,但是刚刚求解过- -次f(99) 。如果不将其缓存起来,那么求f(100)时,我们就必须花100次加法运算重新获取。但是如果刚刚缓存过,只需复用这个子结果,那么将只需一次加法运算即可。
这里的"小” 和“大” 对应的是问题的规模,在这里也就是我们要从f(0),f(1) …到f(n)依次顺序计算。这一点在“数腿”的例子来看,似乎显而易见,因为状态方程基本限制了你只能从小到大一步步递推出最终的结果(假设我们仍然不能用显式方程)。然而当问题复杂起来的时候,你有可能乱了套,所以必须记住这也是目标之-。
看到这里,你可能会觉得怎么跟高中的数列题那么像? ?其实在我看来这就是高中数列题的魔改版。
高中的题一般需先推导出状态转移方程,再据此推导出显式表达式(在高中时代称为通项公式)。然而,动态规划是要我们在推导出状态转移方程后,根据状态转移方程用计算机暴力求解出来。显式表达式?在动态规划中是不存在的!就是因为要暴力计算,所以前面说的目标有两个是涉及到代码层面上:
●缓存中间结果:也就是搞个数组之类的变量记录中间结果。
●按顺序从小往大算:也就是搞个for循环依次计算。
最适合动态规划的入门的试题莫过于斐波那契数列了,斐波那契数列: 0,1,1,2, 3, 5,8, 13, 21, 34, 55, 89, 144, 233.它遵循这样的规律:当前值为前两个值的和。那么第n个值为多少?
int fun(int n){
if(n==1||n==0)
return n;
return fun(n-1)+fun(n-2);
}
如上所示,代码简单易懂,然而这代码却极其低效。 先不说这种递归的方式造成栈空间的极大浪
费,就仅仅是该算法的时间复杂度已经属于0(2n)了。指数级别时间复杂度的算法跟不能用没啥
区别!
为什么是指数时间复杂度?我通过展示求解f(6) 的过程给大家说明一下原因。如图,随着递归的深
入,计算任务不断地翻倍!
int fun(int n)
{
if(n==0||n==1)
return n;
int *k=new int [n];
k[0]=0;k[1]=1;
for(int i=2;i<=n;i++)
{
k[ i ]=k[i-1]+k[i-2];
}
return k[n]
}
如上代码,针对动态规划的三个子目标,都很好地实现了(参考备注),具体为:
●目标1,建立状态转移方程(完成)。也就是前面的f(n)= f(n-1)+ f(n-2)。
●目标2,缓存并复用以往结果(完成)。图1的简单递归存在大量的重复任务。在线性规划解法
中,我们把结果缓存在results列表,同时在results[们] = results[i-1] + results[i-2]中进行了复
用。这相当于我们只需完成图2中红色部分的计算任务即可,时间复杂度瞬间降为O(n)。
通过线性规划通过缓存与复用机制将计算规模缩小,大家可观察上图的最左侧即可
●目标3,按顺序从小往大算(完成)。for循环实现了从0到n的顺序求解,让问题按着从小规模
往大规模求解的顺序走,。
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。问总共有多少条不同的路径?
大家自己先思考一下然后在观看答案,先给大家说一下思路
解这题,如前所述,我们需要完成三个子目标
1.建立状态转移方程。该题就难在这里,这一步搞不定基本上GG了。实际上,如图3所示,第i
行第j列的格子的路径数,是等于它左边格子和上面格子的路径数之和:
f(i,j)= f(i-1,j)+ f(i,j-1)。
2.缓存并复用以往结果。与之前说的一维数列不同,这里的中间结果可以构成-一个二维数列,所以 需要用二维的数组或者列表来存储。
3.按顺序从小往大算。这次有两个维度,所以需两个循环,分别逐行和逐列让问题从小规模到大规
模计算。
static int a[101][101]={0};//记录已经计算过的路径,大大提高效率
class Solution{
public:
int uniquePaths(int m, int n){
if(m <= 0 || n <= 0)
return 0;
else if(m == 1 || n == 1)
return 1;
else if(m == 2 && n == 2)
return 2;
else if((m == 3 && n == 2) || (m == 2 && n == 3))
return 3;
if(a[m][n] > 0)
return a[m][n];
a[m-1][n] = uniquePaths(m-1,n);
a[m][n-1] = uniquePaths(m,n-1);
a[m][n] = a[m-1][n]+a[m][n-1];
return a[m][n];
}
};
给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。
'.' 匹配任意单个字符
'*' 匹配零个或多个前面的那一个元素
所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。
说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 . 和 。
示例一:
输入:s = “aa” p = “a” 输出: false
解释: “a” 无法匹配 “aa” 整个字符串
示例二:
输入:s = “aa” p = "a"输出: true
解释: 因为 ‘’ 代表可以匹配零个或多个前面的那一个元素, 在这里前面的元素就是 ‘a’。因此,字符串 “aa” 可被视为 ‘a’ 重复了一次。
示例三:
输入:s = "aab"p = "cab"输出: true
解释: 因为 '’ 表示零个或多个,这里 ‘c’ 为 0 个, ‘a’ 被重复一次。因此可以匹配字符串 “aab”。
这道题最麻烦的地方在出现*的情况。
首先从最简单的情况开始,只看要比较的两个字符,即源字符串s[i]和模式串p[j]
如果p[j] == '.'或者p[j] == s[i]:当前匹配成功
如果p[j] == ‘*’:需要进一步分情况讨论,假设p[j]前面p[j-1]存在
如果p[j-1] == '.'或者p[j-1] == s[i],则p[j-1]p[j]这两个模式串可以使用1到无数次,匹配成功
如果p[j-1] == '.'和p[j-1] == s[i]都不成立,但可以使用p[j-1]p[j]这两个模式串0次,匹配成功
除了以上情况,均匹配不成功
具体讲一下套路中的第一步:
由于判断p[j] == s[i]、p[j-1]p[j]等当前问题附近的情况,即可减小原问题,因此存在最优子结构,因此可以写出动态规划方程:
记dp[i][j]表示前i个源字符与前j个模式串字符是否匹配(含当前i,j这两个字符)
dp[i][j]就等于:(如下几种情况)
如果p[j] == '.'或者p[j] == s[i]:当前匹配成功,等于true && dp[i-1][j-1]
如果p[j] == ‘*’:需要进一步分情况讨论,假设p[j]前面p[j-1]存在
如果p[j-1] == '.'或者p[j-1] == s[i],则p[j-1]p[j]这两个模式串可以使用1到无数次,匹配成功,等于true && dp[i-1][j]
如果p[j-1] == '.'和p[j-1] == s[i]都不成立,但可以使用p[j-1]p[j]这两个模式串0次,匹配成功,等于true && dp[i-1][j-2]
除了以上情况,均匹配不成功,等于false && dp[i-1][j-1]
#include
#include
#include
using namespace std;
void print(vector> store);
class Solution
{
public:
bool isMatch(string s, string p)
{
int sSize = int(s.size());
int pSize = int(p.size());
if (p.empty())
{
return s.empty();
}
vector tmpVec(pSize + 1, false);//dp大小应该比s,p的size大1
vector> dp(sSize + 1, tmpVec); //dp[i][j] 表示 s 的前 i 个是否能被 p 的前 j 个匹配
dp[0][0] = true;
if (sSize != 0 && (p[0] == s[0] || p[0] == '.'))
{
dp[1][1] = true;
}
if (p[0] == '*')
{
dp[0][1] = true;
}
//初始化情况:s为空,p为.*.*的情况
for (int i = 1; i < pSize; i++)
{
if (p[i] == '*' && dp[0][i - 1] == true)
{
dp[0][i + 1] = true;
}
}
for (int i = 0; i < sSize; i++)
{
for (int j = 1; j < pSize; j++)
{
if (p[j] == '.' || p[j] == s[i])
{ //如果是任意元素 或者是对于元素匹配
dp[i + 1][j + 1] = dp[i][j];
}
if (p[j] == '*')
{
//caa cb* -> caa c
if (p[j - 1] != s[i] && p[j - 1] != '.')
{ //如果前一个元素不匹配 且不为任意元素
dp[i + 1][j + 1] = dp[i + 1][j - 1];
}
else
{
//caa c.* -> ca c.*
//caa ca* -> ca ca*
//ca ca* -> ca ca / c ca*
//ca ca*a* -> ca ca*
dp[i + 1][j + 1] = (dp[i][j + 1] || dp[i + 1][j - 1]);// || dp[i + 1][j]不需要
}
}
}
}
//print(dp);
return dp[sSize][pSize];
}
};
动态规划与其说是一个算法, 不如说是一种方法论。 该访法论主要致力于将合适的问题拆分成三个子目标猪哥击破:
完成这三个套路,你将所向披靡。