Leetcode刷题总结(三)

1、不用加号的加法

在这里插入图片描述
思路:不能用算术运算符,因此考虑位运算来实现加法。
在这里插入图片描述

class Solution {
public:
    int add(int a, int b) {
        int sum=0;
        int carry = 0;
        while(b!=0)     // 当进位为0,说明计算结束
        {
            sum = a ^ b;  // 异或计算未进位部分
            carry = (uint32_t)(a & b) << 1; // 与计算进位部分,进位必须是无符号数
            a = sum;      // 保存未进位部分
            b = carry;   // 保存进位部分
        }
        return a;

    }
};

2、最长单词

在这里插入图片描述
思路:注意题目说的是,该单词由其他单词组合而成,因此不一定是两个单词组合,可能是多个单词组合,那么需要不断递归下去。如果当前字符串中长度为len的子串,出现在单词哈希表中,那么就去掉这一子串,接着递归剩余串能否在单词哈希表中找到。由于可能重复使用单词,因此单词哈希表不做删除。

class Solution {
public:
    bool iscompose(string word, unordered_set<string> & tmp)
    {
        if(word.size()==0)      // 如果字符串为空,那肯定能组合
        {
            return true;
        }
        for(int i=1;i<=word.size();i++)      // 由于word中是被不知长度的若干子串组合,因此这里枚举子串的长度。
        {
            if(tmp.count(word.substr(0, i)) && iscompose(word.substr(i), tmp))  // 第一个判断条件是找到第一个子串,那么第二个条件就是递归剩余子串能否在哈希表中找到
            {
                return true;
            }
        }
        return false;
    }
    string longestWord(vector<string>& words) {
        if(words.size()==0)
        {
            return "";
        }
        string ans="";

        unordered_set<string> mp(words.begin(), words.end());   // 构建单词哈希表
        for(int i=0;i<words.size();i++)
        {
            string word = words[i];
            unordered_set<string> tmp = mp;
            tmp.erase(word);     // 自己不能组合自己,因此在哈希表中先删除自己
            if(iscompose(word, tmp))    // 如果在哈希表中发现,这个单词能够被组合,那就判断长度
            {
                if(word.size()>ans.size())
                {
                    ans = word;
                }else if(word.size()==ans.size())
                {
                    ans = min(word, ans);
                }
            }
        }
        return ans;

    }
};

3、计算器

在这里插入图片描述
思路: 这里没有要求括号,并且都是非负整数,因此可以直接遍历字符串,然后遇到运算符就处理,维护一个数字栈,将数字入栈.

class Solution {
public:
    void trim(string & s)
    {
        int index = 0;
        if(!s.empty())
        {
           while( (index = s.find(' ',index)) != string::npos)
           {
               s.erase(index,1);
           }
        }
    }
    stack<int> s_num;

    int calculate(string s) {
        trim(s);     // 去除字符串中的所有空格
        int num=0;
        char c = '+';  // 可以视为表达式: 0 + 表达式,这样不改变值,而第一个运算符是+
        for(int i=0;i<=s.size();i++)   // 这里可以取到i=s.size(),是因为字符串末尾是'\0',如果不遍历到最后的话,会漏掉最后一个运算数字
        {
            if(isdigit(s[i]))
            {
                num = num*10 + (s[i] - '0');
            }else{
                if(c=='+'){      // 遇到+、-先不运算,直接入栈
                    s_num.push(num);
                }else if(c=='-'){
                    s_num.push(-num);
                }else if(c=='*'){     // 遇到*、/ 要运算完再入栈
                    int tmp = s_num.top();
                    s_num.pop();
                    num *= tmp;
                    s_num.push(num);
                }else{
                    int tmp = s_num.top();
                    s_num.pop();
                    num = tmp / num;
                    s_num.push(num);
                }
                num=0;
                c = s[i];
            }
        }
        int ans=0;
        while(s_num.size()){        // 栈里面的数字,直接相加,没有乘除法
            ans+=s_num.top();
            s_num.pop();
        }
        return ans;
    }
};

4、字母与数字

在这里插入图片描述
思路:数组只存放字母和数字,而要找子数组里面包含字母和数字的个数相同(不考虑字母、数字的长度),因此可以将数字看成1,字母看成-1,计算前缀和.
对于前缀和, 有两种情况:

  1. prefix[i]=0, 说明 0~i 的长度是包含字母和数字个数相同的子数组;
  2. 如果遇到prefix[i]==prefix[j] , 也就是相同前缀和, 那么意味着 i~j 这个区间内存在包含字母和数字个数相同的子数组.(因为相同前缀和, 意味着区间内数字有变大或变小, 但最终回到起始, 说明区间内不管怎么变, 总增量为0)
