2020年面试后端必会算法记录
- C++面试集合
- 十大排序算法
- 两个栈实现一个队列(虾皮笔试)
- 两个队列实现一个栈
- 二叉树
- 数组类
- 字符串类
- 滑动窗口
- 链表类
- 二维矩阵问题
- topk问题(虾皮一面手撕)
- k个有序链表的合并
- k个有序数组的合并
- 出现次数最高的n个单词
- 三数之和(面试手撕题)
- 集合所有的子集
- 全排列
- 字典序第k个数字
- 去重全排列问题
- 组合问题
- 去重组合问题
- 背包问题
- KMP算法
- 二分查找
- 动态规划系列
- 单调栈
- 区间合并(面试手撕题)
- 去重复的三数之和(二面手撕题)
- 并查集(京东笔试题)
- 差分数组/前缀和
- 线段树
- 前缀树(Trie 树)
2020年面试刷题的一些记录
C++面试集合
十大排序算法
参考之前写过的博文
排序算法之bubbleSort,selectSort,insertSort,shellSort
排序算法之mergeSort,quikSort,heapSort
数组排序&&链表排序
快速排序的递归实现
#include<iostream>
#include<vector>
#include<ctime>
#include<bits/stdc++.h>
using namespace std;
//小于主元的放左边,大于等于主元的放右边
int partition(vector<int>& nums,int lt,int rt){
//是否随机选择主元(加上随机选择主元,性能提高好多)
srand((unsigned int)time(NULL));
int pivot=lt+rand()%(rt-lt+1);
swap(nums[lt],nums[pivot]);
//主元
pivot=nums[lt];
int temp;
int i=lt+1,j=rt;
//注意这里的i<=j能换成i<j
while(i<=j){
while(i<=j&&nums[i]<pivot)
i++;
while(j>=i&&nums[j]>=pivot)
j--;
if(i>=j)
break;
temp=nums[j];
nums[j]=nums[i];
nums[i]=temp;
}
//主元交换到rt处
nums[lt]=nums[j];
nums[j]=pivot;
return j;
}
//快速排序
void quickSort(vector<int>& nums,int lt,int rt){
if(lt>=rt) return ;
else{
int mid=partition(nums,lt,rt);
quickSort(nums,lt,mid-1);
quickSort(nums,mid+1,rt);
}
}
快速排序的非递归实现
int myPartition(vector<int>& data,int lt,int rt){
int pivot=data[lt];
while(lt<rt){
while(lt<rt&&data[rt]>=pivot)
rt--;
data[lt]=data[rt];
while(lt<rt&&data[lt]<=pivot)
lt++;
data[rt]=data[lt];
}
data[lt]=pivot;
return lt;
}
//快速排序的非递归实现
void quickSortNotRecursion(vector<int>& data,int lt,int rt){
stack<int> stk;
if(lt<rt){
//先partition,根据mid的左右元素个数决定是否将左右区间的左右边界入栈,
int mid=myPartition(data,lt,rt);
if(mid-1>lt){
stk.push(lt);
stk.push(mid-1);
}
if(mid+1<rt){
stk.push(mid+1);
stk.push(rt);
}
//循环处理栈中的元素,每次弹出两个元素,分别为r,l
while(!stk.empty()){
int r=stk.top();
stk.pop();
int l=stk.top();
stk.pop();
mid=myPartition(data,l,r);
if(mid-1>l){
stk.push(l);
stk.push(mid-1);
}
if(mid+1<r){
stk.push(mid+1);
stk.push(r);
}
}
}
}
两个栈实现一个队列(虾皮笔试)
两个队列实现一个栈
任何时候两个队列总有一个是空的。
添加元素总是向非空队列中 add 元素。
取出元素的时候总是将元素除队尾最后一个元素外,导入另一空队列中,最后一个元素出队。
二叉树
先序遍历
递归
//1.前序遍历,递归,非递归
//递归
void preOrderRecursion(TreeNode* root){
if(root==nullptr) return ;
cout<<root->value<<endl;
preOrderRecursion(root->left);
preOrderRecursion(root->right);
}
非递归
//非递归,面试常考非递归,因为递归写法太简单了
// 给定二叉树的根结点 root:
// (1)并将根结点 root 入栈;
// (2)判断栈是否为空,若不为空,取栈顶元素 cur 访问并出栈。然后先将 cur 的右子结点入栈,再将 cur 的左子结点入栈;
// (3)重复(3)直到栈空,则遍历结束。
void preOrderStack(TreeNode* root){
if(root==nullptr) return;
stack<TreeNode*> Stack;
TreeNode* temp=nullptr;
Stack.push(root);
while(!Stack.empty()){
temp=Stack.top();
cout<<temp->value<<endl;
Stack.pop();
if(temp->right!=nullptr)
Stack.push(temp->right);
if(temp->left!=nullptr)
Stack.push(temp->left);
}
}
中序遍历
递归
//2.中序遍历,递归,非递归
//递归7
void midOrderRecursion(TreeNode* root){
if(root==nullptr) return;
midOrderRecursion(root->left);
cout<<root->value<<endl;
midOrderRecursion(root->right);
}
非递归
//非递归,常考题
// 对于给定的二叉树根结点 root,
// (1)若其左孩子不为空,循环将 root及 root左子树中的所有结点的左孩子入栈;
// (2)取栈顶元素 cur,访问 cur 并将 cur 出栈。然后对 cur 的右子结点进行步骤(1)那样的处理;
// (3)重复(1)和(2)的操作,直到 cur 为空且栈为空。
void midOrderStack(TreeNode* root){
if(root==nullptr) return;
stack<TreeNode*> Stack;
TreeNode* cur=root;
while(!Stack.empty()||cur!=nullptr){//这里循环条件变为栈为空或cur非nullptr
while(cur){//循环将root及root左子树的所有结点的左孩子入栈
Stack.push(cur);
cur=cur->left;
}
cur=Stack.top();
cout<<cur->value<<endl;
cur=cur->right;
Stack.pop();
}
}
后序遍历
递归
//3.后序遍历,递归,非递归
//递归
void postOrderRecursion(TreeNode* root){
if(root==nullptr) return;
midOrderRecursion(root->left);
midOrderRecursion(root->right);
cout<<root->value<<endl;
}
非递归
思路1:
//非递归
//后序遍历的非递归实现是三种遍历方式中最难的一种。
// 第一种思路:对于任一结点 P,将其入栈,然后沿其左子树一直往下搜索,直到搜索到没有左孩子的结点,
// 此时该结点出现在栈顶,但是此时不能将其出栈并访问,因此其右孩子还未被访问。
// 所以接下来按照相同的规则对其右子树进行相同的处理,当访问完其右孩子时,该结点又出现在栈顶,
// 此时可以将其出栈并访问。这样就保证了正确的访问顺序。可以看出,在这个过程中,每个结点都两次出现在栈顶,
// 只有在第二次出现在栈顶时,才能访问它。因此需要多设置一个变量标识该结点是否是第一次出现在栈顶。
void postOrderStack(TreeNode* root){
if(root==nullptr) return ;
stack<pair<TreeNode*, bool>> s;
pair<TreeNode*,bool> cur=make_pair(root,true);
while(cur.first!=nullptr||!s.empty())
{
//沿左子树一直往下搜索,直至出现没有左子树的结点
while(cur.first!=nullptr)
{
s.push(cur);
cur=make_pair(cur.first->left,true);
}
//个人觉得这里的if(!s.empty())没有必要,能进入while(cur.first!=nullptr||!s.empty()),
//然后又经过while(cur.first!=nullptr),栈不可能为空,
if(!s.empty())
{
//表示是第一次出现在栈顶
if(s.top().second==true)
{
s.top().second=false;
if(s.top().first->right!=nullptr)
cur=make_pair(s.top().first->right,true); //将当前节点的右节点入栈
}
else
{
// 第二次出现在栈顶
cout << s.top().first->value << " ";
s.pop();
}
}
}
}
思路2:
//思路2
//第二种思路:要保证根结点在左孩子和右孩子访问之后才能访问,因此对于任一结点R,
//(a)先将R入栈。如果P不存在左孩子和右孩子,则可以直接访问它并出栈;
//(b)如果R存在左孩子或者右孩子,但是其左孩子和右孩子都已被访问过了,则同样可以直接访问该结点并出栈;
//(c)若非上述两种情况,则将R的右孩子和左孩子依次入栈,这样就保证了每次取栈顶元素的时候,左孩子在右孩子前面被访问,左孩子和右孩子都在父结点前面被访问。
// 非递归后序遍历,版本 2
void postOrderStack2(TreeNode* root)
{
if(root==NULL) return;
stack<TreeNode*> s;
TreeNode* cur; //当前结点
TreeNode* pre=NULL; //前一次访问的结点
s.push(root);
while(!s.empty())
{
cur=s.top();
//在判断当前结点时,左孩子和右孩子都在根结点前已经被访问
if((cur->left==NULL&&cur->right==NULL) || (pre!=NULL&&(pre==cur->left || pre==cur->right)))
{
cout<<cur->value<<" "; //如果当前结点没有孩子结点或者孩子节点都已被访问过
s.pop();
pre=cur;
}
else
{
if(cur->right!=NULL) s.push(cur->right);
if(cur->left!=NULL) s.push(cur->left);
}
}
}
层序遍历(最右笔试)
//4.层序遍历
vector<vector<int>>ans;
void BFS(TreeNode* root,vector<vector<int>>& ans){
if(root==nullptr) return;
queue<TreeNode*> Queue;
TreeNode* cur=nullptr;
vector<int> temp;
int size=0;
Queue.push(root);
while(!Queue.empty()){
size=Queue.size();
while(size--){//循环遍历一层
cur=Queue.front();
Queue.pop();
temp.push_back(cur->value);
if(cur->left!=nullptr)
Queue.push(cur->left);
if(cur->right!=nullptr)
Queue.push(cur->right);
}
ans.push_back(temp);//此层插入ans
temp.clear();//temp暂存清空
}
}
重建二叉树
class Solution {
public:
unordered_map<int,int> map; //记录中序遍历各个值的位置
vector<int> pre, ino; //定义全局变量
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
pre = preorder, ino = inorder; //为全局变量赋值
for(int i = 0; i < ino.size(); i ++) map[ino[i]] = i; //存储中序遍历各个值的位置
return dfs(0, pre.size() - 1, 0, ino.size() - 1);
}
TreeNode* dfs(int pl, int pr, int il, int ir) {
if(pl > pr) return nullptr;
TreeNode* root = new TreeNode(pre[pl]); //新建根节点
int pos = map[root->val]; //寻找根节点在中序遍历的位置
root->left = dfs(pl + 1, pl + pos - il, il, pos -1); //新建根节点的左子树,二叉树左分支的区间范围
root->right = dfs(pl + pos - il + 1, pr, pos + 1, ir); //新建根节点的右子树,二叉树右分支的区间范围
return root;
}
};
数组类
4. 寻找两个正序数组的中位数
4. 寻找两个正序数组的中位数
给定两个大小为 m 和 n 的正序(从小到大)数组 nums1 和 nums2。
请你找出这两个正序数组的中位数,并且要求算法的时间复杂度为 O(log(m + n))。
你可以假设 nums1 和 nums2 不会同时为空。
思路:
折半删除寻找两个有序数组第k大的元素(二分思想)
首先,我们考虑一个问题,如果只有一个有序数组,我们需要找中位数,那肯定需要判断元素是奇数个还是偶数个,如果是奇数个那最中间的就是中位数,如果是偶数个的话,那就是最中间两个数的和除以2。
那如果是两个数组,也是一样的,我们先求出两个数组长度之和。如果为奇数,就找中间的那个数,也就是 (长度之和 1)/2 。如果为偶数,那就找 长度之和/2。比如下面的 (9 +5)/2 = 7,那我们最终就是找到排列第7位的值。此时,问题其实已经转化为“找到两个数组中第k小的元素”。找到了第7位之后,第8位我们已经知道了,然后第7位和第8位的和,除以2就是我们要找的中位数(注意:这里的7和8你其实是不知道的,图中画出来,只是为了帮助理解)
现在的问题是,我们如何用二分的思想来找到中间排列第7位的数。这里有一种不太好想到的方式,是用删的方式,因为**如果我们可以把多余的数排除掉,最终剩下的那个数,是不是就是我们要找的数?**对于上面的数组,我们可以先删掉 7/2=3 个数。那这里,可以选择删上面的,也可以选择删下面的。那这里因为 i<j,所以我们选择删除上面的3个数。
(删除前)
(删除后)
由于我们已经排除掉了 3 个数字,现在对于两个数组,我们需要找到7-3=4的数字,来进行下一步运算。我们可以继续删掉4/2=2个数。我们比较i和j的值,删除小的一边。
(删除前)
(删除后)
继续上面的步骤,我们删除 2/2=1 个数。同理,比较7和6的大小,删除小的一边。删完后是下面这样:
(7和6,删除6)
不要忘记我们的目的,我们是为了找第7小的数。此时,两个数组的第一个元素,哪个小,就是我们要找的那个数。因为7<8,所以7就是我们要找的第7小的数。
这里有一点比较特殊的,如果在删除过程中,我们要删除的K/2个数,大于其中一边的数组长度,那我们就将小的一侧数组元素都删除。比如下面这个,此时7/2=3,但是下面的数组只有2个元素,我们就将它全部删除。
删完之后,此时因为只删除了2个元素,所以k变成了5。那我们只需要返回其中一边的第5个元素就ok。
整个上面的过程,完成了本题的算法架构!
class Solution {
public:
//当数据量N为偶数时,中位数为 (第N/2个数+第N/2+1个数)/2
//当数据量N为奇数时,中位数为 第N/2个数
//看看下面是如何统一处理这两种情况的
//helper函数:从nums1中下标为i开始,nums中下标为j开始,寻找第k大的数
int helper(vector<int>& nums1,int i,vector<int>& nums2,int j,int k){
if(i>=nums1.size()){
return nums2[j+k-1];
}
if(j>=nums2.size()){
return nums1[i+k-1];
}
if(k==1){
return min(nums1[i],nums2[j]);
}
//
int mid1= i+k/2-1 < nums1.size()?nums1[i+k/2-1]:INT_MAX;
int mid2= j+k/2-1 < nums2.size()?nums2[j+k/2-1]:INT_MAX;
if(mid1<mid2){
//谁小,谁左边的数都可以被淘汰
//注意陷阱:这里的k-k/2不能直接写成k/2,
//举例:k=5,k-k/2=5-2=3;而k/2=2; 表明在计算机中k-k/2!=k/2,因为k/2丢掉了一些东西
return helper(nums1,i+k/2,nums2,j,k-k/2);
}
//谁小,谁左边的数都可以被淘汰
//注意陷阱:这里的k-k/2不能直接写成k/2,
return helper(nums1,i,nums2,j+k/2,k-k/2);
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int len1=nums1.size(),len2=nums2.size();
int total=len1+len2;
//统一处理数据量是奇数和偶数的情况
int left=(total+1)/2;
int right=(total+2)/2;
return static_cast<double>(helper(nums1,0,nums2,0,left)+helper(nums1,0,nums2,0,right))/2.0;
}
};
字符串类
滑动窗口
最短覆盖子串问题
给你一个字符串 S、一个字符串 T 。请你设计一种算法,可以在 O(n) 的时间复杂度内,从字符串 S 里面找出:包含 T 所有字符的最小子串。
//必会的滑动窗口问题
string minWindow(string s, string t) {
int sl=s.size(),tl=t.size();
if(sl==0||tl==0) return "";
int left=0,right=0;
unordered_map<char,int> need,window;
int match=0;
int start=0,Min=INT_MAX;
for(int i=0;i<tl;i++){
need[t[i]]++;
}
while(right<sl){
char c1=s[right];
if(need.count(c1)){
window[c1]++;
if(window[c1]==need[c1])
match++;
}
while(match==need.size()){
//更新最短覆盖子串的头部位置和长度
if(right-left<Min){
Min=right-left+1;
start=left;
}
char c2=s[left++];
if(need.count(c2)){
if(window[c2]==need[c2])
match--;
window[c2]--;
}
}
right++;
}
//没找到覆盖子串
if(Min==INT_MAX) return "";
return s.substr(start,Min);
}
最长无重复子串问题
思路一:滑动窗口法
class Solution {
public:
//1.滑动窗口
//2.dp
int lengthOfLongestSubstring(string s) {
int N=s.size();
if(0==N) return 0;
unordered_map<char,int> window;
int left=0,right=0;
char c1,c2;
int maxLen=0;
while(right<N){
c1=s[right];
window[c1]++;
while(window[c1]>1){
c2=s[left++];
window[c2]--;
}
//更新最长无重子串
if(right-left+1>maxLen){
maxLen=right-left+1;
}
right++;
}
return maxLen;
}
};
思路二:利用hash表保存字符下标
class Solution {
public:
//1.滑动窗口法
//2.利用hash表保存字符下标
int lengthOfLongestSubstring(string s) {
int N=s.size();
if(N==0) return 0;
int lt=0,rt=0,len=0;
unordered_map<char,int> mp;
for(;rt<N;rt++){
char c=s[rt];
if(mp.count(c)&&mp[c]>=lt){
lt=mp[c]+1;
}
len=max(len,rt-lt+1);
mp[c]=rt;
}
return len;
}
};
链表类
翻转链表
从位置 m 到 n 反转链表
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
链表排序
k个一组排序
二维矩阵问题
矩阵的模拟访问
顺时针打印矩阵
顺时针存放A~Z(笔试题)
#include<bits/stdc++.h>
using namespace std;
vector<vector<char>> spiralOrder(int M,int N,int & count) {
vector<vector<char>> res(M,vector<char>(N));
int nums=M*N;
vector<vector<int>> direction{{0,1},{1,0},{0,-1},{-1,0}};
vector<vector<bool>> visited(M,vector<bool>(N));
int x=0,y=0;
int directionIndex=0;
for(int i=0;i<nums;i++){
visited[x][y]=true;
res[x][y]='A'+(count++)%26;
//仔细体会这里用下一个位置来提前判断的思想
int nextX=x+direction[directionIndex][0];
int nextY=y+direction[directionIndex][1];
//碰壁或者碰到已访问过的元素,就改变方向
if(nextX<0||nextY<0||nextX>M-1||nextY>N-1||visited[nextX][nextY])
directionIndex=(directionIndex+1)%4;
x+=direction[directionIndex][0];
y+=direction[directionIndex][1];
}
return res;
}
int main(){
int M,N;
cin>>M>>N;
int count=0;
vector<vector<char>> res=spiralOrder(M,N,count);
for(auto& i:res){
for(auto& j:i){
cout<<j<<" ";
}
cout<<endl;
}
return 0;
}
矩阵的dfs
象棋中每步采用马的日字走法,计算马可以有多少途径遍历棋盘上的所有点
题目:
总时间限制:
1000ms
内存限制:
1024kB
描述
马在中国象棋以日字形规则移动。
请编写一段程序,给定n*m大小的棋盘,以及马的初始位置(x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。
输入
第一行为整数T(T < 10),表示测试数据组数。
每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标n,m,x,y。(0<=x<=n-1,0<=y<=m-1, m < 10, n < 10)
输出
每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,0为无法遍历一次。
样例输入
1
5 4 0 0
样例输出
32
思路:典型的深度搜索,判断条件是点是否在棋盘内以及未经过。
#include<bits/stdc++.h>
using namespace std;
int cnt;
int tot=0;
int width;int height;//棋盘长宽
int map[10][10] = {0};//0表示未经过
int action_x[] = {1,2, 2, 1,-1,-2,-2,-1};
int action_y[] = {2,1,-1,-2,-2,-1, 1, 2};
//判断是否在棋盘内
bool check(int x,int y){
if(x>=0&&y>=0&&x<width&&y<height)
return true;
else
return false;
}
int dfs(int x,int y){
map[x][y] = 1;
//向八个方向搜索
for(int i=0;i<8;i++){
if(check(x+action_x[i],y+action_y[i])&&map[x+action_x[i]][y+action_y[i]]==0){
//走过的地方标记为1
map[x+action_x[i]][y+action_y[i]]=1;
//记录步数
tot++;
//
if(tot==(width*height-1)) {
cnt++;
}
dfs(x+action_x[i],y+action_y[i]);
tot--;
map[x+action_x[i]][y+action_y[i]]=0;
}
}
return cnt;
}
int main(){
int t;int x_ini; int y_ini;int ans[t];
(cin>>t).get();
for(int j=0;j<t;j++){
(cin>>width>>height>>x_ini>>y_ini).get();
if(!check(x_ini,y_ini)){
ans[j]=0;break;
}
if(width==1&&height==1){
ans[j]=1;break;
}
ans[j] = dfs(x_ini,y_ini);
cnt = 0;
memset(map,0,sizeof(map));//map清零
}
for(int i = 0;i<t;i++){
cout<<ans[i]<<endl;
}
}
象棋中A点到B点的最短路径,每步采用马的日字走法
跟上面同理
只是在以A点作为起点,dfs过程中遇到B点就更新A到B的最短路径,
#include<bits/stdc++.h>
using namespace std;
int cnt=INT_MAX;
int tot=0;
int width;int height;//棋盘长宽
int map[10][10] = {0};//0表示未经过
int action_x[] = {1,2, 2, 1,-1,-2,-2,-1};
int action_y[] = {2,1,-1,-2,-2,-1, 1, 2};
//判断是否在棋盘内
bool check(int x,int y){
if(x>=0&&y>=0&&x<width&&y<height)
return true;
else
return false;
}
int dfs(int x,int y){
map[x][y] = 1;
//向八个方向搜索
for(int i=0;i<8;i++){
if(check(x+action_x[i],y+action_y[i])&&map[x+action_x[i]][y+action_y[i]]==0){
//走过的地方标记为1
map[x+action_x[i]][y+action_y[i]]=1;
//记录步数
tot++;
//如果到达B点就更新全局最小路径
if(x+action_x[i]==Bx&&y+action_y[i]==By)) {
cnt=min(cnt,tot);
}
dfs(x+action_x[i],y+action_y[i]);
tot--;
map[x+action_x[i]][y+action_y[i]]=0;
}
}
return cnt;
}
topk问题(虾皮一面手撕)
利用堆
利用partition
k个有序链表的合并
利用小顶堆
k个有序数组的合并
出现次数最高的n个单词
三数之和(面试手撕题)
先排序,后双指针
15. 三数之和
vector<vector<int>> threeSum(vector<int>& nums) {
int N=nums.size();
if(N<3) return {};
sort(nums.begin(),nums.end());
vector<vector<int>> res;
for(int i=0;i<=N-3; ){
//最小的一个数都大于0了,下面就不必做了
if(nums[i]>0) break;
int left=i+1,right=N-1;
//-4 -1 -1 -1 0 1 2
while(left<right){
int sum=nums[i]+nums[left]+nums[right];
if(sum<0){
//避免重复,所以需要移动
while(left<right&&nums[left]==nums[++left]);
}else if(sum>0){
while(right>left&&nums[right]==nums[--right]);
}else{
res.push_back({nums[i],nums[left],nums[right]});
//移去重复项
while(left<right&&nums[left]==nums[++left]);
while(right>left&&nums[right]==nums[--right]);
}
}
//保证i的下一个也不要重复
while(i<=N-3&&nums[i]==nums[++i]);
}
return res;
}
集合所有的子集
递归解法
回溯解法
全排列
字典序第k个数字
先看看60. 第k个排列
class Solution {
public:
//参考https://blog.youkuaiyun.com/wbin233/article/details/72998375
string getPermutation(int n, int k) {
if(n<1||k<1) return "";
vector<int> fac={1,1,2,6,24,120,720,5040,40320,362880};// 阶乘
vector<int> data;
for(int i=1;i<=n;i++){
data.push_back(i);
}
k--;//第k个元素,前面只有k-1个数
string res="";
for(int i=n-1;i>=0;i--){
int a=k/fac[i];
int b=k%fac[i];
res+=to_string(data[a]);
data.erase(data.begin()+a);
k=b;
}
return res;
}
};
再看看440. 字典序的第K小数字
腾讯字节常考题
去重全排列问题
47. 全排列 II
给定一个可包含重复数字的序列,返回所有不重复的全排列。
输入: [1,1,2]
输出:
[
[1,1,2],
[1,2,1],
[2,1,1]
]
class Solution {
public:
vector<vector<int>> res;
map<int,int> mp;
void dfs(vector<int>& nums,vector<int>& track){
if(track.size()==nums.size()){
res.push_back(track);
return ;
}
//第一层只取非重复的元素
for(auto&i:mp){
//该元素i.first已经被选完了,跳过
if(mp[i.first]==0) continue;
track.push_back(i.first);
mp[i.first]--;
//下层还可以继续取到上层取过的元素
dfs(nums,track);
track.pop_back();
mp[i.first]++;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
int N=nums.size();
if(N==0) return {};
for(auto& i:nums){
mp[i]++;
}
sort(nums.begin(),nums.end());
vector<int> track;
dfs(nums,track);
return res;
}
};
组合问题
去重组合问题
40. 组合总和 II
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
class Solution {
public:
/*
一棵决策树:要去重,就先排序
*/
vector<pair<int,int>> freq;
vector<vector<int>> res;
void trackback(int target,int start,vector<int>& track)
{
if(0==target){
res.push_back(track);
return ;
}
if(target<0||freq.size()==start) return;
for(int i=start;i<freq.size();i++){
for(int j=1;j<=freq[i].second;j++){
//cout<<freq[start].first<<endl;
for(int k1=1;k1<=j;k1++) track.push_back(freq[i].first);
target=target-j*freq[i].first;
trackback(target,i+1,track);
target=target+j*freq[i].first;
for(int k2=1;k2<=j;k2++) track.pop_back();
}
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target)
{
if(candidates.size()==0) return {};
sort(candidates.begin(),candidates.end());
for(auto&i:candidates){
if(freq.empty()||i!=freq.back().first)
freq.push_back(make_pair(i,1));
else
freq.back().second++;
}
vector<int> track;
trackback(target,0,track);
return res;
}
};
背包问题
KMP算法
背诵:
1.如何求出next数组
2.如何利用next数组去查找
#include<bits/stdc++.h>
using namespace std;
//未优化前的getNext
vector<int> getNext(string Pat){
int len=Pat.size();
int i=0;
int j=-1;
vector<int> next(len+1);
next[0]=-1;
while(i<len){
if(j==-1||Pat[i]==Pat[j]){
i++;
j++;//j前移
next[i]=j;//更新next[i]
}else //j回退
j=next[j];
}
return next;
}
/*
//优化后的getNext
vector<int> getNext(string Pat){
int len=Pat.size();
int i=0;
int j=-1;
vector<int> next(len+1);
next[0]=-1;
while(i<len){
if(j==-1||Pat[i]==Pat[j]){
i++;
j++;
//不相同,则i处的真前缀位置就是j
if(Pat[i]!=Pat[j])
next[i]=j;
else//既然相同,就继续往前找真前缀
next[i]=next[j];
}else
j=next[j];
}
return next;
}
*/
//
int KMP(string& txt,int start,string& pat){
vector<int> next=getNext(pat);
int i=start;
int j=0;
int N=txt.size();
int M=pat.size();
while(i<N&&j<M){//因为末尾'\0'的存在,所以不会越界
//pat的第一个字符不匹配或者txt[i]==pat[j];
if(j==-1||txt[i]==pat[j]){
i++;
j++;
}else
{//当前字符匹配失败,进行跳转
j=next[j];
}
}
if(j==M)//匹配成功
return i-j;
return -1;
}
int main(){
string txt="Smith, where Jones had had \"had\", had had \"had had\"."" \"Had had\" had had the examiners\' approval.";
cout<<txt<<endl;
string pat="had had";
int count=0;
int res=0;
//找出txt中所有的pat子串位置
while((res=KMP(txt,res,pat))!=-1){
count++;
//下次从res+1位置开始查找
res++;
cout<<res<<" ";
}
cout<<"\n\""<<pat<<"\""<<" was found "<<count<<" times";
return 0;
}
二分查找
lower_bound()
up_bound()
写二分
动态规划系列
零钱兑换问题
零钱兑换I(建信金融笔试题)
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
带备忘录的递归解法
设dp[n]表示凑成金额j所需要的最少硬币个数
1.base case:
dp[负数]=-1;
dp[0]=0;
2.状态转移
class Solution {
public:
vector<int> memo;
int coinChange(vector<int>& coins,int amount){
if(amount<0) return -1;
if(0==amount) return 0;
//1~amount对应存到memo[0]~[amount-1]
memo.resize(amount);
return dp(coins,amount);
}
int dp(vector<int>& coins,int amount){
if(amount<0) return -1;
if(0==amount) return 0;
if(memo[amount-1]!=0) return memo[amount-1];
int res=INT_MAX;
for(int i=0;i<coins.size();i++){
int pre = dp(coins,amount-coins[i]);
// if(pre!=-1)
// res=min(res,pre+1);
if(pre!=-1&&pre<res)//避免每次调用min(res,pre+1);效率高了不少
res=pre+1;
}
//循环结束后,若res还是INT_MAX,表明无法凑出金额amount
memo[amount-1]= (res==INT_MAX?-1:res);
return memo[amount-1];
}
};
动态规划解法
设dp[n]表示凑成金额j所需要的最少硬币个数
1.base case:
dp[0]=0;
2.状态转移
int coinChange(vector<int>& coins,int amount){
if(amount<0) return -1;
vector<int> dp(amount+1,0);
for(int i=1;i<=amount;i++){
int res=INT_MAX;
for(int j=0;j<coins.size();j++){
if(i>=coins[j]){
int pre=dp[i-coins[j]];
//只有pre!=-1,pre+1才能参与全局最小的比较
if(pre!=-1)
res=min(res,pre+1);
}
}
//res==INT_MAX说明无法凑出i金额
dp[i]= res==INT_MAX?-1:res;
}
return dp[amount];
}
零钱兑换II
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
示例 2:
注意:
你可以假设:
0 <= amount (总金额) <= 5000
1 <= coin (硬币面额) <= 5000
硬币种类不超过 500 种
结果符合 32 位符号整数
直接按照完全背包求解
dp[i][j]:表示一共i类硬币,凑成总金额j的组合数
状态转移:选第i类硬币的组合数+不选第i类硬币的组合数
dp[i][j]= dp[i][j-coins[i-1]] + dp[i-1][j]; 当j>=coins[i-1]时
dp[i][j]= dp[i-1][j]; 当j<coins[i-1]时
base case
dp[0][...]=0
dp[...][0]=1
完整代码
int change(int amount, vector<int>& coins) {
if(amount==0) return 1;
int N=coins.size();
if(N==0) return 0;
vector<vector<int>> dp(N+1,vector<int>(amount+1));
//base case
for(int i=0;i<=N;i++)
dp[i][0]=1;
//状态转移
for(int i=1;i<N+1;i++){
for(int j=1;j<amount+1;j++){
if(j>=coins[i-1])
dp[i][j]=dp[i][j-coins[i-1]]+dp[i-1][j];
else
dp[i][j]=dp[i-1][j];
}
}
return dp[N][amount];
}
官方提供的解法
https://leetcode-cn.com/problems/coin-change-2/solution/ling-qian-dui-huan-ii-by-leetcode/
举一个例子:amount = 11,可用面值有 2 美分,5 美分和 10 美分。 请注意,数量是无限的
其中第二行表示硬币为0的情况
第三行表示只有面值为2美分的情况
第四行表示只有面值为2美分和5美分的情况
第五行表示有面值为2美分和5美分和10美分的情况
当前行是在上一行基础上转移而来,转移:dp[x]+=dp[x-coin]
C++代码示例:
dp[i]表示第1~k枚硬币凑成i金额的组合数
状态转移
dp[i]=dp[i]+dp[i-coin];当i>=coin时
dp[i]=dp[i];当i<coin
base case:
dp[i]={1,0,0,...,0}
int change(int amount, vector<int>& coins) {
if(amount==0) return 1;
int N=coins.size();
if(N==0) return 0;
vector<int> dp(amount+1);
dp[0]=1;
//注意下面内外循环不能交换顺序
for(auto& coin:coins){
for(int i=0;i<=amount;i++){
if(i>=coin)
dp[i]+=dp[i-coin];
}
}
return dp[amount];
}
子串和子序列问题
动态规划解最长子序列子串等一类问题
最长无重复子串问题
给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。
class Solution {
public:
//1.滑动窗口法
//2.利用hash表保存字符下标
int lengthOfLongestSubstring(string s) {
int N=s.size();
if(N==0) return 0;
int lt=0,rt=0,len=0;
unordered_map<char,int> mp;
for(;rt<N;rt++){
char c=s[rt];
if(mp.count(c)&&mp[c]>=lt){
lt=mp[c]+1;
}
len=max(len,rt-lt+1);
mp[c]=rt;
}
return len;
}
};
最长递增子串问题
最长递增子序列问题
#include <iostream>
using namespace std;
int lis(int arr[], int len) {
int longest[len];
for (int i = 0; i < len; i++) {
longest[i] = 1;
}
for (int j = 1; j < len; j++) {
//j前面所有能将arr[j]加入的情况中的最大值
for (int i = 0; i < j; i++) {
if (arr[j] > arr[i] && longest[j] < longest[i] + 1) {// 注意longest[j] 小于 longest[i]+1 不能省略
longest[j] = longest[i] + 1;// 计算以arr[j]结尾的序列的最长递增子序列的长度
}
}
}
int max = 0;
for (int j = 0; j < len; j++) {
cout << "longest[" << j << "]=" << longest[j] << endl;
if (longest[j] > max) max = longest[j];// 从longest[j]中找出最大值,即为最长长度
}
return max;
}
int main() {
int arr[] = {5, 2, 8, 6, 3, 6, 9, 7};// 测试数组
cout << "The Length of Longest Increasing Subsequence is " << lis(arr, sizeof(arr) / sizeof(arr[0])) << endl;
return 0;
}
最长回文子串(状态定义,转移,遍历方向,base case,返回结果)
状态定义
dp[i][j] 表示子串 s[i…j] 是否为回文子串,这里子串 s[i…j] 定义为左闭右闭区间,可以取到 s[i] 和 s[j]
状态转移
注意事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果,即填表顺序很重要
dp[i][j] = (s[i] == s[j]) and dp[i + 1][j - 1]
base case
当i>j;时 不用考虑;
当i==j;时 dp[i][j]=true;
最后结果
枚举i和j,找出dp[i][j]==true中 j-i+1的最大值
代码示例
class Solution {
public:
string longestPalindrome(string s) {
int N=s.size();
if(N<2) return s;
//dp[i][j]:s[i]~s[j]是否时回文串
vector<vector<int>> dp(N,vector<int>(N));
for(int i=0;i<N;i++){
dp[i][i]=true;
}
//注意这里i,j枚举时的范围和遍历方向(这里只能斜着向下,或者由底向上)
//关于最后返回的结果,也可以在循环中挑战全局最大的操作
int start=0,res=1;
for(int i=N-2;i>=0;i--){
for(int j=i+1;j<N;j++){
if(s[i]==s[j]){
//[i,j]中间没有字符了,可以直接判断
if(j-i==1)
dp[i][j]=true;
else
dp[i][j]=dp[i+1][j-1];
}else
dp[i][j]=false;
if(dp[i][j]&&j-i+1>res){
res=j-i+1;
start=i;
}
}
}
return s.substr(start,res);
}
};
最长回文子序列
516. 最长回文子序列
状态定义
dp[i][j] 表示子串 s[i…j] 的最长回文子序列的长度
状态转移
注意事项:总是先得到小子串的回文判定,然后大子串才能参考小子串的判断结果,即填表顺序很重要
if(s[i]==s[j]){
dp[i][j]=2+dp[i+1][j-1];
}else
dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
base case
当i>j;时 dp[i][j]=0;
当i==j;时 dp[i][j]=1;
最后结果
dp[0][N-1]
代码示例
class Solution {
public:
int longestPalindromeSubseq(string s) {
int N=s.size();
if(N<2) return N;
//dp[i][j]表示s[i]~s[j]之间形成的子串符的最长回文子序列的长度
vector<vector<int>> dp(N,vector<int>(N));
//base case
for(int i=0;i<N;i++){
dp[i][i]=1;
}
//填表方向很重要,必须保证小问题的解先被求出来
//这里只能选择由底向上,或者斜着向下
for(int i=N-2;i>=0;i--){
for(int j=i+1;j<N;j++){
if(s[i]==s[j]){
dp[i][j]=2+dp[i+1][j-1];
}else
dp[i][j]=max(dp[i+1][j],dp[i][j-1]);
}
}
return dp[0][N-1];
}
};
中心扩散法
动态规划法
最长公共子串问题(笔试题)
返回最长公共子串的长度
先看看暴力法
class Solution {
public:
/**
* 找出字符串中最大公共子字符串
* @param str1 string字符串 输入字符串1
* @param str2 string字符串 输入字符串2
* @return string字符串
*/
int Maxsubstr(string a,string b,string *&s)
{
unsigned int start,start1,start2;
int count=0,Max=0;
for(unsigned int i=0; a[i]!='\0'; i++)
{
for(unsigned int j=0; b[j]!='\0'; j++)
{
start1=i;
start2=j;
while(a[start1]==b[start2] && start1<a.length() && start2<b.length())
{
start1++;
start2++;
count++;
}
if(count>Max)
{
start = i;
Max = count;
}
count=0;
}
}
//保存字符串
s=new string[Max+1];
for(int i=0; i<Max; i++)
{
s[i]=a[i+start];
}
s[Max]='\0';
return Max;
}
string GetCommon(string str1, string str2) {
// write code here
string *str;
int length=Maxsubstr(str1,str2,str);
string res="";
for(int i=0; i<length; i++)
{
res+=str[i];
}
return res;
}
};
返回最长公共子串本身
再看看动态规划解法
dp[i][j] 的含义是: 分别以s1[i-1] 和 s2[j-1] 结尾的两个字符串的最长公共子串, dp[i][j]
定义 base case:
dp[0][…] 和 dp[…][0] 都应该初始化为 0,
状态转移:
if(str1[i-1]==str2[j-1]
dp[i][j]=1+dp[i-1][j-1];
else
dp[i][j]=0;//注意这里为0,看看dp[i][j]的含义
#include<bits/stdc++.h>
using namespace std;
/**
找出两个字符串的公共子串(动态规划)
dp[i][j]: 分别以s1[i-1] 和 s2[j-1] **结尾的两个字符串**的最长公共子串, dp[i][j]
状态转移
if(str1[i-1]==str2[j-1]
dp[i][j]=1+dp[i-1][j-1];
else
dp[i][j]=0;//注意这里为0,看看dp[i][j]的含义
*/
string maxCommonSubstr(string& str1, string& str2) {
int len1=str1.size(),len2=str2.size();
if(len1==0||len2==0) return "";
vector<vector<int>> dp(len1+1,vector<int>(len2+1));
// 循环遍历
for(int i = 1; i <= len1; i++) {
for(int j = 1; j <= len2; j ++) {
// 如果两个字符相同
if (str1[i - 1] == str2[j - 1]) {
dp[i][j]=1+dp[i-1][j-1];
}
// 如果两个字符不同
else {
dp[i][j]=0;
}
}
}
//如何获取最长的公共子串
//找出dp[i][j]中的最大值
string res="";
int Max=0;
int Max_i,Max_j;
for(int i=1;i<=len1;i++){
for(int j=1;j<=len2;j++){
if(dp[i][j]>Max){
Max_i=i;
Max_j=j;
Max=dp[i][j];
}
}
}
res+=str1.substr(Max_i-Max,Max);
return res;
}
int main() {
string str1="ABCDAB";
string str2="ABCAB";
string res=maxCommonSubstr(str1,str2);
cout<<res<<endl;
return 0;
}
最长公共子序列问题
返回最长公共子序列的长度
dp[i][j] 的含义是: 对于 s1[1…i] 和 s2[1…j] ,它们的 LCS ⻓度是 dp[i][j]
定义 base case:
dp[0][…] 和 dp[…][0] 都应该初始化为 0,
状态转移:
if(str1[i-1]==str2[j-1]
dp[i][j]=1+dp[i-1][j-1];
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
返回最长公共子序列的字符串
关注点:如何获取最长的公共子序列
从尾部向前扫描,根据dp[i][j]的值来决定是i–,还是j–,还是i–;j–;
#include<bits/stdc++.h>
using namespace std;
/**
找出 两个 字符串 的公共 子序列(动态规划)
*/
string maxPublicSubSequenceTwo(string& str1, string& str2) {
int len1=str1.size(),len2=str2.size();
if(len1==0||len2==0) return "";
vector<vector<int>> dp(len1+1,vector<int>(len2+1));
// 循环 遍历
for(int i = 1; i <= len1; i++) {
for(int j = 1; j <= len2; j ++) {
// 如果 两个字符 相同
if (str1[i - 1] == str2[j - 1]) {
dp[i][j]=1+dp[i-1][j-1];
}
// 如果 两个 字符 不同
else {
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
}
//如何获取最长的公共子序列
//从尾部向前扫描,根据dp[i][j]的值来决定是i--,还是j--,还是i--;j--;
string res="";
int i, j ;
for (i = len1, j = len2; i >= 1 && j >= 1;) {
if (str1[i - 1] == str2[j - 1]) {
res.insert(res.begin(),str1[i-1]);
i --;
j --;
}
else {
if (dp[i][j - 1] > dp[i - 1][j])
j--;
else
i --;
}
}
return res;
}
int main() {
string str1="ABCBDAB";
string str2="BDCABA";
string res=maxPublicSubSequenceTwo(str1,str2);
cout<<res<<endl;
return 0;
}
背包问题
状态定义:
dp[i][j] 前i个物品,背包容量为j的情况下,XXX
状态转移:
base case:
最后返回的结果:
01 背包问题
N个物品,每个物品只有一个(价值value[] ,体积weight[]),背包容量为j的情况下,能够装下的最大价值
状态定义:
dp[i][j] 前i个物品,每个物品只有一个,背包容量为j的情况下,能够装下的最大价值
状态转移:
if(j-weight[i-1]>=0)
dp[i][j]= max(dp[i-1][j],dp[i-1][j-weight[i-1]+value[i-1]])
else
dp[i][j]=dp[i-1][j]
base case:
dp[0][]=0;
dp[][0]=0;
最后返回的结果:
N个物品,容量为W时
dp[N][W]
完全背包问题
- 一种问法
N个物品,每个物品有无数个(价值value[] ,体积weight[]),背包容量为j的情况下,有多少种方法可以装满背包
动态规划解法
状态定义:
dp[i][j] 前i个物品,每个物品有无数个,背包容量为j的情况下,有多少种方法可以装满背包
状态转移:
if(j-weight[i-1]>=0)
dp[i][j]= max(dp[i-1][j],dp[i][j-weight[i-1]])
else
dp[i][j]=dp[i-1][j]
base case:
dp[0][]=0;
dp[][0]=1;
最后返回的结果:
N个物品,容量为W时
dp[N][W]
- 另一种问法
N个物品,每个物品有无数个(价值value[] ,体积weight[]),背包容量为j的情况下,背包可以装载的最大价值为多少
递归解法
dp(i,t)表示前i种物品放入一个容量为t的背包获得的最大价值。
那么对于第i种物品,我们有k种选择,0 <= k * V[i] <= t,所以递推表达式为:
//第i类物品可以有k种选择,约束0 <= k * V[i] <= t,从中选择最优的那个
(0 <= k * V[i] <= t)
dp(i,t) = max{dp(i-1, t - V[i] * k) + P[i] * k};
代码示例如下:
int dp(int i, int t){
int result = 0;
if (i == 0 || t == 0){
// 初始条件
result = 0;
} else if(V[i] > t){
// 装不下该物体
result = ks(i-1, t);
} else {
// 可以装下
// 取k个物品i,取其中使得总价值最大的k
for (int k = 0; k * V[i] <= t; k++){
int tmp2 = dp(i-1, t - V[i] * k) + P[i] * k;
if (tmp2 > result){
result = tmp2;
}
}
}
return result;
}
子集背包问题
状态定义:
dp[i][j] 为i个物品,背包容量为j的情况下,是否能够恰好把背包装满
状态转移:
if(j-weight[i-1]>=0)
dp[i][j]= dp[i-1][j]||dp[i][j-weight[i-1]])
else
dp[i][j]=dp[i-1][j]
base case:
dp[0][0]=true;
dp[0][]=false;
dp[][0]=true;
最后返回的结果:
N个物品,容量为W时
dp[N][W]
多重背包问题
有N种物品和一个容量为T的背包,
第i种物品最多有M[i]件可用,价值为P[i],体积为V[i],
求解:选哪些物品放入背包,可以使得这些物品的价值最大,并且体积总和不超过背包容量。
对比一下完全背包,其实只是多了一个限制条件,完全背包问题中,物品可以选择任意多件,只要你装得下,装多少件都行。
但多重背包就不一样了,每种物品都有指定的数量限制,所以不是你想装,就能一直装的。
举个栗子:有A、B、C三种物品,相应的数量、价格和占用空间如下图:
- 递归法
还是用之前的套路,我们先来用递归把这个问题解决一次。
用ks(i,t)表示前i种物品放入一个容量为t的背包获得的最大价值,那么对于第i种物品,我们有k种选择,0 <= k <= M[i] && 0 <= k * V[i] <= t,即可以选择0、1、2…M[i]个第i种物品,所以递推表达式为:
(0 <= k <= M[i] && 0 <= k * V[i] <= t)
ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k};
同时,ks(0,t)=0;ks(i,0)=0;
对比一下完全背包的递推关系式:
(0 <= k * V[i] <= t)
ks(i,t) = max{ks(i-1, t - V[i] * k) + P[i] * k};
vector<int> P={0,2,3,4};
vector<int> V={0,3,4,5};
vector<int> M={0,4,3,2};
int T=15;
//dp(i,t)表示前i种物品放入一个容量为t的背包获得的最大价值
int dp(int i,int t){
int result=0;
// 初始条件
if(i==0||t==0) return 0;
else if(V[i]>t){// 装不下该物品
return dp(i-1,t);
}else{
// 可以装下
// 取k个物品i,取其中使得总价值最大的k
for(int k=0;k<=M[i]&&k*V[i]<=t;k++){
int tmp2=dp(i-1,t-V[i]*k)+P[i]*k;
if(tmp2>result){
result=tmp2;
}
}
}
return result;
}
- 动态规划法
dp[i][j]:表示将前i类物品装入容量为j的背包,获得的最大价值。
vector<int> P={2,3,4};
vector<int> V={3,4,5};
vector<int> M={4,3,2};
int T=15;
void MultiPack(vector<int>&P,vector<int>&V,vector<int>&M,int T){
vector<vector<int>> dp(P.size()+1,vector<int>(T+1));
//base case: dp[0][]==0;dp[][0]=0;
for(int i=1;i<=P.size();i++){
for(int j=0;j<=T;j++){
//k:表示第i个物体可以选的个数范围 约束条件k<=M[i]&&k*V[i]<=j
for(int k=0;k<=M[i]&&k*V[i]<=j;k++){
//选择最优的那个
dp[i][j]=max(dp[i][j],dp[i-1][j-k*V[i-1]]+k*P[i-1]);
}
}
}
return dp[P.size()][T];
}
股票问题
labuladong 框架
每天都有三种「选择」 : 买⼊、 卖出、 ⽆操作
这个问题的「状态」 有三个, 第⼀个是天数, 第⼆个是允许交易的最⼤次数, 第三个是当前的持有状态(即之前说的rest 的状态, 我们不妨⽤ 1 表⽰持有, 0 表⽰没有持有)
状态转移:
base case:
121. 买卖股票的最佳时机I
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
如果你最多只允许完成一笔交易(即买入和卖出一支股票一次),设计一个算法来计算你所能获取的最大利润。
注意:你不能在买入股票前卖出股票。
示例 1:
输入: [7,1,5,3,6,4]
输出: 5
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。
示例 2:
输入: [7,6,4,3,1]
输出: 0
解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
方法1:一次遍历
我们只需要遍历价格数组一遍,记录历史最低点,然后在每一天考虑这么一个问题:如果我是在历史最低点买进的,那么我今天卖出能赚多少钱?当考虑完所有天数之时,我们就得到了最好的答案。
//一次遍历,更新当前位置i前面部分的最小值min,同时判断是否更新全局最大值
int maxProfit(vector<int>& prices) {
int N=prices.size();
if(N<2) return 0;
int Min=prices[0];
int Profit=INT_MIN;
for(int i=1;i<N;i++){
if(prices[i-1]<Min)
Min=prices[i-1];
if(prices[i]-Min>Profit)
Profit=prices[i]-Min;
}
//Profit<=0;说明不能进行交易
if(Profit<=0) return 0;
return Profit;
}
方法2:动态规划思想
状态定义
dp[i][k]表示第i天,状态为k时(k=0:未持有股票,k=1:持有股票)获得的最大利润
转移方程:
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + price[i])
dp[i][1]表示前 i天,持有股票状态下的最大利润
dp[i][1] = max(dp[i-1][1], 0 - price[i])
而我们需要的答案就是,
前 n天没有持有股票状态下的最大利润:dp[n][0]
你可能注意到,
第二个状态转移方程,正常来讲应该写成:dp[i][1] = max(dp[i-1][1], dp[i][0] - price[i]),但上面 maxmax 的第二个参数却是 0 - price[i] 。
这是因为题目要求股票只能买卖一次,0表示未进行股票交易时的初始金额,而 dp[i-1][0]表示前 i - 1天未持有股票状态下的最大利润,但前 i - 1天可能完成了多次股票交易,所以不满足条件。
代码
class Solution {
public:
//dp[i][k]表示第i天,状态为k时(k=0:未持有股票,k=1:持有股票)获得的最大利润
int maxProfit(vector<int>& prices) {
int N=prices.size();
if(N<2) return 0;
vector<vector<int>> dp(N+1,vector<int>(2));
dp[0][0]=0;
dp[0][1]=INT_MIN;
dp[1][0]=0;
dp[1][1]=-prices[0];
for(int i=2;i<=N;i++){
//第i天未持有股票:
//可能是第i-1天持有股票,然后第i天卖出
//也可能是第i-1天未持有股票,然后第i天,啥也没干,还是未持有
dp[i][0]=max(dp[i-1][1]+prices[i-1],dp[i-1][0]);
//第i天持有股票:
//可能是第i-1天持有股票,然后第i天,啥也没干,还是持有
//也可能是第i-1天未持有股票,然后第i天买入,注意这里只能有一次交易机会,
//所以第i天买入股票时,最大利润直接是0-prices[i-1]
//而不是dp[i-1][0]-prices[i-1];
dp[i][1]=max(dp[i-1][1],0-prices[i-1]);
}
return dp[N][0];
}
};
122. 买卖股票的最佳时机 II
给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [7,1,5,3,6,4]
输出: 7
解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
方法1:峰谷法
求出连续的峰和谷的差值,然后求和
T
o
t
a
l
P
r
o
f
i
t
=
s
u
m
(
h
e
i
g
h
t
(
p
e
a
k
i
)
−
h
e
i
g
h
t
(
v
a
l
l
e
y
i
)
)
TotalProfit=sum(height(peak_i)-height(valley_i))
TotalProfit=sum(height(peaki)−height(valleyi))
int maxProfit(vector<int> prices){
int i=0;
int valley=prices[0];
int peak=prices[0];
int maxprofit=0;
while(i<prices.size()-1){
while(i<prices.size()-1&&prices[i]>=prices[i+1])
i++;
valley=prices[i];
while(i<prices.size()-1&&prices[i]<=prices[i+1])
i++;
peak=prices[i];
maxprofit+=peak-valley;
}
return maxprofit;
}
方法2:方法1的改进
我们可以简单地继续在斜坡上爬升并持续增加从连续交易中获得的利润,而不是在谷之后寻找每个峰值。
例如[1, 7, 2, 3, 6, 7, 6, 7] 与此数组对应的图形是:
从上图中,我们可以观察到 A+B+C 的和等于差值 D 所对应的连续峰和谷的高度之差。
int maxProfit(vector<int> prices){
int maxprofit=0;
for(int i=1;i<prices.size();i++){
if(prices[i]>prices[i-1])
maxprofit+=prices[i]-prices[i-1];
}
return maxprofit;
}
方法3:暴力搜索
根据题意:因为不限制交易次数,在每一天,我就可以根据当前是否持有股票选择相应的操作。“暴力搜索” 也叫 “回溯搜索”、“回溯法”,首先画出树形图。
class Solution {
public:
//暴力回溯
int res=0;
/*
prices:股票价格数组
depth:决策树深度
status:当前状态, 0:当前未持有股票;1:当前持有股票
profit:当达当前深度时的收入
*/
void trackback(vector<int>& prices,int depth,int status,int profit){
if(depth==prices.size()-1){
//最大利润肯定出现在最后未持有股票的情况下
if(status==0&&profit>res)
res=profit;
return ;
}
//status==0当前未持有股票
if(status==0){
//可以买入股票,花费prices[depth+1],收入变为profit-prices[depth+1]
trackback(prices,depth+1,1,profit-prices[depth+1]);
//可以不买入股票,不花钱,收入还是profit
trackback(prices,depth+1,0,profit);
}
//status==1当前持有股票
else{
//可以卖出股票,获得prices[depth+1],收入变为profit+prices[depth+1]
trackback(prices,depth+1,0,profit+prices[depth+1]);
//可以不卖出股票,收入还是profit
trackback(prices,depth+1,1,profit);
}
}
int maxProfit(vector<int>& prices) {
int N=prices.size();
if(N<2) return 0;
int profit=0;
//第一天可以不买入股票
trackback(prices,0,0,profit);
//第一天也可以买入股票
trackback(prices,0,1,profit-prices[0]);
return res;
}
};
暴力回溯会超时
方法4:动态规划
来自:
https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/solution/tan-xin-suan-fa-by-liweiwei1419-2/
第 1 步:定义状态
状态 dp[i][j] 定义如下
第一维 i 表示索引为 i 的那一天(具有前缀性质,即考虑了之前天数的收益)能获得的最大利润;
第二维 j 表示索引为 i 的那一天是持有股票,还是持有现金。这里 0 表示持有现金(cash),1 表示持有股票(stock)。
第 2 步:思考状态转移方程
状态从持有现金(cash)开始,到最后一天我们关心的状态依然是持有现金(cash);
每一天状态可以转移,也可以不动。状态转移用下图表示:
(状态转移方程写在代码中)
说明:
因为不限制交易次数,除了最后一天,每一天的状态可能不变化,也可能转移;
写代码的时候,可以不用对最后一天单独处理,输出最后一天,状态为 0 的时候的值即可。
第 3 步:确定起始
起始的时候:
如果什么都不做,dp[0][0] = 0;
如果买入股票,当前收益是负数,即 dp[0][1] = -prices[i];
第 4 步:确定终止
终止的时候,上面也分析了,输出 dp[len - 1][0],因为一定有 dp[len - 1][0] > dp[len - 1][1]。
int maxProfit(vector<int>& prices) {
int N=prices.size();
if(N<2) return 0;
vector<vector<int>> dp(N,vector<int>(2));
//base case
dp[0][0]=0;
dp[0][1]=-prices[0];
//填表
for(int i=1;i<N;i++){
dp[i][0]=max(dp[i-1][0],dp[i-1][1]+prices[i]);
dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]);
}
//返回结果
return max(dp[N-1][0],dp[N-1][1]);
}
123. 买卖股票的最佳时机 III
https://leetcode-cn.com/circle/article/qiAgHn/
123. 买卖股票的最佳时机 III
给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔交易。
注意: 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
示例 1:
输入: [3,3,5,0,0,3,1,4]
输出: 6
解释: 在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。
示例 2:
输入: [1,2,3,4,5]
输出: 4
解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。
注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。
因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。
动态规划思路:
我们可以对「状态」进行穷举。我们具体到每一天,看看总共有几种可能的「状态」,再找出每个「状态」对应的「选择」。我们要穷举所有「状态」,穷举的目的是根据对应的「选择」更新状态。
for 状态1 in 状态1的所有取值:
for 状态2 in 状态2的所有取值:
for ...
dp[状态1][状态2][...] = 择优(选择1,选择2...)
每天都有三种「选择」:买入、卖出、无操作,我们用 buy, sell, rest 表示这三种选择。但问题是,并不是每天都可以任意选择这三种选择的,因为 sell 必须在 buy 之后,buy 必须在 sell 之后。那么 rest 操作还应该分两种状态,一种是 buy 之后的 rest(持有了股票),一种是 sell 之后的 rest(没有持有股票)。而且别忘了,我们还有交易次数 k 的限制,就是说你 buy 还只能在 k > 0 的前提下操作。
这个问题的「状态」有三个,第一个是天数,第二个是允许交易的最大次数,第三个是当前的持有状态(即之前说的 rest 的状态,我们不妨用 1 表示持有,0 表示没有持有)。然后我们用一个三维数组就可以装下这几种状态的全部组合:
dp[i][k][0 or 1]
0 <= i <= n-1, 1 <= k <= K
n 为天数,大 K 为最多交易数
此问题共 n × K × 2 种状态,全部穷举就能搞定。
for 0 <= i < n:
for 1 <= k <= K:
for s in {0, 1}:
dp[i][k][s] = max(buy, sell, rest)
状态转移图
根据这个图,我们来写一下状态转移方程:
dp[i][k][0] = max(dp[i-1][k][0], dp[i-1][k][1] + prices[i])
max( 选择 rest , 选择 sell )
解释:今天我没有持有股票,有两种可能:
要么是我昨天就没有持有,然后今天选择 rest,所以我今天还是没有持有;
要么是我昨天持有股票,但是今天我 sell 了,所以我今天没有持有股票了。
dp[i][k][1] = max(dp[i-1][k][1], dp[i-1][k-1][0] - prices[i])
max( 选择 rest , 选择 buy )
解释:今天我持有着股票,有两种可能:
要么我昨天就持有着股票,然后今天选择 rest,所以我今天还持有着股票;
要么我昨天本没有持有,但今天我选择 buy,所以今天我就持有股票了。
如果 buy,就要从利润中减去 prices[i],如果 sell,就要给利润增加 prices[i]。今天的最大利润就是这两种可能选择中较大的那个。而且注意 k 的限制,我们在选择 buy 的时候,把 k 减小了 1,很好理解吧,当然你也可以在 sell 的时候减 1,一样的。 (只能在买入的时候减1才不会出错)
base case
关于这里的负无穷,得仔细想想为啥?
dp[-1][k][0] = 0
解释:因为 i 是从 0 开始的,所以 i = -1 意味着还没有开始,这时候的利润当然是 0 。
dp[-1][k][1] = -infinity
解释:还没开始的时候,是不可能持有股票的,用负无穷表示这种不可能。
dp[i][0][0] = 0
解释:因为 k 是从 1 开始的,所以 k = 0 意味着根本不允许交易,这时候利润当然是 0 。
dp[i][0][1] = -infinity
解释:不允许交易的情况下,是不可能持有股票的,用负无穷表示这种不可能。
一种写法:
class Solution {
public:
//动态规划
//dp[i][j][k]:表示第i天,最多还能完成j笔交易,状态为k的时候的最大利润
//k:0表示未持有股票,1表示持有股票
//状态转移:
//dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1])
//dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1])
//base case
//dp[0][j][0]=0
//dp[0][j][1]=INT_MIN 用负无穷表示不可能
int maxProfit(vector<int>& prices) {
int N=prices.size();
if(N<2) return 0;
int j=2;
//三维dp
vector<vector<vector<int>>> dp(N+1,vector<vector<int>>(j+1,vector<int>(2)));
for(int i=0;i<=N;i++){
for(int j=1;j<=2;j++){
if(i==0){
//处理base case
dp[i][j][0]=0;
dp[i][j][1]=INT_MIN;
}else if(j==0){
dp[i][0][0]=0;
dp[i][0][1]=INT_MIN;
}else{
//关于在第i天买入股票时dp[i-1][j-1][0]-prices[i-1],
//因为必须留一次给第i天,才能买入,所以第i-1天未持有股票时的可交易次数为j-1次
dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1]);
dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1]);
}
}
}
return dp[N][2][0];
}
};
另一种写法
class Solution {
public:
//动态规划
//dp[i][j][k]:表示第i天,最多还能完成j笔交易,状态为k的时候的最大利润
//k:0表示未持有股票,1表示持有股票
//状态转移:
//dp[i][j][0]=max(dp[i-1][j][0],dp[i-1][j][1]+prices[i-1])
//dp[i][j][1]=max(dp[i-1][j][1],dp[i-1][j-1][0]-prices[i-1])
//base case
//dp[0][j][0]=0
//dp[0][j][1]=INT_MIN 用负无穷表示不可能
int maxProfit(vector<int>& prices) {
int N=prices.size();
if(N<2) return 0;
int j=2;
//三维dp
vector<vector<vector<int>>> dp(N+1,vector<vector<int>>(j+1,vector<int>(2)));
//处理base case
dp[0][1][0]=0;
dp[0][1][1]=INT_MIN;
dp[0][2][0]=0;
dp[0][2][1]=INT_MIN;
for(int i=1;i<=N;i++){
//dp[i][0][0]已经是0了,不用管
//关于这里的j-1,在买入股票的时候j减1
dp[i][1][1]=max(dp[i-1][1][1],dp[i-1][0][0]-prices[i-1]);
dp[i][1][0]=max(dp[i-1][1][0],dp[i-1][1][1]+prices[i-1]);
dp[i][2][1]=max(dp[i-1][2][1],dp[i-1][1][0]-prices[i-1]);
dp[i][2][0]=max(dp[i-1][2][0],dp[i-1][2][1]+prices[i-1]);
}
return dp[N][2][0];
}
};
再进行状态压缩,降低空间复杂度
class Solution {
public:
int maxProfit(vector<int>& prices) {
int N=prices.size();
if(N<2) return 0;
int dp10=0;
int dp11=INT_MIN;
int dp20=0;
int dp21=INT_MIN;
for(int i=1;i<=N;i++){
dp10=max(dp10,dp11+prices[i-1]);
dp11=max(dp11,-prices[i-1]);
dp20=max(dp20,dp21+prices[i-1]);
dp21=max(dp21,dp10-prices[i-1]);
}
return dp20;
}
};
打家劫舍系列
区间dp问题
石子合并问题
Description
一条直线上摆放着一行共n堆的石子。现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆石子数记为该次合并的得分。
请编辑计算出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分。
Input
输入有多组测试数据。
每组第一行为n(n<=100),表示有n堆石子,。
二行为n个用空格隔开的整数,依次表示这n堆石子的石子数量ai(0<ai<=100)
Output
每组测试数据输出有一行。输出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分。 中间用空格分开。
Sample Input
3
1 2 3
Sample Output
9 11
思路:区间DP问题
定义dp[i][j]为将第i堆到第j堆石子合并获得的最小得分
状态转移
dp[i][j]=min{dp[i][k]+dp[k+1][j]}+cost[i][j]
#include<bits/stdc++.h>
using namespace std;
const int maxn=110;
/*
数组a保存输入的数据
数组dp保存求最小得分时的动态规划状态转移数据
数组sum用于保存前缀和
数组xp保存求最大得分时的动态规划状态转移数据
*/
int n,a[maxn],dp[maxn][maxn],sum[maxn],xp[maxn][maxn];
int main(){
while(cin>>n){
memset(xp,0,sizeof(xp));
memset(sum,0,sizeof(sum));
memset(dp,0x3f3f3f3f,sizeof(dp));
for(int i=1;i<=n;i++){
dp[i][i]=0;
xp[i][i]=0;
cin>>a[i];
sum[i]=sum[i-1]+a[i];
}
//枚举长度
for(int len=1;len<=n;len++){
//枚举起点,len是左闭右闭区间的长度
for(int i=1;i+len-1<=n;i++){
//j为终点
int j=i+len-1;
//枚举分割点
for(int k=i;k<j;k++){
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1]);
xp[i][j]=max(xp[i][j],xp[i][k]+xp[k+1][j]+sum[j]-sum[i-1]);
}
}
}
cout<<dp[1][n]<<" "<<xp[1][n];
}
return 0;
}
87.扰乱字符串问题
参考:
简单易懂的dp思路,逐行解释
详细通俗的思路分析,多解法
递归思路
这道题很容易想到用递归的思想去解,假如两个字符串 great 和 rgeat。考虑其中的一种切割方式。
第 1 种情况:S1 切割为两部分,然后进行若干步切割交换,最后判断两个子树分别是否能变成 S2 的两部分。
第 2 种情况:S1 切割并且交换为两部分,然后进行若干步切割交换,最后判断两个子树是否能变成 S2 的两部分。
//带有备忘录的递归解法
class Solution {
public:
unordered_map<string,bool> mp;
bool helper(string& s1,string& s2){
if(s1.size()!=s2.size()) {
mp[s1+s2]=false;
return false;
}
if(s1==s2){
mp[s1+s2]=true;
return true;
}
//判断字符个数不一致,直接返回false
//做了这个判断,效率提高不少
int c[26] = {0};
for(int i=0;i<s1.length();i++)
{
c[s1[i]-'a']++;
c[s2[i]-'a']--;
}
for(int i=0;i<26;i++)
if(c[i] != 0){
mp[s1+s2]=false;
return false;
}
if(mp.count(s1+s2)){
return mp[s1+s2];
}else{
//递归思路
//枚举切割点k
for(int k=1;k<s1.size();k++){
//只要有一个切割点满足了,就返回true;
//情况1:s1的第一步只是切割,然后递归处理左右子串与s2的关系
if(isScramble(s1.substr(0,k),s2.substr(0,k))
&&isScramble(s1.substr(k,s1.size()-k),s2.substr(k,s2.size()-k))){
mp[s1+s2]=true;
return true;
}
//情况2:s1的第一步切割后,并且交换,然后递归处理左右子串与s2的关系
if(isScramble(s1.substr(k,s1.size()-k),s2.substr(0,s2.size()-k))
&&isScramble(s1.substr(0,k),s2.substr(s2.size()-k,k))){
mp[s1+s2]=true;
return true;
}
}
mp[s1+s2]=false;
return false;
}
}
bool isScramble(string s1, string s2) {
if(s1.size()!=s2.size()) return false;
if(s1==s2) return true;
return helper(s1,s2);
}
};
动态规划思路
初步分析
给定两个字符串 T 和 S,假设 T 是由 S 变换而来
如果 T 和 S 长度不一样,必定不能变来
如果长度一样,顶层字符串 S 能够划分为
S
1
S_1
S1和
S
2
S_2
S2,同样字符串 TT 也能够划分为 T_1和
T
2
T_2
T2
情况一:没交换,
S
1
=
=
>
T
1
S_1 ==> T_1
S1==>T1,
S
2
=
=
>
T
2
S_2 ==> T_2
S2==>T2
情况二:交换了,
S
1
=
=
>
T
2
S_1 ==> T_2
S1==>T2,
S
2
=
=
>
T
1
S_2 ==> T_1
S2==>T1
子问题就是分别讨论两种情况,
T
1
T_1
T1是否由
S
1
S_1
S1 变来,
T
2
T_2
T2是否由
S
2
S_2
S2变来,或
T
1
T_1
T1是否由
S
2
S_2
S2变来,
T
2
T_2
T2是否由
S
1
S_1
S1变来。
状态定义
dp[i][j][k][h] 表示 T[k…h]是否由 S[i…j]变来。
由于变换必须长度是一样的,因此这边有个关系 j - i = h - k,可以把四维数组降成三维。
dp[i][j][len]表示从字符串 S中 i开始长度为 len的字符串是否能变换为从字符串 T中 j开始长度为len的字符串
转移方程
base case
最后的结果
dp[0][0][N]
代码如下
class Solution {
public:
//区间dp
//dp[i][j][k][h]表示s1[i]~s1[j]与s2[k]~s2[h]是否是满足,因为j-i==h-k,所以可以减少一个状态
//dp[i][j][len]表示已s[i]开头,长度len的字符串与已s2[j]开头,长度len的字符串是否满足
//状态转移
/*枚举len的分割点,对于每一个长度len,只要有一个分割点满足,dp[i][j][len]就为true
for(int len=2;len<s1.size();len++)
for(int k=1;k<len;k++ )
//第一种情况 仅分割
if(dp[i][j][k]&&dp[i+k][j+k][len-k])
dp[i][j][len]=true;
//第二种情况,分割后交换
if(dp[i][j+len-k][k]&&dp[i+k][j][len-k]){
dp[i][j][len]=true;
*/
//base case
//dp[i][j][0]=true;
//dp[i][j][1]=(s1[i]==s2[j])
//返回结果为
//dp[0][0][s1.size()]
bool isScramble(string s1, string s2) {
if(s1.size()!=s2.size()) return false;
if(s1==s2) return true;
//当s1.size()!=s2.size(),这里还可以做一个检查,效率高了不少
//如果s1,s2中每一个字符出现的次数不一样,直接放回false
int c[26]={0};
for(int i=0;i<s1.size();i++){
c[s1[i]-'a']++;
c[s2[i]-'a']--;
}
for(int i=0;i<26;i++){
if(c[i]!=0) return false;
}
int N=s1.size();
vector<vector<vector<bool>>> dp(N,vector<vector<bool>>(N,vector<bool>(N+1)));
//base case
for(int i=0;i<N;i++){
for(int j=0;j<N;j++){
dp[i][j][0]=true;
dp[i][j][1]=(s1[i]==s2[j]);
}
}
//状态转移
for(int len=2;len<=N;len++){
for(int i=0;i<=N-len;i++){
for(int j=0;j<=N-len;j++){
//k表示分割给左边的区间长度
//只要有一个分割满足,dp[i][j][len]=true;
for(int k=1;k<len;k++){
//第一种情况 仅分割
if(dp[i][j][k]&&dp[i+k][j+k][len-k]){
dp[i][j][len]=true;
break;
}
//第二种情况,分割后交换
if(dp[i][j+len-k][k]&&dp[i+k][j][len-k]){
dp[i][j][len]=true;
break;
}
}
}
}
}
return dp[0][0][N];
}
};
741. 摘樱桃
741. 摘樱桃
一个N x N的网格(grid) 代表了一块樱桃地,每个格子由以下三种数字的一种来表示:
- 0 表示这个格子是空的,所以你可以穿过它。
- 1 表示这个格子里装着一个樱桃,你可以摘到樱桃然后穿过它。
- -1 表示这个格子里有荆棘,挡着你的路。
你的任务是在遵守下列规则的情况下,尽可能的摘到最多樱桃:
- 从位置 (0, 0) 出发,最后到达 (N-1, N-1) ,只能向下或向右走,并且只能穿越有效的格子(即只可以穿过值为0或者1的格子);
- 当到达 (N-1, N-1) 后,你要继续走,直到返回到 (0, 0) ,只能向上或向左走,并且只能穿越有效的格子;
- 当你经过一个格子且这个格子包含一个樱桃时,你将摘到樱桃并且这个格子会变成空的(值变为0);
- 如果在 (0, 0) 和 (N-1, N-1) 之间不存在一条可经过的路径,则没有任何一个樱桃能被摘到。
变形题如下
单调栈
矩形的最大面积
84. 柱状图中最大的矩形
给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。
求在该柱状图中,能够勾勒出来的矩形的最大面积。
图中阴影部分为所能勾勒出的最大矩形面积,其面积为 10 个单位。
示例:
输入: [2,1,5,6,2,3]
输出: 10
思路一:枚举高
思路二:单调栈
//1.枚举高,确定高的情况下,左右扩展使得宽最大,可以先遍历,得到left[i],right[i]表示i号柱子左边第一个小于它的下标,右边第一个小于它的下标
//2.单调栈,从左往右遍历,大于栈顶元素,则入栈,小于,则弹出栈中元素直到大于栈顶,每次都填充left[i]
// 从右往左遍历,大于栈顶元素,则入栈,小于,则弹出栈中元素直到大于栈顶,每次都填充right[i]
//3. 2中分两次填充left数组和right数组的操作可以一次完成
class Solution {
public:
int largestRectangleArea(vector<int>& heights) {
int N=heights.size();
if(N==0) return 0;
if(N==1) return heights[0];
vector<int> left(heights.size()),right(heights.size());
stack<int> stk;
//1.从左到右,获取left
for(int i=0;i<heights.size();i++){
while(!stk.empty()&&heights[i]<=heights[stk.top()]){
stk.pop();
}
if(stk.empty()) {
left[i]=-1;
stk.push(i);
}else{
left[i]=stk.top();
stk.push(i);
}
}
//2.从右到左,获取right
stk=stack<int>();
for(int i=N-1;i>=0;i--){
while(!stk.empty()&&heights[i]<=heights[stk.top()]){
stk.pop();
}
if(stk.empty()){
right[i]=N;
stk.push(i);
}else{
right[i]=stk.top();
stk.push(i);
}
}
//3.利用left,right,获取最大矩形面积
int res=0;
for(int i=0;i<N;i++){
if((right[i]-left[i]-1)*heights[i]>res)
res=(right[i]-left[i]-1)*heights[i];
}
return res;
}
};
85. 最大矩形
85. 最大矩形
给定一个仅包含 0 和 1 的二维二进制矩阵,找出只包含 1 的最大矩形,并返回其面积。
示例:
思路一:
枚举行,对每一行到第一行,形成的柱状图,计算柱状图中的最大矩形面积,用上面84题的方法,然后更新全局最大值即可
class Solution {
public:
//利用84题的方法解决这个题
int largestRectangleArea(vector<int>& heights) {
int N=heights.size();
if(N==0) return 0;
if(N==1) return heights[0];
vector<int> left(heights.size()),right(heights.size());
stack<int> stk;
//1.从左到右,获取left
for(int i=0;i<heights.size();i++){
while(!stk.empty()&&heights[i]<=heights[stk.top()]){
stk.pop();
}
if(stk.empty()) {
left[i]=-1;
stk.push(i);
}else{
left[i]=stk.top();
stk.push(i);
}
}
//2.从右到左,获取right
stk=stack<int>();
for(int i=N-1;i>=0;i--){
while(!stk.empty()&&heights[i]<=heights[stk.top()]){
stk.pop();
}
if(stk.empty()){
right[i]=N;
stk.push(i);
}else{
right[i]=stk.top();
stk.push(i);
}
}
//3.利用left,right,获取最大矩形面积
int res=0;
for(int i=0;i<N;i++){
if((right[i]-left[i]-1)*heights[i]>res)
res=(right[i]-left[i]-1)*heights[i];
}
return res;
}
int maximalRectangle(vector<vector<char>>& matrix) {
int rows=matrix.size();
if(rows==0||matrix[0].size()==0) return 0;
//此句不能放到前面去,因为matrix为空时,matrix[0]越界了
int cols=matrix[0].size();
vector<int> temp(cols);
int res=0;
for(int i=0;i<rows;i++){
for(int j=0;j<cols;j++){
temp[j]=matrix[i][j]=='1' ? temp[j]+1 : 0;
}
res=max(res,largestRectangleArea(temp));
}
return res;
}
};
思路二:动态规划求解最大矩形
class Solution {
public:
// dp(i, j)={x1,x2,x3}为三元组(向左走连续1的个数,向上走连续1的个数,包围的最大面积)
int maximalRectangle(vector<vector<char>>& matrix) {
int result = 0;
if (matrix.empty()) {return result;}
int m = matrix.size();
int n = matrix[0].size();
vector<vector<vector<int>>> dp(m, vector<vector<int>>(n, {0, 0, 0}));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == '1') {
if (i == 0 && j == 0) { // 左上角的值
dp[i][j] = {1, 1, 1};
} else if (i == 0) { // 第一行
dp[i][j] = {dp[i][j - 1][0] + 1, 1, dp[i][j - 1][2] + 1};
} else if (j == 0) { // 第一列
dp[i][j] = {1, dp[i - 1][j][1] + 1, dp[i - 1][j][2] + 1};
} else {
dp[i][j][0] = dp[i][j - 1][0] + 1; // 向左连续1的个数
dp[i][j][1] = dp[i - 1][j][1] + 1; // 向上连续1的个数
// 计算面积
int col_min = dp[i][j][0]; // 当前位置向左1的个数
int row = dp[i][j][1]; // 当前位置向上1的个数
for (int k = 0; k < row; k++) {
col_min = min(col_min, dp[i - k][j][0]); // 向左最小的1个数
dp[i][j][2] = max(dp[i][j][2], col_min * (k + 1));
}
}
result = max(result, dp[i][j][2]);
}
}
}
return result;
}
};
接雨水
思路一:单调栈
//方法3,单调栈,小于等于栈顶,入栈;大于栈顶,连续出栈;
int trap(vector<int>& height) {
int N=height.size();
if(N<3) return 0;
stack<int> stk;
int cur=0;
int res=0;
while(cur<N){
while(!stk.empty()&&height[cur]>height[stk.top()]){
int index=stk.top();
stk.pop();
if(stk.empty()) break;
int dis=cur-stk.top()-1;
res+=(min(height[cur],height[stk.top()])-height[index])*dis;
}
stk.push(cur++);
}
return res;
}
思路二:双指针
class Solution {
public:
//每一个柱子能接雨水的量为min(l_max,r_max)
//方法1,用vector<int> left,left[i]表示i位置左边的最大值;用vector<int> right,right[i]表示i位置右边的最大值
//方法2,双指针l_max表示左边当前最大值,r_max表示右边当前最大值
int trap(vector<int>& height) {
int N=height.size();
if(N<2) return 0;
int l_max=height[0],r_max=height[N-1];
int left=1,right=N-2;
int res=0;
while(left<=right){
if(l_max<r_max){
if(l_max>height[left])
res+=l_max-height[left++];
else
l_max=height[left++];
}else{
if(r_max>height[right])
res+=r_max-height[right--];
else
r_max=height[right--];
}
}
return res;
}
};
区间合并(面试手撕题)
#include<bits/stdc++.h>
using namespace std;
bool cmp(vector<int>& a,vector<int>& b){
return a[0]<b[0];
}
vector<vector<int>> merge(vector<vector<int>>&intervals){
sort(intervals.begin(),intervals.end(),cmp);
vector<vector<int>> res;
res.push_back(intervals[0]);
for(int i=1;i<intervals.size();i++){
if(intervals[i][0]<=res.back()[1]){
if(intervals[i][1]>res.back()[1])
res.back()[1]=intervals[i][1];
}else{
res.push_back(intervals[i]);
}
}
return res;
}
int main(){
vector<vector<int>> input={{1,3},{2,6},{8,10},{15,18}};
vector<vector<int>> res=merge(input);
for(auto& i:res){
for(auto& j:i){
cout<<j<<" ";
}
cout<<endl;
}
return 0;
}
去重复的三数之和(二面手撕题)
#include<bits/stdc++.h>
using namespace std;
//1.暴力法:为了方便去除重复的三元组,先排序,后挑出和为0的组合
//2.利用twoSum的思想,先排序,然后选择第一个数,剩下就是在后面的数中做twoSum问题了
//注意:避免重复的结果
vector<vector<int>> threeSum(vector<int>& nums){
if(nums.size()<3) return {};
sort(nums.begin(),nums.end());
vector<vector<int>> res;
int a,b,c;
//注意nums.size()返回值是无符号类型size_t,所以nums.size() -3的值不对哟
for(int i=0;i<=nums.size()-3;){
a=nums[i];
int left=i+1,right=nums.size()-1;
while(left<right){
b=nums[left];
c=nums[right];
if(a+b+c==0){
res.push_back({a,b,c});
while(left<right&&nums[left]==nums[++left]);
while(right>left&&nums[right]==nums[--right]);
}else if(a+b+c<0){
while(left<right&&nums[left]==nums[++left]);
}else
while(right>left&&nums[right]==nums[--right]);
}
while(i<=nums.size()-3&&nums[i]==nums[++i]);
}
return res;
}
int main(){
vector<int> nums={-1,-1,0,1,2,-1,-4};
vector<vector<int>> res=threeSum(nums);
for(auto& i:res){
for(auto& j:i){
cout<<j<<" ";
}
cout<<endl;
}
return 0;
}
并查集(京东笔试题)
find函数:找掌门人,同时做了路径压缩
to_union函数:找出掌门人,不同门派按秩合并,更新秩,
is_same函数:判断两个人的掌门人是否相同
路径压缩,
按秩合并
#include<bits/stdc++.h>
using namespace std;
class Dis{
private:
vector<int> parent;
vector<int> rank;
public:
Dis(int sz):rank(sz+1),parent(sz+1){
for(int i=1;i<=sz;i++){
parent[i]=i;
}
}
int find(int t){
return parent[t]==t?t:(parent[t]=find(parent[t]));
}
void to_union(int t1,int t2){
int f1=find(t1);
int f2=find(t2);
if(f1!=f2){
if(rank[f1]>rank[f2])
parent[f2]=f1;
else{
parent[f1]=f2;
if(rank[f1]==rank[f2])
rank[f2]++;
}
}
}
bool is_same(int t1,int t2){
return find(t1)==find(t2);
}
};
差分数组/前缀和
https://mp.weixin.qq.com/s/9L6lz0XDZ9gi-d_iPrSs8Q
线段树
前缀树(Trie 树)
用于敏感词过滤