LC刷题简要记录
是leetcode的刷题记录
一个看到困难题就退缩的渣渣
布尔运算
2021.7.14
区间DP
题意:给一个布尔表达式,其中包括0,1,&,|,^五种符号,给出一个值result,问有几种运算顺序使得该布尔表达式的结果为result。
布尔表达式中的运算符数量不超过19个。
样例:
输入:s = "1^0|0|1", result = 0
输出:2
解释: 两种可能的运算顺序是1^(0|(0|1))和1^((0|0)|1)
思路:看到运算符数量不超过19个,想着搜索。想半天想不出一个好一点的复杂度,直接求助题解。
用dp[i][j][r]表示在表达式的i至j段,计算结果为r(r=0/1)的方案数。
枚举i至j段中的分割点k(即运算符),枚举左右两边的结果(0/1),据此得到对应的计算结果。
总结一下就是,枚举左边的数为n1,右边的数为n2,则有
dp[i][j][cal(n1,n2,s[k])]+=dp[i][k-1][n1]*dp[k+1][j][n2];
其中cal(n1,n2,a[k])表示布尔表达式n1 s[k] n2的结果(例如1 & 1,即cal(1,1,'&'),结果为1)
然后按区间dp的模板做就行了。初始化所有s[k]=='0'|'1',dp[i][i][s[k]-'0']=1,其余dp为0
class Solution {
public:
int cal(int x,int y,char c){
if (c=='&') return x&y;
else if (c=='|') return x|y;
else if (c=='^') return x^y;
return 0;
}
int countEval(string s, int result) {
int len,i,j,k,n1,n2,r,n;
int dp[42][42][2];
memset(dp,0,sizeof(dp));
n=s.length();
for (i=0;i<n;i+=2){
dp[i][i][s[i]-'0']=1;
}
for (len=2;len<n;len+=2){/*枚举区间长度*/
for (i=0;i+len<n;i+=2){/*枚举区间起点*/
j=i+len;
for (k=i+1;k<j;k+=2){/*枚举分割点(符号)*/
for (n1=0;n1<=1;n1++){
for (n2=0;n2<=1;n2++){
r=cal(n1,n2,s[k]);
dp[i][j][r]+=dp[i][k-1][n1]*dp[k+1][j][n2];
}
}
}
}
}
return dp[0][n-1][result];
}
};
为运算表达式设计优先级
2021.7.15
分治
题意:给一个含有数字和'+','-','*'三种符号的表达式,为表达式增加括号,改变运算优先级以求出不同结果。要求给出所有可能的结果。
思路:
对于表达式的s[lr]段,枚举l到r之间的运算符号位i,把问题变为s[li-1]和s[i+1~r]。
解出s[li-1]的答案集left和s[i+1r]的答案集right后,枚举,从两答案集合各取出一个,进行s[i]符号对应的运算,将结果加入s[l~r]的答案集。
这应该算用分治的思想,递归求解。
(啊我说不清楚了,看代码吧
class Solution {
public:
string s;
vector<int> work(int l,int r){
int i,j,k,n,m,now,flag=0;
vector<int> left,right,res;
for (i=l;i<=r;i++){
if (s[i]>='0' && s[i]<='9') continue;
flag=1;
left=work(l,i-1);
right=work(i+1,r);
n=left.size();
m=right.size();
for (j=0;j<n;j++){
for (k=0;k<m;k++){
if (s[i]=='+') now=left[j]+right[k];
else if (s[i]=='-') now=left[j]-right[k];
else if (s[i]=='*') now=left[j]*right[k];
res.push_back(now);
}
}
}
if (!flag){
now=0;
for (i=l;i<=r;i++) now=now*10+s[i]-'0';
res.push_back(now);
}
return res;
}
vector<int> diffWaysToCompute(string expression) {
s=expression;
return work(0,s.length()-1);
}
};
冗余连接 II
2021.7.15
分类讨论 并查集
题意:定义有根树:该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。
给一个有向图,图由一个含有n个结点的有根树和一条附加边组成,附加边的两个端点均包含于这n个结点中,且该附加边不与其他边重合。
现在要求找到一条边,使得从有向图中删去该边后,剩下一颗有根树。(就是找附加边)
思路:画图很重要啊。
(思考时考虑到的5种情况的图示,待补充)
最后可以把这些情况分为3类
1.存在一个点入度为2,且图中没有有向环。
该情况下,只要找到这个入度为2的点,指向它的两条边均是一种答案,任意输出一种即可。
2.存在一个点入度为2,且图中存在一个有向环。
该情况下,找到这个入度为2的点,指向它的两条边中有一条是正确答案。
此时,任选其中的一条边,将它从图中删去。
若这条边是错误答案,那么图中一定会出现一个环(因为有向环未被破坏)
因此,可以使用并查集,检查剩余图中是否存在环,
若存在,则说明答案是另一条边;否则,答案就是删去的那条边。
3.不存在任何一点入度为2。
即附加边为子节点指向根节点的情况,
在该情况下,图中环上的任意一条边都可以是答案。
因此使用并查集,找到这个环中的任意一条边输出即可。
我们可以再将这三种情况简略为两种,把12分为情况一(省去判断时检查是否存在有向环的步骤),3分为情况二。
然后用并查集就可以快乐地解决了。
class Solution {
public:
int fa[1005];
int find(int x){
if (x==fa[x]) return x;
return fa[x]=find(fa[x]);
}
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int i,fx,fy,n=edges.size();
int del_edge,ndel_edge;
int flag=0;
/*默认为情况二,不存在入度为2的点,即附加边从子节点指向根节点*/
int cnt[1005];/*cnt[i]用于记录指向i结点的边的序号*/
for (i=1;i<=n;i++){
fa[i]=i;
cnt[i]=-1;
}
for (i=0;i<n;i++){
if (cnt[edges[i][1]]!=-1) {/*出现了入度为2的点edges[i][0]*/
del_edge=i;/*先假定i是要删除的那条边*/
ndel_edge=cnt[edges[i][1]];/*另一条可能删除的边*/
flag=1;/*情况一,存在入度为2的点*/
break;
}
cnt[edges[i][1]]=i;
}
for (i=0;i<n;i++){
if (flag==1 && i==del_edge) continue;
/*删去了边del_edge,所以跳过*/
fx=find(edges[i][0]);
fy=find(edges[i][1]);
if (fx==fy){/*出现了环,这一条边i是环上的一条边*/
if (flag==1) return edges[ndel_edge];
/*情况一,删去del_edge后仍有环,说明答案为另一条边*/
else return edges[i];
/*情况二,答案为环上的任意一条边*/
}
fa[fy]=fx;
}
return edges[del_edge];
/*情况一,删去的边del_edge即为答案*/
}
};
三数之和
2021.7.15
排序+双指针
题意:给一个数组nums,其中有n个元素,要求找到nums中的三个元素a,b,c使得a+b+c=0,输出所有可能的且不重复的abc三元组。(n<=3000)
思路:由于n<=3000,\(O(n^2)\)的算法是可行的。
于是一开始想到了枚举abc中的两个,假设枚举a和b,然后找使得a+b+c=0的第三个数c是否存在。
具体做法是先遍历一边nums,用数组cnt记录nums[i]出现的次数。找第三个数时看cnt[-(a+b)]是否不为0即可。
然而这个做法卡死在了答案不重复这一点上,于是开始了漫长的调试。。
假设a<=b<=c,若a+b+c=0,那么一定有a<=0,c>=0,
将nums从小到大排序,从前往后枚举a,从后往前枚举c,然后根据cnt和a<=b<=c,寻找对应的b是否存在。
此时若发现a的值在之前已经被枚举过了(nums[i]==nums[i-1]),就直接跳过,因为如果继续得到的答案一定与之前得到的是一样的。c也是同理。
由于a<=b<=c,所以还存在三种特别的情况,就是a=b!=c,a!=b=c,a=b=c。对于这几种情况需要判断cnt[b]的值是否>=2或>=3。
然后稀里糊涂地就做出来了,花了不少时间,看了一下题解发现我的做法似乎有些奇怪,效率也不大高。
class Solution {
public:
const int M=100000;
vector<vector<int>> threeSum(vector<int>& nums) {
int i,j,k,n;
int cnt[2*M+5];
vector<vector<int>> ans;
sort(nums.begin(),nums.end());
n=nums.size();
memset(cnt,0,sizeof(cnt));
for (i=0;i<n;i++){
cnt[M+nums[i]]++;
}
for (i=0;i<n && nums[i]<=0;i++){/*a[i]*/
if (i>0 && nums[i]==nums[i-1]) continue;
for (j=n-1;j>i+1 && nums[j]>=0;j--){
if (j<n-1 && nums[j]==nums[j+1]) continue;
k=-nums[i]-nums[j];
if (nums[i]<=k && k<=nums[j] && cnt[M+k]>0){
if ((nums[i]==k || nums[j]==k)&& cnt[M+k]<2) continue;
if (nums[i]==k && nums[j]==k && cnt[M+k]<3) continue;
vector<int> anans;
anans.push_back(nums[i]);
anans.push_back(k);
anans.push_back(nums[j]);
ans.push_back(anans);
}
}
}
return ans;
}
};
接下来讲一下题解用的排序+双指针的方法。
有一个很显然的\(O(n^3)\)的方法,将nums从小到大排序,枚举a,b,c。
假设a<b<c且a+b+c=0,那么对于$a+b'+c'=0,b>b',一定有c<c'。
所以若从小到大枚举b,由于以上的性质,满足条件的c一定是越来越小的,不需要再增加一重循环进行枚举。
这样我们就只需要枚举ab的二重循环,和一个用来记录当前c的指针。
class Solution {
public:
const int M=100000;
vector<vector<int>> threeSum(vector<int>& nums) {
int i,j,k,n;
vector<vector<int>> ans;
sort(nums.begin(),nums.end());
n=nums.size();
for (i=0;i<n && nums[i]<=0;i++){
if (i!=0 && nums[i]==nums[i-1]) continue;//跳过重复的a
k=n-1;
for (j=i+1;j<n && j<k;j++){
if (nums[k]<0) break;
if (j!=i+1 && nums[j]==nums[j-1]) continue;//跳过重复的b
while (j<k && nums[i]+nums[j]+nums[k]>0) k--;//找c
if (j<k && nums[i]+nums[j]+nums[k]==0){
ans.push_back({nums[i],nums[j],nums[k]});
}
}
}
return ans;
}
};
盛最多水的容器
2021.7.16
双指针
题意:给n个正整数\(a_1~a_n\),表示在有一个垂直于x坐标的,起始点为(i,0)终止点为(i,\(a_i\))的板子。
问,如何选取两块板子,使得它们与x坐标组成的凹形能够容纳最多的水。
思路:想到三条性质:
1.若i<i'且a[i]<=a[i'],对于任意j满足i<i'<j,有\(S_{ij}\)<=\(S_{i'j}\)。
2.若i<j且a[i]>a[j],则面积\(S_{ij}\)与i的选取无关。
3.若i<j且a[i]<=a[j],则面积\(S_{ij}\)与j的选取无关。
假设选取的两块板子为i,j(i<j)。
从前往后枚举第一块板子i,并保证每次枚举的a[i']都大于上一次枚举的a[i](性质1)
用指针j表示第二块板子的位置,初始为n。若当前的a[i]>a[j],则统计答案,并将j--。(性质2)
若当前的a[i]<=a[j],则统计答案,进入下一次i的枚举。(性质3)
(然而题解里还有更简洁的。。不管了= =)
class Solution {
public:
int maxArea(vector<int>& height) {
int n=height.size();
int lsti=0;
int i,j=n-1,now,ans=0;
for (i=0;i<n && i<j;i++){
if (height[i]<=lsti) continue;
while (height[i]>height[j] && i<j){
now=height[j]*(j-i);
if (now>ans) ans=now;
j--;
}
if (height[i]<=height[j]) {
now=height[i]*(j-i);
if (now>ans) ans=now;
}
lsti=height[i];
}
return ans;
}
};
正则表达式匹配
2021.7.20
dp
题意:给一个字符串s和一个正则表达式p,p中含有'.'和'',其中'.'可以匹配任意字符,''可以匹配零个或多个前一个字符。问p能否匹配s。
思路:一开始觉得是模拟,做着做着觉得不对,应该是dp。
假设dp[i][j]表示s的前i位,p的前j位能否匹配。
当s[i]p[j]或p[j]'.'时,显然dp[i][j]=dp[i-1][j-1];
重点在于如何处理p[j]==''的情况。
假设p为ab,则可以匹配a,ab,abb,abbb......
那么此时dp[i][j]=dp[i]j-2|dp[i]j-1|dp[i-1][j-1]