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]

posted @ 2021-07-14 18:27  lsy_kk  阅读(73)  评论(0编辑  收藏  举报