class Solution {
public:
    vector<string> findLongestSubarray(vector<string>& array) {
        int n=array.size();
        vector<int> prefix(n,0);
        unordered_map<int,int> M;   //key,left_index
        int left=0,right=-1;
        for(int i=0;i<n;++i){   // 这里将数组改造为遇到字母标记-1, 遇到数字标记1
            char ch=array[i][0];
            if(ch>='A' && ch<='z') prefix[i]=-1;
            else prefix[i]=1;
        }

        // 计算前缀和
        for(int i=1;i<n;++i){
            prefix[i]+=prefix[i-1];
        }

        for(int i=0;i<n;++i){
            auto it=M.find(prefix[i]);
            if(prefix[i]==0){  // 如果前缀和为0, 那就比较当前满足题目的长度 和 下标到数组开始位置的长度 谁更长. 因此此时表示0~i 是满足题目的子数组.
                if(right-left+1 < i+1){
                    right=i;left=0;
                }
                continue;
            }
            if(it==M.end()) M[prefix[i]]=i;  // 第一次遇到这个前缀和, 那就记录对应的下标
            else {
                if(right-left+1 < i-it->second){   // 不是第一次遇到这个前缀和, 那就将当前下标和最左端的下标 长度进行比较
                    right=i;left=it->second+1;
                }
            }
        }

        // 给出结果
        vector<string> ans;
        for(int i=left;i<=right;++i) ans.push_back(array[i]);
        return ans;
    }
};

5、2出现的次数

在这里插入图片描述
思路:直接暴力肯定超时. 需要分别统计数字中每一位数字能出现2的次数,而在看每一位数字时,要考虑其前缀、后缀.

class Solution {
public:
    int numberOf2sInRange(int n) {
        long ans=0;
        long base = 1;    // 基底
        int prex = n/10;   // 前缀
        int lastx = n%10;   // 看每一位数字
        int post=0;    // 后缀
        
        while(n!=post)     //  当后缀和原来一样大时,说明已经遍历完了
        {
            if(lastx>2)    // 如果当前数字大于2, 那么2必然出现, 前缀有多大, 就会出现多少次2.   因此统计次数为: 0~pre, 共pre+1次, 再乘上基底.
            {
                ans+=(prex+1)*base;
            }else if(lastx==2){     // 如果刚好等于2, 那么不管数字出现多少, 都会被统计
                ans+= prex*base + post + 1;
            }else{
                ans+= prex * base;    // 如果小于2, 那么只考虑前缀出现的次数
            }
            post += lastx*base;
            lastx = prex%10;
            prex = prex/10;
            base*=10;
        }


        return ans;

    }
};

6、婴儿名字

在这里插入图片描述
思路: 用哈希表来统计名字和对应的次数, 但是这里有含义相同的名字, 需要进行归类. 这里采用并查集的思想, 每个名字都保留其父亲名字(实质上是含义相同且字典序最小), 这样每个名字都有对应, 记录在另一个哈希表中. 最后遍历名字和对应的次数即可.

class Solution {
public:
    unordered_map<string, int> name2num;
    unordered_map<string, string> parent;   // 保存

    string find(string s)
    {
        if(parent.count(s)==0)
            return s;
        string root = find(parent[s]);
        parent[s] = root;
        return root;
    }

    void m_union(string s1, string s2)
    {
        s1 = find(s1);
        s2 = find(s2);
        if(s1!=s2)
        {
            if(s1<s2)
            {
                parent[s2]=s1;
            }else{
                parent[s1]=s2;
            }
        }
    }

    vector<string> trulyMostPopular(vector<string>& names, vector<string>& synonyms) {
        for(auto name : synonyms)
        {
            int pos = name.find(',');
			string n1 = name.substr(1, pos - 1);
			string n2 = name.substr(pos + 1, name.size() - pos - 2);
			m_union(n1,n2);
        }

        for(auto name : names)
        {
            int pos = name.find('(');
			string nm = name.substr(0, pos);
			int ifre = stoi(name.substr(pos + 1, name.size() - pos - 2));
			name2num[find(nm)] += ifre;
        }

        vector<string> result;
		for (auto& name : name2num)
		{
			string fre = to_string(name.second);
			result.push_back(name.first + "(" + fre + ")");
		}
		return result;
    }

};

7、主要元素

