leetcode 题单
ps:因为codeforces新的题都刷的差不多了,比赛也不是天天有,所以打算上leetcode补点知识,题单里会贴一些比较有意思(比较有技巧性)的题
难度大概在mid-hard(其实大部分是hard..)
缺失的第一个正数(类似于求数组的mex):将数组本身看成哈希表
class Solution { public int firstMissingPositive(int[] nums) { int n=nums.length,flag=0; for(int i=0;i<n;i++){ if(nums[i]==1)flag=1; } if(flag!=1)return 1;//数组里没有1 for(int i=0;i<n;i++){//把数组里[1,n]以外的数变为1 if(nums[i]<=0 || nums[i]>n)nums[i]=1; } for(int i=0;i<n;i++){//用nums[i]的正负来表示i出现过没有 int a=Math.abs(nums[i]); if(a==n)nums[0]=-Math.abs(nums[0]); else nums[a]=-Math.abs(nums[a]); } for(int i=1;i<n;i++){ if(nums[i]>0)return i; } if(nums[0]>0)return n; return n+1; } }
接雨水大合集
1.最普通版接雨水:维护左右两个指针向内扫描,每次移动的是高度低的那个,然后再维护左边最高值和右边最高值,统计贡献即可
class Solution { public: int trap(vector<int>& height) { if(height.size()==0)return 0; int sum=0,i=0,j=height.size()-1,lMax=height[0],rMax=height[j]; while(i<j){ if(height[i]<height[j]){ ++i; if(lMax<height[i])lMax=height[i]; sum+=lMax-height[i]; }else { --j; if(rMax<height[j])rMax=height[j]; sum+=rMax-height[j]; } } return sum; } };
2.二维接雨水:优先队列+bfs+单调性
我们先将最外面一圈方格拿出来,由于木桶理论,这圈方格内注水高度必定等于高度最低的那个格子
然后进行bfs拓展,每次取出高度最低的那个格子(i,j),向其周围四个格子拓展
被拓展到的格子如果比(i,j)高,那么不能注水
被拓展到的格子比(i,j)低,那么可以注水,然后再将的高度变为(i,j)的高度(想想为什么)
用优先队列维护被拓展到的格子
class Solution { public: struct Node{ int i,j,h; Node(){} bool operator>(const Node a)const { return h>a.h; } }; int n,m,vis[200][200]; priority_queue<Node,vector<Node>,greater<Node> >pq; int trapRainWater(vector<vector<int>>& heightMap) { if(heightMap.size()==0)return 0; if(heightMap[0].size()==0)return 0; n=heightMap.size(); m=heightMap[0].size(); memset(vis,0,sizeof vis); for(int j=0;j<m;j++){ Node t; t.i=0,t.j=j,t.h=heightMap[0][j]; pq.push(t); t.i=n-1,t.j=j,t.h=heightMap[n-1][j]; pq.push(t); vis[0][j]=vis[n-1][j]=1; } for(int i=1;i<n-1;i++){ Node t; t.i=i,t.j=0,t.h=heightMap[i][0]; pq.push(t); t.i=i,t.j=m-1,t.h=heightMap[i][m-1]; pq.push(t); vis[i][0]=vis[i][m-1]=1; } int sum=0; while(pq.size()){ Node now=pq.top();pq.pop(); int i=now.i,j=now.j,h=now.h; sum+=h-heightMap[i][j]; //cout<<i<<" "<<j<<" "<<h<<"\n"; if(i-1>=0 && vis[i-1][j]==0){ Node t; t.i=i-1,t.j=j,t.h=max(h,heightMap[i-1][j]); pq.push(t); vis[i-1][j]=1; } if(i+1<n && vis[i+1][j]==0){ Node t; t.i=i+1,t.j=j,t.h=max(h,heightMap[i+1][j]); pq.push(t); vis[i+1][j]=1; } if(j-1>=0 && vis[i][j-1]==0){ Node t; t.i=i,t.j=j-1,t.h=max(h,heightMap[i][j-1]); pq.push(t); vis[i][j-1]=1; } if(j+1<m && vis[i][j+1]==0){ Node t; t.i=i,t.j=j+1,t.h=max(h,heightMap[i][j+1]); pq.push(t); vis[i][j+1]=1; } } return sum; } };
3.盛水最多的容器:贪心+双指针:和1差不多的做法,用贪心证明下就行
换个角度:总共有C(n,2)种选择方案,若每次移动长版,得到的面积只会比答案小,直接减掉这些移动长版的方案集,所以移动短板目的在于压缩选择方案集合
class Solution { public: int maxArea(vector<int>& height) { int i=0,j=height.size()-1; int ans=0; while(i<j){ ans=max(ans,min(height[i],height[j])*(j-i)); if(height[i]>height[j])--j; else ++i; } return ans; } };
通配符匹配
通配符匹配:给定一个字符串 (s
) 和一个字符模式 (p
) ,实现一个支持 '?'(匹配一个字符)
和 '*'(匹配任意个字符)
的通配符匹配:
dp[i][j]表示s[1..i]和p[1..j]是否能匹配
匹配“*”的转移 dp[i][j]=dp[i][j-1]||dp[i-1][j]:第一种转移表示“*”不匹配任何字符,第二种转移表示既然s[1..i-1]能和p[1..j]匹配,由于p[j]=*,所以s[1..i]也能和p[1..j]匹配
class Solution { public: //dp[i][j]表示p[1..j]能否匹配上s[1..i] bool dp[2005][2005]; char S[2005],P[2005]; bool isMatch(string s, string p) { int n=s.size(),m=p.size(); for(int i=1;i<=n;i++)S[i]=s[i-1]; for(int i=1;i<=m;i++)P[i]=p[i-1]; dp[0][0]=1; int pos=1; while(P[pos]=='*'&&pos<=m) dp[0][pos]=1,pos++; for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ if(P[j]=='*'){ dp[i][j]=dp[i-1][j]||dp[i][j-1]; }else if(P[j]=='?'){ dp[i][j]=dp[i-1][j-1]; }else { if(P[j]==S[i])dp[i][j]=dp[i-1][j-1]; else dp[i][j]=0; } } return dp[n][m]; } };
正则表达式匹配:给你一个字符串 s
和一个字符规律 p
,请你来实现一个支持 '.'(匹配一个字符)
和 '*'(匹配人一个前面那个元素)
的正则表达式匹配
dp[i][j]表示s[1..i]和p[1..j]是否能匹配
class Solution { public: bool dp[1005][1005]; char S[1005],P[1005]; bool isMatch(string s, string p) { int n=s.size(),m=p.size(); for(int i=0;i<n;i++)S[i+1]=s[i]; for(int j=0;j<m;j++)P[j+1]=p[j]; dp[0][0]=1; for(int j=1;j<=m;j++){ if(P[j]=='*' && j-2>=0)dp[0][j]=dp[0][j-2]; } for(int i=1;i<=n;i++) for(int j=1;j<=m;j++){ if(S[i]==P[j])dp[i][j]=dp[i-1][j-1]; else if(P[j]=='.')dp[i][j]=dp[i-1][j-1]; else if(P[j]=='*'){ if(P[j-1]=='.' || P[j-1]==S[i]){//* dp[i][j]=dp[i][j-2]|| //*用来忽略前一位字符 dp[i][j-1]|| //*忽略 dp[i-1][j]; //*用来复制前一位字符 } else dp[i][j]=dp[i][j-2];//*只能用来忽略前一位字符 } } return dp[n][m]; } };
跳跃游戏2
给定一个非负整数数组,你最初位于数组的第一个位置。数组中的每个元素代表你在该位置可以跳跃的最大长度。你的目标是使用最少的跳跃次数到达数组的最后一个位置。
类似bfs的思路,dp[i]表示跳到i需要的步数
class Solution { public: int dp[100005]; int jump(vector<int>& nums) { int p=0,n=nums.size(); for(int i=0;i<n;i++){ while(p<i+nums[i] && p<n-1) dp[++p]=dp[i]+1; if(p==n-1)return dp[n-1]; } return dp[n-1]; } };
插入区间
给出一个无重叠的 ,按照区间起始端点排序的区间列表。在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。
二分(一个比较奇怪的二分,要多判一些条件)找到左右可合并的段,模拟一下即可
class Solution { public: int judge(vector<int>A, vector<int>B){ if(A[0]>B[0])swap(A,B); //cout<<A[0]<<" "<<A[1]<<" "<<B[0]<<" "<<B[1]<<'\n'; if(A[1]>=B[0])return 1; return 0; } vector<vector<int>> insert(vector<vector<int>>& intervals, vector<int>& newInterval) { int n=intervals.size(); vector<vector<int> >res; int L=0,R=n-1,mid,ans1=-1;//找最左侧和新区间相交的段 while(L<=R){ mid=(L+R)/2; if(judge(intervals[mid],newInterval)) ans1=mid,R=mid-1; else if(intervals[mid][0]>newInterval[1]) R=mid-1; else L=mid+1; } L=0,R=n-1; int ans2=n;//找最右侧和新区间相交的段 while(L<=R){ mid=(L+R)/2; if(judge(intervals[mid],newInterval)) ans2=mid,L=mid+1; else if(intervals[mid][1]<newInterval[0])L=mid+1; else R=mid-1; } if(ans1==-1 || ans2==n){//没有段和新区间相交 int flag=0; for(int i=0;i<n;i++){ if(newInterval[1]<intervals[i][0] && !flag){ res.push_back(newInterval); flag=1; } res.push_back(intervals[i]); } if(!flag) res.push_back(newInterval); return res; } //cout<<ans1<<" "<<ans2<<'\n'; for(int i=0;i<=ans1-1;i++){ res.push_back(intervals[i]); } vector<int>t(2); t[0]=min(intervals[ans1][0],newInterval[0]); t[1]=max(intervals[ans2][1],newInterval[1]); res.push_back(t); for(int i=ans2+1;i<n;i++) res.push_back(intervals[i]); return res; } };
二叉树任务调度
有一个二叉树形式的任务依赖结构,我们有两个 CPU 核,这两个核可以同时执行不同的任务,问执行完所有任务的最小时间,也即是希望两个 CPU 核的并行时间尽可能大
先对题目给定的条件进行分析:
1.对于子树u来说,结点u是必须串行的(u下的结点都依赖于u)
2.子树u的执行策略必定是:在u上串行,并行一段时间,最后执行到一条链的时候必须串行
3.设左儿子lson执行总时间>右儿子rson,那么有一个最优的策略:尽可能的让lson和rson并行最大时间
那么我们在lson上减去rson的运行时间(这部分可以左右并行,且优先减去lson上不能并行的那部分时间),lson剩下的时间尽量并行跑即可
所以结点u需要维护两个量:sum, parallel,sum表示子树u下运行总时间,parallel表示子树u下最大的并行时间
设a,b,c,d分别为lson的sum,parallel, rson的sum,parallel,要求u的sum,parallel
如果a-2b<=c,说明lson和rson可以一直并行,sum[u]=a+c+val[u],parallel[u]=a+c>>1
反之a-2b>c,说明lson里有a-2b-c的时间只能串行,sum[u]=a+c+val[u],parallel[u]=c+b
/** * Definition for a binary tree node. * struct TreeNode { * int val; * TreeNode *left; * TreeNode *right; * TreeNode(int x) : val(x), left(NULL), right(NULL) {} * }; */ class Solution { public: pair<int,double> dfs(TreeNode* root){ if(!root)return {0,0.0}; auto lson=dfs(root->left); auto rson=dfs(root->right); if(lson.first<rson.first) swap(lson,rson); auto a=lson.first,c=rson.first; auto b=lson.second,d=rson.second; if(a-2*b<=c) return {a+c+root->val,(a+c)*0.5}; else return {a+c+root->val,b+c}; } double minimalExecTime(TreeNode* root) { auto p=dfs(root); return p.first-p.second; } };
最小跳跃次数
给定一个数组 jump
,长度为 N
,在第 i
个位置可以选择跳到 0..i-1
和 i + jump[i]
,问从 0
跳过 n-1
的最小跳跃次数是多少。
典型的bfs处理,从0开始,不断将可拓展的点加入queue,再用一个单调指针p维护当前到达的最右端即可
class Solution { public: struct Node{ int pos,time; }; int time[1000006]; bool vis[1000006]; int minJump(vector<int>& jump) { int n=jump.size(); queue<Node> q; int p=0; q.push((Node){0,1}); time[0]=1; vis[p++]=1; while(q.size()){ Node now=q.front();q.pop(); //cout<<now.pos<<" "<<now.time<<'\n'; while(p<=now.pos){ if(!vis[p]){ vis[p]=1; q.push((Node){p,now.time+1}); time[p]=now.time+1; } p++; } int nxt=now.pos+jump[now.pos]; if(nxt<=n-1 && !vis[nxt]){ vis[nxt]=1; q.push((Node){nxt,now.time+1}); time[nxt]=now.time+1; } } int ans=0x3f3f3f3f; for(int i=0;i<n;i++) if(i+jump[i]>n-1 && time[i]) ans=min(ans,time[i]); return ans; } };
覆盖
经典状压dp做法:用S表示一行的状态,某位为1表示该位被占用,反之表示该位未被占用
dp[i][S]表示第i行状态为S时的最大覆盖数,那么枚举第i-1行的状态S',如果S,S'都合法,那么此时可以求出S状态下最多可以放多少块砖
预处理出cnt[S1][S2]表示上一行是S1,下一行是S2情况下,S2上的最大填放数,填放策略:先放竖的再放横的
然后dp时对于两个状态就可以o(1)转移了
class Solution { public: int cnt[1<<8][1<<8]; int mp[100][100],block[100]; void prework(int m){ for(int S1=0;S1<(1<<m);S1++) for(int S2=0;S2<(1<<m);S2++){//上下状态为S1 S2时下放最多填放数量 int num=0,T=S2; for(int i=0;i<m;i++)//把竖的填了 if(!(S1>>i & 1) && (T>>i & 1))num++,T-=(1<<i); for(int i=0;i<m-1;i++) if((T>>i&1) && (T>>(i+1)&1)){ num++; i++; } cnt[S1][S2]=num; } } int dp[50][1<<8]; int domino(int n, int m, vector<vector<int>>& broken) { prework(m); for(auto v:broken)mp[v[0]][v[1]]=1; for(int i=0;i<n;i++) for(int j=0;j<m;j++) if(mp[i][j])block[i]|=(1<<j); memset(dp,-1,sizeof dp); for(int i=0;i<n;i++){ for(int S=0;S<(1<<m);S++){ int f=0; for(int j=0;j<m;j++) if(!(S>>j & 1) && mp[i][j])f=1; if(f)continue; int T=S-block[i];//状态S可以自由填放的格子 if(i==0) dp[i][S]=max(dp[i][S],cnt[(1<<m)-1][T]); else { for(int S2=0;S2<(1<<m);S2++) if(dp[i-1][S2]!=-1) dp[i][S]=max(dp[i][S],dp[i-1][S2]+cnt[S2][T]); } } } int res=0; for(int i=0;i<(1<<m);i++) res=max(res,dp[n-1][i]); return res; } };
二分图做法:进行黑白染色,每个地板抽象成一个点,黑白格分成两部分
显然可以在相邻两个地板之间连边,如果是障碍物就不能连,匈牙利算法找下最大匹配即可
切分数组
线性筛+dp
用埃氏筛好像过不去。。可能是我写的dp太慢了。。
首先可以确定的是这题是可以dp的,对于每个数num[i],将其质因子分解出来,对于每个质因子pi,找到[1..i-1]里所有可以被pi整除的位置pos,用mi[pos-1]去更新mi[i]即可
向右扫描的时候维护一个pos[]数组,pos[i]表示质因子i出现的所有位置x中,mi[x-1]最小的那个
class Solution { public: #define N 1000005 #define maxn 1000005 int prime[1000005],m; bool vis[maxn]; void init(){ for(int i=2;i<maxn;i++){ if(!vis[i]){ prime[++m]=i; } for(int j=1;j<=m;j++){ if(prime[j]*i>=maxn)break; vis[prime[j]*i]=1; if(i%prime[j]==0) break; } } } int p[N],mm; void divide(int x){ mm=0; for(int i=1;i<=m && prime[i]*prime[i]<=x;i++) if(x%prime[i]==0){ p[++mm]=prime[i]; while(x%prime[i]==0)x/=prime[i]; } if(x>1)p[++mm]=x; } int mi[N],pos[N];//mi[i]表示i结尾的最小值,pos[i]表示素数i最右的被当成数组结尾的位置 int splitArray(vector<int>& nums) { init(); int n=nums.size(); for(int i=0;i<=1e6;i++)mi[i]=pos[i]=1e8; divide(nums[0]); for(int i=1;i<=mm;i++){ mi[0]=1;pos[p[i]]=0; } for(int i=1;i<n;i++){ divide(nums[i]); mi[i]=min(mi[i],mi[i-1]+1); for(int j=1;j<=mm;j++){ if(pos[p[j]]==1e8)continue; if(pos[p[j]]==0)mi[i]=1; else mi[i]=min(mi[i],mi[pos[p[j]]-1]+1); } for(int j=1;j<=mm;j++){ if(pos[p[j]]==0)continue; if(pos[p[j]]==1e8)pos[p[j]]=i; else if(mi[i-1]<mi[pos[p[j]]-1]) pos[p[j]]=i; } } return mi[n-1]; } };
游乐园的游览计划
三元环计数:这题要先把三元环统计出来 https://www.cnblogs.com/Dance-Of-Faith/p/9759794.html
贪心:以v为顶点,把所有包含v的三元环统计出来,然后取权值最大的三个三元环t1,t2,t3,可以确定v为顶点的策略中至少有这三个中的一个(鸽笼定理,或者随便想想一定是这样)
所以可以枚举前三个三元环,再枚举另一个三元环,设v有K个三元环,那么复杂度就是O(3K)
由于所有三元环个数为O(m^1.5),所以总复杂度也是这个
ps:在class内自定义sort的cmp函数,要用lambda表达式来写 关于lambda的使用以及捕获列表:https://blog.csdn.net/jlusuoya/article/details/75299096
class Solution { public: #define N 10005 vector<int>G[N]; int id[N],rank[N],n,vis[N]; int d[N],val[N]; struct Circle{int a,b,c;}; vector<Circle>s[N]; inline int calc(Circle a,Circle b){ int res=val[a.a]+val[a.b]+val[a.c]+val[b.a]+val[b.b]+val[b.c]; if(a.a==b.a)res-=val[b.a]; if(a.b==b.a)res-=val[b.a]; if(a.c==b.a)res-=val[b.a]; if(a.a==b.b)res-=val[b.b]; if(a.b==b.b)res-=val[b.b]; if(a.c==b.b)res-=val[b.b]; if(a.a==b.c)res-=val[b.c]; if(a.b==b.c)res-=val[b.c]; if(a.c==b.c)res-=val[b.c]; return res; } int maxWeight(vector<vector<int>>& edges, vector<int>& value) { n=value.size(); for(int i=0;i<n;i++)id[i]=i,val[i]=value[i]; for(auto e:edges){ int u=e[0],v=e[1]; d[u]++,d[v]++; } sort(id,id+n,[&](int &a,int &b){ if(d[a]==d[b])return a>b; return d[a]>d[b]; }); for(int i=0;i<n;i++)rank[id[i]]=i; for(auto e:edges){ int u=e[0],v=e[1]; if(rank[u]<rank[v])swap(u,v); G[u].push_back(v); } for(int i=0;i<n;i++){ for(auto j:G[i])vis[j]=1; for(auto j:G[i]) for(auto k:G[j]) if(vis[k]==1){ Circle t=(Circle){i,j,k}; s[i].push_back(t); s[j].push_back(t); s[k].push_back(t); } for(auto j:G[i])vis[j]=0; } int ans=0; for(int i=0;i<n;i++){ sort(s[i].begin(),s[i].end(),[&](Circle &a,Circle &b){ int res1=val[a.a]+val[a.b]+val[a.c],res2=val[b.a]+val[b.b]+val[b.c]; return res1>res2; }); int j=s[i].size(); if(j>=1){ for(int k=0;k<j;k++) ans=max(ans,calc(s[i][0],s[i][k])); } if(j>=2){ for(int k=0;k<j;k++) ans=max(ans,calc(s[i][1],s[i][k])); } if(j>=3){ for(int k=0;k<j;k++) ans=max(ans,calc(s[i][2],s[i][k])); } } return ans; } };
游乐园的迷宫
平面上有 N个点,找到一条访问N个点的路径,使得路径的转角满足给定的转角序列。
构造方法:设当前点是now,如果下一次向右转,那么下一个点就是未访问过的和now叉积最逆时针的那个;反之下一次左转,那么下一个点就是未访问过的和now叉积最顺时针的(意会一下即可)那个点
总之就是不断找最极端的点nxt,使其他所有点都在now->nxt的左侧和右侧,这样必定会有解,复杂度O(n^2)
class Solution { public: #define N 2005 inline vector<int> vec(vector<int>&k1,vector<int>k2){//k1->k2 k2[0]-=k1[0];k2[1]-=k1[1]; return k2; } inline int cross(vector<int>&k1,vector<int>&k2){ return k1[0]*k2[1]-k1[1]*k2[0]; } int n,vis[N]; vector<int> now,nxt; vector<int> visitOrder(vector<vector<int>>& points, string direction) { n=points.size(); nxt.resize(2);now.resize(2); vector<int>ans; int id=0; now=points[0]; for(int i=1;i<n;i++) if(points[i][0]<now[0])now=points[i],id=i; vis[id]=1; ans.push_back(id); for(int i=0;i<n-2;i++){ if(direction[i]=='L'){//下一步左转 int id; for(int j=0;j<n;j++)if(!vis[j])nxt=points[j],id=j; for(int j=0;j<n;j++)if(!vis[j]){ vector<int>p=vec(now,nxt); vector<int>q=vec(now,points[j]); if(cross(p,q)<0){ id=j;nxt=points[j]; } } vis[id]=1; ans.push_back(id); vis[id]=1; now=points[id]; }else {//下一步右转 int id; for(int j=0;j<n;j++)if(!vis[j])nxt=points[j],id=j; for(int j=0;j<n;j++)if(!vis[j]){ vector<int>p=vec(now,nxt); vector<int>q=vec(now,points[j]); if(cross(p,q)>0){id=j;nxt=points[j];} } vis[id]=1; ans.push_back(id); vis[id]=1; now=points[id]; } } for(int i=0;i<n;i++)if(!vis[i])ans.push_back(i); return ans; } };
环形链表 II
给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null
。
这题的快慢指针真的巧妙。。
/** * Definition for singly-linked list. * struct ListNode { * int val; * ListNode *next; * ListNode(int x) : val(x), next(NULL) {} * }; */ class Solution { public: ListNode *detectCycle(ListNode *head) { if(head==NULL)return NULL; ListNode *s=head;//慢指针 ListNode *f=head;//快指针 int t=0; while(1){//碰到NULL说明没有环 ++t; //cout<<t<<" "; if(s->next!=NULL) s=s->next; else return NULL; if(f->next!=NULL) f=f->next; else return NULL; if(f->next!=NULL) f=f->next; else return NULL; if(s==f){//快慢指针相遇 auto res=head; while(res!=s){ s=s->next; res=res->next; } return res; } } return NULL; } };