zoukankan      html  css  js  c++  java
  • 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]

  • 相关阅读:
    软件原则
    Optional
    雪花算法原理
    AOP
    trycatchfinally
    lambda表达式
    BeanUtils属性转换工具
    @Transactional 失效
    用户线程&&守护线程
    卡顿问题
  • 原文地址:https://www.cnblogs.com/lsykk/p/15012364.html
Copyright © 2011-2022 走看看