在这里插入图片描述
思路: 超过一半的元素, 其实是众数. 可以通过投票法来找到众数, 但是这里可能不存在, 而投票法的前提是众数一定存在, 因此投票完后要再验证一次是否满足条件.
在这里插入图片描述
根据主要元素的定义,主要元素的出现次数大于其他元素的出现次数之和,因此在遍历过程中,主要元素和其他元素两两抵消,最后一定剩下至少一个主要元素,此时 candidate为主要元素,且 count≥1.

class Solution {
public:
    int majorityElement(vector<int>& nums) {
        int piao=0;
        int pepole = nums[0];
        for(int i=0;i<nums.size();i++)
        {
            if(piao==0)
            {
                pepole = nums[i];
            }
            if(pepole!=nums[i])
            {
                piao--;
            }else{
                piao++;
            }
            
        }

        int no=0;
        for(int i=0;i<nums.size();i++)    // 再验证一次是否为众数
        {
            if(nums[i]==pepole)
            {
                no++;
            }
        }
        if(no>nums.size()/2)
        {
            return pepole;
        }else{
            return -1;
        }

    }
};

8、拿出最少的魔法豆(第280场周赛,超时)

在这里插入图片描述
思路: (1) 由于只拿出豆子, 不放回豆子, 因此数量必然是减少的. 又因为剩余非空袋子的豆子数量相同, 因此拿走豆子数量=总和-每袋豆子数量*袋数.
(2) 要使拿掉豆子的数量最少, 那先对数组排序, 从最少的豆子开始拿.
(3) 遍历数组, 考虑第 i 个位置, 基于(2), 0 ~ i-1 位置的豆子都要拿完, 而后面的袋子豆子数需要都等于x. 那这个x 最大值只能取beans[i], 如果比当前位置的豆子更多, 那么当前位置不能算入袋子, 必须拿空, 这样豆子被拿掉的数量更多了.

class Solution {
public:
    long long minimumRemoval(vector<int>& beans) {
        if(beans.size()==1)
        {
            return 0;
        }
        int len = beans.size();
        sort(beans.begin(), beans.end());
        long long sum=0;
        for(int i=0;i<len;i++)
        {
            sum+=beans[i];     // 计算所有豆子总数
        } 
        long long ans=LLONG_MAX;
        for(int i=0;i<len;i++)
        {
            long long res = (long long)beans[i]*(len-i);    // 计算当前位置及后面位置的袋子, 都以beans[i]为豆子数, 所剩余的豆子总数.
            ans = min(ans, sum - res);   //  sum - res 表示被拿掉的豆子数量
        }
        return ans;
        
    }
};

9、马戏团人塔(超时)

在这里插入图片描述
思路: 第一次用动态规划解,超时了. 看了题解, 用贪心 + 二分来做.

class Solution {
public:
    int bestSeqAtIndex(vector<int>& height, vector<int>& weight) {
        if(height.size()==0)
        {
            return 0;
        }
        vector<pair<int, int> > man;
        for(int i=0;i<height.size();i++)
        {
            man.push_back(make_pair(height[i], weight[i]));
        }
        //身高升序, 相同身高体重降序. 这样的话保证在数组中, 后面的身高必然大于前面的,可以直接取; 而当身高相同的话, 体重更小的人应该用来替换, 体现了贪心,能够使数组尽可能的长.
        sort(man.begin(), man.end(),
        [](const pair<int, int> & a, const pair<int, int> &b){
            if(a.first==b.first)
            {
                return a.second>b.second;
            }else{
                return a.first<b.first;
            }
        });

        vector<int> res;
        res.push_back(man[0].second);   // 第一个人的体重可以直接加入
        for(int i=1;i<height.size();i++)
        {
            if(man[i].second > res.back())     // 如果当前的体重比数组末尾的人更大, 可以直接加入, 满足条件(因为身高是升序, 必然满足)
            {
                res.push_back(man[i].second);
            }else{     // 如果当前体重更小, 而之前排序是按身高升序, 身高相同再体重降序, 体重更小说明身高相同. 那就从数组中找到恰好大于等于当前体重的人(此人身高和当前的身高相同), 进行体重的替换(换上了体重更小的人). 这样体现了贪心思想, 使体重更紧凑, 可以尽可能加入更多的人.
                int index = lower_bound(res.begin(), res.end(), man[i].second) - res.begin();
                res[index] = man[i].second;
            }
        }
        return res.size();

    }
};

10、骑士在棋盘上的概率(dfs超时)

在这里插入图片描述
在这里插入图片描述
思路: 定义dp[step][i][j]表示从(i, j)出发走了step步后还停留在棋盘上的概率。当(i,j)不在棋盘上时,dp[step][i][j]=0;当(i,j)在棋盘上且step=0时,dp[step][i][j]=1。而其他情况,dp[step][i][j] += dp[step - 1][ni][nj] / 8,是由上一步的8种情况统计得到概率。

