力扣Hot100题单个人计划c++版(一)
力扣Hot100题单个人计划c++版(二)
力扣Hot100题单个人计划c++版(三)
力扣Hot100题单个人计划c++版(四)
力扣Hot100题单个人计划c++版(五)
刷题链接:力扣Hot 100
每日一题,每日一更,白板手写。
11.3打卡
拓扑排序,还是待补吧。有点难写。
11.3打卡
这个还是等深入了解吧。本题只是皮毛,没有可以参考的标准代码。
11.4打卡
经典topk问题。快速排序的思想可以找到第k大元素。堆排序的思想可以找到前k个数。
class Solution {
public:
int partation(vector<int>& nums, int l, int r) {
int idx=l+rand()%(r-l+1);
swap(nums[l],nums[idx]);
int mid=nums[l];
while(l<r){
while(l<r&&nums[r]<=mid)--r;
nums[l]=nums[r];
while(l<r&&nums[l]>=mid)++l;
nums[r]=nums[l];
}
nums[l]=mid;
return l;
}
int findKthLargest(vector<int>& nums, int k) {
int n=nums.size();
int l=0,r=n-1;
int p;
while(l<=r){
p=partation(nums,l,r);//p是坐标
if(p==k-1)return nums[p];
else if(p>k-1)r=p-1;
else l=p+1;
}
return nums[p];
}
};
11.5打卡
动态规划问题,这个递推关系很巧妙,我们用 dp ( i , j ) \textit{dp}(i, j) dp(i,j) 表示以 ( i , j ) (i, j) (i,j)为右下角,且只包含 1 的正方形的边长最大值。如果该位置的值是 0,则 dp ( i , j ) = 0 \textit{dp}(i, j)=0 dp(i,j)=0,如果该位置的值是 1,则 dp ( i , j ) \textit{dp}(i, j) dp(i,j) 的值由其上方、左方和左上方的三个相邻位置的 dp ( i , j ) \textit{dp}(i, j) dp(i,j) 值决定。具体而言,当前位置的元素值等于三个相邻位置的元素中的最小值加 1。很难想到的思路,mark一下。
class Solution {
public:
int maximalSquare(vector<vector<char>>& matrix) {
int n=matrix.size();
if(!n)return 0;
int m=matrix[0].size();
if(!m)return 0;
int ans=0;
vector<vector<int>> dp(n,vector<int>(m,0));
for(int i=0;i<n;++i){
for(int j=0;j<m;++j){
if(matrix[i][j]=='1'){
if(i==0||j==0)dp[i][j]=1;
else dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
}
else dp[i][j]=0;
ans=max(ans,dp[i][j]);
}
}
return ans*ans;
}
};
11.6打卡
递归版很好写。迭代版要花点时间,就不码了。
class Solution {
public:
void dfs(TreeNode* p){
if(!p)return;
if(p->left)dfs(p->left);
if(p->right)dfs(p->right);
TreeNode* t=p->left;
p->left=p->right;
p->right=t;
}
TreeNode* invertTree(TreeNode* root) {
dfs(root);
return root;
}
};
11.7打卡
O(1)空间复杂度。。。怎么说呢,只能递归了。很难写而且没必要。本题算了。
class Solution {
public:
ListNode* front;
bool check(ListNode* cur){
if(cur){
if(!check(cur->next))return false;
if(cur->val!=front->val)return false;
front=front->next;
}
return true;
}
bool isPalindrome(ListNode* head) {
front=head;
return check(head);
}
};
11.8打卡
说来惭愧,LCA问题到现在还没完全熟练。一般来说LCA问题有常见三种解法(参考最近公共祖先以及博客LCA 最近公共祖先
Tarjan(离线)算法的基本思路及其算法实现)。Tarjan算法,倍增算法,转化为RMQ问题后DFS+ST解决。Tarjanr是离线算法,而后两者是在线算法。这三个算法待补,闲了回过头写。暂时先写个最简单的,即递归查询左右子树是否有p和q即可。
class Solution {
public:
TreeNode* ans;
bool dfs(TreeNode* pos,TreeNode* p,TreeNode* q){
if(pos==nullptr)return false;
bool l=dfs(pos->left,p,q);
bool r=dfs(pos->right,p,q);
if(l&&r)ans=pos;
if((pos->val==p->val||pos->val==q->val)&&(l||r))ans=pos;
return l||r||pos->val==p->val||pos->val==q->val;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
dfs(root,p,q);
return ans;
}
};
11.9打卡
本题最直观思路就是算出所有数的乘积然后除以每个数放到原数组,如果数组中有零就返回全零的数即可。
但题目要求不能用除法。那么只需要类似前缀和的思想,求出每个数的左侧前缀乘积和右侧后缀乘积即可。但是又题目又要求常数复杂度。所以可以先用返回的答案数组存前缀乘积,用一个变量更新右侧乘积即可。
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
int n=nums.size();
vector<int> ans(n);
ans[0]=1;
for(int i=1;i<n;++i)ans[i]=ans[i-1]*nums[i-1];//左侧乘积
int r=1;
for(int i=n-1;i>=0;--i){
ans[i]=ans[i]*r;
r*=nums[i];
}
return ans;
}
};
11.10打卡
最直观的解法即将窗口的k个数组成最大堆(优先队列),每次删去第一个数,加入后一个数,维护这个堆即可。
不过本题还有一种巧妙的解法,基于这样一个性质:如果当前的滑动窗口中有两个下标 i 和 j,其中 i 在 j的左侧(i < j),并且 i 对应的元素不大于 j 对应的元素 ( nums [ i ] ≤ nums [ j ] n u m s [ i ] ≤ n u m s [ j ] ) (\textit{nums}[i] \leq \textit{nums}[j]nums[i]≤nums[j]) (nums[i]≤nums[j]nums[i]≤nums[j]),那么可以删掉元素i,因为只要 i 还在窗口中,那么 j 一定也还在窗口中,由于 nums [ j ] \textit{nums}[j] nums[j] 的存在, nums [ i ] \textit{nums}[i] nums[i]一定不会是滑动窗口中的最大值了。所以我们可以维护一个单调队列,维护下标递增,值非严格递减的队列。
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n=nums.size();
deque<int> dq;
vector<int> ans;
for(int i=0;i<k;++i){
//注意while语句一定是小于等于
while(!dq.empty()&&nums[dq.back()]<=nums[i])dq.pop_back();
dq.emplace_back(i);
}
ans.emplace_back(nums[dq.front()]);
for(int i=k;i<n;++i){
while(!dq.empty()&&nums[dq.back()]<=nums[i])dq.pop_back();
dq.emplace_back(i);
while(dq.front()<=i-k)dq.pop_front();
ans.emplace_back(nums[dq.front()]);
}
return ans;
}
};
11.11打卡
剑指offer的经典题。由于特殊性质可以整行整列的排除。z字形搜索。可以从右上角也可以左下角。本代码为从右上角搜索。
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m=matrix.size();
int n=matrix[0].size();
int x=0,y=n-1;
while(x<m&&y>-1){
int t=matrix[x][y];
if(t==target)return true;
t>target?--y:++x;
}
return false;
}
};
11.12打卡
由于没有会员解锁不了本题。今天休息一天。
11.13打卡
第一眼动态规划题,可以算出前面数字平方和个数,从1到 n \sqrt{n} n枚举减去的一个平方和即可。
但题解还有一个数学定理证法很巧妙:四平方和定理证明了任意一个正整数都可以被表示为至多四个正整数的平方和。这给出了本题的答案的上界。
同时四平方和定理包含了一个更强的结论:当且仅当 n ≠ 4 k × ( 8 m + 7 ) n n \neq 4^k \times (8m+7)n n=4k×(8m+7)n时,n 可以被表示为至多三个正整数的平方和。由此可判断该数是不是四个平方和排除4,然后是不是平方数排除1,然后只需在 n \sqrt{n} n搜索可不可以两个平方数表示可以排除2,这样结果只能是3.
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n+1);
dp[0]=0;
int minn=n+1;
for(int i=1;i<=n;++i){
minn=n+1;
for(int j=1;j*j<=i;++j){
minn=min(minn,dp[i-j*j]+1);
}
dp[i]=minn;
}
return dp[n];
}
};
11.14打卡
题解思路是双指针。这里也有一个类似思路,即记录遇到0的个数,每次移动只需要把该数移动到该位减去0个数之前。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int znum=0;
int n=nums.size();
for(int i=0;i<n;++i){
if(nums[i]==0)++znum;
else swap(nums[i],nums[i-znum]);
}
}
};
11.16打卡
要求空间复杂度为常数,本题官方解法很多。剑指offer也给出了一个常数空间复杂度,线性时间复杂度的解法。只能说是特定问题下的特定解法。当初看剑指offer时就一头雾水,回头再补。
11.16打卡
仍是费脑子的题,亏欠太多,考完研补。
11.17打卡
动态规划入门经典问题,网上讲解很多,最常见的就是dp+二分查找, O ( n l o n g n ) O(nlongn) O(nlongn)时间复杂度。这个解法非常巧妙,当初第一次学的时候十分不解,后来领悟后还是感叹不已。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
int dp[n];
dp[0]=nums[0];
for(int i=1;i<n;i++)dp[i]=INT_MAX;
int ans=0;
for(int i=1;i<n;++i){
if(nums[i]>dp[ans])dp[++ans]=nums[i];
else{
int pos=lower_bound(dp,dp+ans,nums[i])-dp;
dp[pos]=nums[i];
}
}
return ans+1;
}
};
11.18打卡
hard题,但思路并不难。然而今天要写别的实验,留给考完研的我写吧哈哈。
11.19打卡
壮位dp入门题,因为同一时间只能存在持有股票和没有持有两种情况,我们用0表示没有这个时间未持有股票,用1表示已经持有。冷却期一天可以看做当前持有股票的话前一天不能卖出股票,即退两天没有持有和退一天持有的最大值。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n=prices.size();
int dp[n][2];
dp[0][0]=0;
dp[0][1]=-prices[0];
if(n==1)return 0;
dp[1][0]=max(dp[0][0],dp[0][1]+prices[1]);
dp[1][1]=max(dp[0][1],dp[0][0]-prices[1]);
if(n==2)return dp[1][0];
for(int i=2;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-2][0]-prices[i]);
}
return dp[n-1][0];
}
};
11.20打卡
区间dp,太难想了,贴一个非常容易理解的题解。此外还有回溯的思想也很巧妙,因为去掉气球会改变次序,可以逆向思考,把问题看作给两端为1的气球中间添加气球的过程。此题太巧妙了,值得多回头看几眼。
class Solution {
public:
int maxCoins(vector<int>& nums) {
int n=nums.size()+2;
nums.insert(nums.begin(),1);
nums.push_back(1);
vector<vector<int>> dp(n,vector<int>(n,0));
for(int i=n-3;i>=0;--i){
for(int j=i+2;j<=n-1;++j){
for(int k=i+1;k<j;++k){
dp[i][j]=max(dp[i][k]+dp[k][j]+nums[i]*nums[k]*nums[j],dp[i][j]);
}
}
}
return dp[0][n-1];
}
};
11.21打卡
硬币问题,如果硬币从小到大排序,任意两个小的加起来都不超过前面大的面值,那说明钱分成硬币不可能有别的最小组合,可以贪心地从大到小选取面值。但是如果相等或者大于,那就是本题的动态规划解法,类似完全背包,只不过成了背包必须恰好装够,dp存放的是最少取数而不是最大价值。强烈推荐大家看下背包九讲的完全背包问题。之后提到的常数项优化也被解释。
原理都是一样的,dp[i][j]表示用前i种硬币支付j元时的最少硬币数。显然当加入下一枚硬币时,即第i+1枚硬币可以选的时候,只有两种情况,要么不选这枚硬币,要么选取这枚硬币,所以 d p [ i + 1 ] [ j ] = m i n ( d p [ i ] [ j ] , d p [ i + 1 ] [ j − c o i n s [ i ] ] + 1 ] ) dp[i+1][j]=min(dp[i][j],dp[i+1][j-coins[i]]+1]) dp[i+1][j]=min(dp[i][j],dp[i+1][j−coins[i]]+1]),换成正常写法即 d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − c o i n s [ i ] ] + 1 dp[i][j]=min(dp[i-1][j],dp[i][j-coins[i]]+1 dp[i][j]=min(dp[i−1][j],dp[i][j−coins[i]]+1,从上式可以看出第i次只和第i-1次有关,所以没必要二维数组记录前i-1个,只需要用一个数组记录第i-1次,在原数组上从0到V进行dp即可。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n=coins.size();
vector<int> dp(amount+1,amount+1);
dp[0]=0;
for(int i=0;i<n;++i){
for(int j=0;j+coins[i]<=amount;++j){
dp[j+coins[i]]=min(dp[j]+1,dp[j+coins[i]]);
}
}
if(dp[amount]>amount)return -1;
return dp[amount];
}
};
但上方代码其实效率很低,原因在于我们每次先枚举种类次数再枚举钱数。实际上两个for循环互换可以带来很大的优化,因为钱数是很大的,而且都需要枚举完。但硬币种类数不超过15个,所以先枚举钱数再小范围枚举钱数会比该方法快一些。
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n=coins.size();
vector<int> dp(amount+1,amount+1);
dp[0]=0;
for(int i=1;i<=amount;++i){
for(int j=0;j<n;++j){
if(i-coins[j]>=0)
dp[i]=min(dp[i],dp[i-coins[j]]+1);
}
}
if(dp[amount]>amount)return -1;
return dp[amount];
}
};