class Solution {
public:
    vector<vector<int>> dirs = {{-2, -1}, {-2, 1}, {2, -1}, {2, 1}, {-1, -2}, {-1, 2}, {1, -2}, {1, 2}};

    double knightProbability(int n, int k, int row, int column) {
        vector<vector<vector<double>>> dp(k + 1, vector<vector<double>>(n, vector<double>(n)));
        for (int step = 0; step <= k; step++) {
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                    if (step == 0) {
                        dp[step][i][j] = 1;
                    } else {
                        for (auto & dir : dirs) {
                            int ni = i + dir[0], nj = j + dir[1];
                            if (ni >= 0 && ni < n && nj >= 0 && nj < n) {
                                dp[step][i][j] += dp[step - 1][ni][nj] / 8;
                            }
                        }
                    }
                }
            }
        }
        return dp[k][row][column];
    }
};


11、最短超串(不会

在这里插入图片描述
思路:遇到满足什么什么条件的连续区间问题,可以考虑用滑动窗口来解决。
滑动窗口的解题步骤:
1)先初始化l=0,r=0。
2)然后不断将右指针右移进行遍历,此时滑动窗口相应发生变化。
3)当区间满足条件后,再将左指针右移进行收缩区间,最终找到最短的满足条件的连续区间。

class Solution {
public:
    
    vector<int> shortestSeq(vector<int>& big, vector<int>& small) {
        unordered_map<int, int> need;
        for(int i=0;i<small.size();i++)
        {
            need[small[i]]++;      // 统计所要找的这个数组中,每个数字以及出现的次数
        }
        int diff=small.size();   // 表示所要找到的数字总数
        int l=0, r=0;    // 定义左、右指针
        int min_len = INT_MAX;
        vector<int> res;
        for(;r<big.size();r++)   // 先将右指针不断右移
        {
            if(need.find(big[r])!=need.end() && --need[big[r]]>=0)   // 如果当前右指针的数字是small中需要的数字,并且该数字的出现次数还有多余,则diff--,表示要找的数字数量变少了
            {
                diff--;
            }
            while(diff==0)    // diff==0 表示数字都被滑动窗口找到了,即当前窗口满足条件
            {
                if(r - l < min_len)    // 比较窗口大小
                {
                    min_len = r - l;
                    res = {l, r};
                }
                if(need.find(big[l])!=need.end() && ++need[big[l]]>0)      // 如果左指针的数字是small中的数字,并且假设左指针右移后该数字的出现次数>0了,说明当前窗口已经不再满足条件,因为漏了一个数字出去。因此diff++,表示要找的数字数量变多了。
                {
                    diff++;
                }
                l++;    // 将左指针右移,进行区间的收缩
            }
        }

        return res;

    }
};

12、煎饼排序(不会

在这里插入图片描述
思路:从arr.size()大小开始遍历,每次找当前数组里的最大值,然后通过两次翻转将最大值放到当前数组的尾部;而随着数组长度的缩减,每次都能将最大值排到末尾,最后当数组长度=1时,已经有序了。

class Solution {
public:
    
    vector<int> pancakeSort(vector<int>& arr) {
        vector<int> ans;
        int len = arr.size();
        
        for(int i=len;i>1;i--)
        {
            int index = max_element(arr.begin(), arr.begin()+i) - arr.begin();   // 找当前长度为 i 的情况下,数组最大值
            if(index==i-1)   // 如果最大值的索引已经在尾部,那就不用动
            {
                continue;
            }
            reverse(arr.begin(), arr.begin()+index+1);    // 进行这样两次翻转
            reverse(arr.begin(), arr.begin() + i);
            ans.push_back(index+1);
            ans.push_back(i);
        }
        return ans;

    }
};

13、最大子矩阵(不会)

在这里插入图片描述
思路:实际上是最长子序列和的二维版本。详情可看题解。

class Solution {
public:
    vector<int> getMaxMatrix(vector<vector<int>>& matrix) {
        int m = matrix.size();
        int n = matrix[0].size();
        vector<int> b(n, 0);      // 记录的是,矩形中每一列的元素和。这样就把求二维矩阵的和,转化成求b的子序列和。
        vector<int> ans(4, 0);
        int max_sum = INT_MIN;
        int ans_r1=0;
        int ans_c1=0;
        int sum=0;

        for(int i=0;i<m;i++)      // 定义矩形上界
        {
            // 每次清空b数组,因为上界变了,矩形也变了
            for(int t=0;t<n;t++)
            {
                b[t]=0;
            }

            for(int j=i;j<m;j++)    // 定义矩形下界,下界不断向下拓展,意味着矩形的高在增加
            {
                sum=0;
                for(int k=0;k<n;k++)    // 遍历每一列
                {
                    b[k]+=matrix[j][k];  
                    if(sum>0)
                    {
                        sum+=b[k];
                    }else{
                        sum = b[k];
                        ans_r1 = i;
                        ans_c1 = k;
                    }
                    if(sum>max_sum)
                    {
                        max_sum = sum;
                        ans[0] = ans_r1;
                        ans[1] = ans_c1;
                        ans[2] = j;
                        ans[3] = k;
                    }
                }

            }
        }

        return ans;
    }
};

14、元素和为目标值的子矩阵数量

在这里插入图片描述
思路:这题和上一题可以采用相同的降维方法,定义每一列的和。然后进行全局遍历,只要和等于目标值就++。思路比较简单。

class Solution {
public:
    int numSubmatrixSumTarget(vector<vector<int>>& matrix, int target) {
        int ans=0;
        int sum=0;
        int m = matrix.size();
        int n = matrix[0].size();
        vector<int> b(n, 0);  // 记录每一列的和
        
        for(int i=0;i<m;i++)
        {
            for(int j=0;j<n;j++)    // 每次枚举上边界,就要把b清空
            {
                b[j]=0;
            }
            for(int k=i;k<m;k++)   // 枚举下边界
            {
                for(int j=0;j<n;j++)   
                {
                    b[j]+=matrix[k][j];
                }
                for(int p=0;p<n;p++)   // 这里就判断,一维数组b中,有多少子序列和为target即可
                {
                    sum=0;
                    for(int q=p;q<n;q++)
                    {
                        sum+=b[q];
                        if(sum==target)
                        {
                            ans++;
                        }
                    }
                }

            }
        }

        return ans;

    }
};

15、二叉搜索树序列

在这里插入图片描述
思路:其实这题意思是要遍历二叉搜索树的所有可能性,
在这里插入图片描述
以这个为例:
1)路径的第一个元素必然是根节点12,而下一个元素的选择必然是5或19,和顺序无关;
2)假设选了5,当前路径为【12,5】,那么接下来可以选的是2、9、19,也同样和顺序无关;
3)后续同理,直到没有可选的节点了,就是一个完整的路径。
因此这里是回溯的做法,可以定义一个队列来保存之后可选择的节点,如果队列为空意味着没得选,那就路径完成。
而在(1)中,给出了5、19两种选择,因此假设当前选了5进入下一层递归,那么当递归结束返回时(准备选19),要将5再加入队列,此时路径也要去掉最后一个元素(也就是5),这样就把5留在下一层递归中去选择。

/**
 * 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:
    vector<vector<int> > ans;
    vector<int> path;   // 保存当前路径
    void dfs(deque<TreeNode*>& q)
    {
        if(q.empty())    // 已经没得选了,那就已经得到了完整路径
        {
            ans.push_back(path);
            return;
        }
        for(int i=0;i<q.size();i++)   // 还有的选,那就依次遍历可选节点
        {
            TreeNode* node = q.front();
            q.pop_front();
            path.push_back(node->val);   // 选了当前节点,然后下面分别把左右孩子入队(这是下一层递归中的可选节点)
            if(node->left)
            {
                q.push_back(node->left);
            }
            if(node->right)
            {
                q.push_back(node->right);
            }
            dfs(q);   // 进行下一层递归

			// 递归结束,这时候要消除当前节点的影响,就剔除左右孩子入队的情况
            if(node->left)
            {
                q.pop_back();
            }
            if(node->right)
            {
                q.pop_back();
            }
            q.push_back(node);     // 当前节点要再次入队,例如原来是【5,19】,现在递归选过5后,要变成【19,5】,下次选19
            path.pop_back();    // 同时路径也要剔除5

        }
        
    }

    vector<vector<int>> BSTSequences(TreeNode* root) {
        if(root==NULL)
        {
            return {{}};
        }
        deque<TreeNode*> q;   // 定义双端队列,来保留下一个候选的节点
        q.push_back(root);   // 第一个候选节点肯定是根节点
        dfs(q);

        return ans;

    }
};

回溯模版:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本层集合中元素) {
        处理结点;
        backtracking(路径,选择列表)); // 递归
        回溯,撤销处理结果;
    }
}

作者:dong-men
链接:https://leetcode-cn.com/problems/bst-sequences-lcci/solution/pei-tu-hui-su-mo-ban-xiang-xi-zhu-shi-by-dong-men/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

16、布尔运算(不会

在这里插入图片描述
思路:区间dp的做法,定义一个三维的dp数组。
dp[i][j][0]代表第i个字符到第j个字符,result=0的可能性个数。
dp[i][j][1]代表第i个字符到第j个字符,result=1的可能性个数。

class Solution {
public:
    int countEval(string s, int result) {
        int n = s.size();
        vector<vector<vector<int> > > dp(n, vector<vector<int> > (n, vector<int>(2,0)));
        // 初始化,只有一个字母的情况
        for(int i=0;i<n;i+=2)
        {
            int tmp=0;
            if(s[i]=='1')
                tmp=1;
            dp[i][i][0] = 1-tmp;
            dp[i][i][1] = tmp;
        }

		// 这里step表示一个块的长度-1。  例如有一个块 1&1,那么step=2。
        for(int step=0;step<n;step+=2)    // 枚举所有可能出现的块大小
        {
            for(int i=0;i+step<n;i+=2)       // i 表示这个块的起始位置,一定是数字
            {
                for(int j=i+1;j<i+step;j+=2)   // j 表示符号位,因此一开始是 i+1,也是+=2遍历
                {
                	// 以遍历的符号 j 为界,可以分成左右两边,对左右两边进行联合dp推
                	//step = 6时为例 假设此时块为0&1|1^0
        		   //i = 0,j = 1就把快分为(0)和(1|1^0)两部分进行dp的递推
       			   //i = 0,j = 3就把块分为(0&1)和 (1^0)两部分部分进行dp的递推
                  //i = 0,j = 5就把块分为(0&1|1)和 (0)两部分部分进行dp的递推
                    int left0 = dp[i][j-1][0];
                    int left1 = dp[i][j-1][1];
                    int right0 = dp[j+1][i+step][0];
                    int right1 = dp[j+1][i+step][1];
                    if(s[j]=='&')  
                    {
                        dp[i][i+step][0]+=left0*(right0+right1) + left1*right0;   // 要想这个块的结果为0,那必须左=0(就不管右边是多少),或者左边=1,右边必须=0
                        dp[i][i+step][1]+=left1*right1; 
                    }else if(s[j]=='|')
                    {
                        dp[i][i+step][0]+=left0*right0;
                        dp[i][i+step][1]+=left1*(right0+right1)+left0*right1;
                    }else if(s[j]=='^')
                    {
                        dp[i][i+step][0]+=left0*right0+left1*right1;
                        dp[i][i+step][1]+=left0*right1+left1*right0;
                    }

                }
            }
        }
        
        return dp[0][n-1][result];
    }
};

17、最大黑方阵(只会暴力枚举)

在这里插入图片描述
思路:动态规划,cnt[r][c][0/1]分别保存(r,c)右侧、下侧连续的黑色像素的个数。

class Solution {
public:
    vector<int> findSquare(vector<vector<int>>& matrix) {
        vector<int> ans(3, 0);
        int n = matrix.size();
        if(n == 0) return {};
        if(n == 1){
            if(matrix[0][0] == 0)
                return {0, 0, 1};
            else
                return {};
        }
        //cnt[r][c][0/1],0右侧,1下侧
        vector<vector<vector<int>>> cnt(n, vector<vector<int>>(n, vector<int>(2)));
        for(int r = n-1; r >= 0; r--){      // 要从方阵右下角开始遍历,因为这样才能让索引小的cnt保存到后面的值
            for(int c = n-1; c >= 0; c--){
                if(matrix[r][c] == 1)
                    cnt[r][c][0] = cnt[r][c][1] = 0;
                else{
                    //统计cnt[r][c][0/1]
                    if(r < n-1) cnt[r][c][1] = cnt[r+1][c][1] + 1;
                    else cnt[r][c][1] = 1;

                    if(c < n-1) cnt[r][c][0] = cnt[r][c+1][0] + 1;
                    else cnt[r][c][0] = 1;
				   //更新当前最大子方阵
                    int len = min(cnt[r][c][0], cnt[r][c][1]);//最大的可能的边长,要取短边,不然不能构成方阵
                    while(len >= ans[2]){//要答案r,c最小,所以带等号
                        if(cnt[r+len-1][c][0] >= len && cnt[r][c+len-1][1] >= len){   // 再看看另外两条边是否满足长度,注意题目只要求4条边均为黑色,而不是整个方阵都是黑色
                            //可以构成长为len的方阵
                            ans = {r, c, len};
                            break;
                        }
                        len--;
                    }
                }
            }
        }
        return ans;
    }
};

18、三数之和(超时

在这里插入图片描述
思路:基本思路是三重循环,而题目要求不重复的三元组,因此要考虑去重。
1)对数组先排序,这样就避免获得重复的三元组,要保证a<=b<=c。
2)排序后,相邻元素可能是相同的,这也要避免。
例如-1,0,1,1
先选了第一个1,但是下次遍历可能又选到第二个1,组成了相同的三元组。

// 伪代码
nums.sort()
for first = 0 .. n-1
    // 只有和上一次枚举的元素不相同,我们才会进行枚举
    if first == 0 or nums[first] != nums[first-1] then
        for second = first+1 .. n-1
            if second == first+1 or nums[second] != nums[second-1] then
                for third = second+1 .. n-1
                    if third == second+1 or nums[third] != nums[third-1] then
                        // 判断是否有 a+b+c==0
                        check(first, second, third)

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/3sum/solution/san-shu-zhi-he-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

3)在从小到大遍历b的时候,要从大到小遍历c。因为假设a+b1+c1=0,那下一个b2>b1,因此若存在a+b2+c2=0,c2一定小于c1,也就是左侧。

class Solution {
public:
    vector<vector<int>> threeSum(vector<int>& nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        vector<vector<int>> ans;
        // 枚举 a
        for (int first = 0; first < n; ++first) {
            // 需要和上一次枚举的数不相同
            if (first > 0 && nums[first] == nums[first - 1]) {
                continue;
            }
            // c 对应的指针初始指向数组的最右端
            int third = n - 1;
            int target = -nums[first];    // 此时题目变为了两数之和=target,可以用双指针来使时间复杂度从O(n2)降为O(n)
            // 枚举 b
            for (int second = first + 1; second < n; ++second) {
                // 需要和上一次枚举的数不相同
                if (second > first + 1 && nums[second] == nums[second - 1]) {
                    continue;
                }
                // 需要保证 b 的指针在 c 的指针的左侧
                while (second < third && nums[second] + nums[third] > target) {
                    --third;
                }
                // 如果指针重合,随着 b 后续的增加
                // 就不会有满足 a+b+c=0 并且 b<c 的 c 了,可以退出循环
                if (second == third) {
                    break;
                }
                if (nums[second] + nums[third] == target) {
                    ans.push_back({nums[first], nums[second], nums[third]});
                }
            }
        }
        return ans;
    }
};

作者:LeetCode-Solution
链接:https://leetcode-cn.com/problems/3sum/solution/san-shu-zhi-he-by-leetcode-solution/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

19、组合

在这里插入图片描述
思路:有1~n个数,要选其中k个数。其实就是回溯的思维,遍历每个数的时候有选、不选两种情况,进行分别的回溯即可。

class Solution {
public:
    vector<vector<int> > ans;
    vector<int> tmp;
    void dfs(int cur, int n, int k)
    {
        // 剪枝。当前已经遍历到cur位置,如果已有的数量tmp.size + 剩余的数字数量,还小于k。说明就算把剩余的数字全选上也不能满足k个数字的要求,因此可以提前返回。
        if(tmp.size() + (n - cur + 1) < k)
        {
            return;
        }
        if(tmp.size()==k)
        {
            ans.push_back(tmp);
            return;
        }
        // 选取当前的cur
        tmp.push_back(cur);
        dfs(cur+1, n, k);

        // 回溯状态
        tmp.pop_back();
        // 不选当前的cur,那就直接进入下一个状态cur+1
        dfs(cur+1, n, k);
    }

    vector<vector<int>> combine(int n, int k) {
        dfs(1, n, k);
        return ans;
    }
};

20、下一个排列 (不会

在这里插入图片描述
思路:题目要找下一个序列,这个序列要是字典序刚好更大一点的序列,如果当前序列已经是字典序最大序列(降序排列),那么下一个序列是字典序最小的序列(升序排列)。
1、首先要从后往前遍历,找到第一个i 且 nums[i]<nums[i+1]。此时i+1 ~ n-1 的序列都满足降序排列。
2、再从后往前遍历,找到第一个j 且 nums[j]>nums[i]。交换 i 和 j ,再将i+1~n-1的序列进行从小到大排列(其实就是翻转,因为有第1步可知,后面都是降序的,这样可以满足使序列变化最小),得到满足题目的下一个序列。
在这里插入图片描述

class Solution {
public:
    
    void nextPermutation(vector<int>& nums) {
        int i=nums.size()-2;
        while(i>=0 && nums[i]>=nums[i+1])
        {
            i--;
        }
        if(i>=0)
        {
            int j=nums.size()-1;
            while(j>=0 && nums[j]<=nums[i])
            {
                j--;
            }
            swap(nums[i], nums[j]);   // 找到比 i 更大的第一个数,交换之后,整个序列比原始序列更大了
            reverse(nums.begin()+i+1, nums.end());  // 使后面从小到大排列,使序列变化幅度最小

        }else{   // 说明此时是完全倒序(字典序最大),直接翻转整个数组即得到字典序最小的序列
            reverse(nums.begin(), nums.end());
        }


    }
};

21、Z字形变换

在这里插入图片描述
思路:直接模拟可行,就是比较复杂。而题解中有一种巧妙的思路,用一个标志位来判断当前遍历是从上往下,还是从下往上。

class Solution {
public:
    string convert(string s, int numRows) {
        if(numRows<2)
        {
            return s;
        }
        vector<string> vec(numRows, "");   // 定义每一行的字符串
        int i=0; 
        int flag=-1;    // 正向取、反向取的标记,控制了Z字形读取的顺序
        for(int k=0;k<s.size();k++)
        {
            vec[i]+=s[k];
            if(i==0||i==numRows-1)   // 遇到i==0,此时应该从上往下;反之是从下往上
            {
                flag = -flag;
            }
            i+=flag;
        }
        string ans="";
        for(auto str: vec)
        {
            ans+=str;
        }
        return ans;

    }
};

22、适合打劫银行的日子

在这里插入图片描述
思路:这种前后都要判断的,可以先遍历数组,把前后的情况都保存下来(这里是动态规划预处理),再进行比较。设left数组,left[i]表示i之前非递减的天数;设right数组同理。

class Solution {
public:
    vector<int> goodDaysToRobBank(vector<int>& security, int time) {
        int n = security.size();
        vector<int> left(n, 0);
        vector<int> right(n, 0);
        for(int i=1;i<n-1;i++)
        {
            if(security[i]<=security[i-1])
            {
                left[i] = left[i-1]+1;
            }
            if(security[n-i-1]<=security[n-i])
            {
                right[n-i-1] = right[n-i]+1;
            }
        }
        vector<int> ans;
        for(int i=time;i<n-time;i++)
        {
            if(left[i]>=time && right[i]>=time)
            {
                ans.push_back(i);
            }
        }
        return ans;

        
    }
};

23、最长回文子串

在这里插入图片描述
思路1 : 中心扩展的方法,遍历每个位置,然后以该位置为回文子串的中心点向左右两边扩展,直到不构成回文串为止。

class Solution {
public:
    pair<int, int> expand(string s, int left, int right)
    {
        while(left>=0&&right<s.size() && s[left]==s[right])
        {
            left--;
            right++;
        }
        return {left+1, right-1};
    }

    string longestPalindrome(string s) {
        int start=0;
        int end=0;
        int max_len=1;
        for(int i=0;i<s.size();i++)
        {
            auto [left1, right1] = expand(s, i, i);  // 中心点可能是单独的
            auto [left2, right2] = expand(s, i, i+1);  // 中心点可能是两个点  如 abba
            if(right1 - left1 + 1>max_len)
            {
                start = left1;
                end = right1;
                max_len = right1 - left1+1;
            }
            if(right2 - left2 + 1>max_len)
            {
                start = left2;
                end = right2;
                max_len = right2 - left2+1;
            }
        }
        return s.substr(start, max_len);
    }
};

思路2: 动态规划,设dp[i][j]表示i~j内是回文子串。

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        if(n<2)
        {
            return s;
        }
        vector<vector<int> > dp(n, vector<int>(n, 0));
        for(int i=0;i<n;i++)
        {
            dp[i][i]=1;    // 单个字符当然能构成回文
        }
        int start=0;
        int max_len = 1;
        for(int len=2;len<=n;len++)   // 回文子串的长度从2开始
        {
            for(int i=0;i<n;i++)   // 枚举左边界
            {
                int j = i+len-1;  // 右边界
                if(j>=n)   // 超出边界
                {
                    break;
                }
                if(s[i]!=s[j])   // 左右边界不相等,那么肯定不是回文
                {
                    dp[i][j]=0;
                }else{
                    if(j-i<3)     // 当长度不超过3,说明是回文
                    {
                        dp[i][j]=1;
                    }else{         // 超过3,要取决于内部是不是回文
                        dp[i][j]=dp[i+1][j-1];
                    }
                }
                if(dp[i][j] && j-i+1>max_len)
                {
                    max_len = j-i+1;
                    start=i;
                }
            }
        }
        return s.substr(start, max_len);
    }
};

24、

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值