二月刷题笔记(C++)

2-1 最长的美好子字符串(X)


今天的每日一题是:1763. 最长的美好子字符串 - 力扣(LeetCode) (leetcode-cn.com)

由于总共26个字母,故使用二进制来标识。

解决方法分为:枚举、分治、滑动窗口

class Solution {
public:
    string longestNiceSubstring(string s) {
        int n = s.size();
        int maxPos = 0;
        int maxLen = 0;
        for (int i = 0; i < n; ++i) {
            int lower = 0;
            int upper = 0;
            for (int j = i; j < n; ++j) {
                if (islower(s[j])) {
                    lower |= 1 << (s[j] - 'a');
                } else {
                    upper |= 1 << (s[j] - 'A');
                }
                if (lower == upper && j - i + 1 > maxLen) {
                    maxPos = i;
                    maxLen = j - i + 1;
                }
            }
        }
        return s.substr(maxPos, maxLen);
    }
};

2-2 反转单词前缀


2000. 反转单词前缀 - 力扣(LeetCode) (leetcode-cn.com)

直接反转:

class Solution {
public:
    string reversePrefix(string word, char ch) {
        reverse(word.begin(),word.begin()+word.find(ch)+1);
        return word;
    }
};

这里需要了解:reverse()第二个参数为被反转单词的下一个单词,故+1;

find()为查找字符,找到则返回对应位置索引,否则返回-1;故不需要判断是否找到。

官方题解:

class Solution {
public:
    string reversePrefix(string word, char ch) {
        int index = word.find(ch);
        if (index != string::npos) {
            reverse(word.begin(), word.begin() + index + 1);
        }
        return word;
    }
};、

2-3 和为 K 的最少斐波那契数字数目(?)


今天的每日一题是:1414. 和为 K 的最少斐波那契数字数目 - 力扣(LeetCode) (leetcode-cn.com)

本题遍历会发生超时,使用贪心方法。

官方题解:

class Solution {
public:
    int findMinFibonacciNumbers(int k) {
        vector<int> f;
        f.emplace_back(1);
        int a = 1, b = 1;
        while (a + b <= k) {
            int c = a + b;
            f.emplace_back(c);
            a = b;
            b = c;
        }
        int ans = 0;
        for (int i = f.size() - 1; i >= 0 && k > 0; i--) {
            int num = f[i];
            if (k >= num) {
                k -= num;
                ans++;
            }
        }
        return ans;
    }
};

运行测试用例7时,注意该写法中k=k-vec[i]发生错误:

第12行:Char 18:运行时错误:有符号整数溢出:-2147483646-7不能在类型“int”中表示(solution.cpp)
总结:未定义的行为程序:未定义的行为程序。cpp:21:18

class Solution {
public:
    int findMinFibonacciNumbers(int k) {
        vector<int>vec(1,1);
        int fib=1,prefib=1,ans=0;
        while(fib<=k){
            vec.push_back(fib);
            fib=prefib+fib;
        }
        for(int i=vec.size()-1;i>=0;--i){
            while(vec[i]>=k){
                k=k-vec[i];
                ans++;
            }
            if(k==0) break;
        }
        return ans;
    }
};

出现了两个错误:

1.第一个while循环中fib一直在更新,而prefib保持不变

2.第二个while循环条件出错,应为vec[i]<=k;

结论:vec中最后一个元素为7,其等于k当执行到第二个while循环后,7-7=0 < 7 陷入死循环,k被一直减去7直到溢出。

改正后代码如下:

class Solution {
public:
    int findMinFibonacciNumbers(int k) {
        vector<int>vec(1,1);
        int fib=1,prefib=1,ans=0;
        while(fib<=k){
            vec.push_back(fib);
            int tmp=fib;
            fib=prefib+fib;
            prefib=tmp;
        }
        for(int i=vec.size()-1;i>=0;--i){
            while(vec[i]<=k){
                k=k-vec[i];
                ans++;
            }
            if(k==0) break;
        }
        return ans;
    }
};

2-4 可以形成最大正方形的矩形数目


今天的每日一题是:1725. 可以形成最大正方形的矩形数目 - 力扣(LeetCode) (leetcode-cn.com)

方法一:遍历

O(n)时间复杂度,遍历数组找到最大边矩阵个数。

class Solution {
public:
    int countGoodRectangles(vector<vector<int>>& rectangles) {
        int _max=INT_MIN,ans=0;
        for(auto & rec:rectangles){
            int edge = min(rec[0],rec[1]);
            if(edge > _max){
                _max = edge;
                ans = 1;
            }
            else if(edge == _max){
                ans++;
            }
        }
        return ans;
    }
};

2-5 黄金矿工(X)


今天的每日一题是:1219. 黄金矿工 - 力扣(LeetCode) (leetcode-cn.com)

方法一:dfs回溯

时间超时:

class Solution {
public:
    vector<vector<int>> dir={{-1,0},{1,0},{0,-1},{0,1}};
    int recur(vector<vector<int>> grid,int i,int j,int sum){
        if(i<0 ||i>grid.size()-1 || j<0 || j>grid[0].size()-1 || grid[i][j] == 0 )
        {
            return sum;
        }
        else{
            sum+=grid[i][j];
            grid[i][j]=0;
        }
        return max(max(max(recur(grid,i+1,j,sum),recur(grid,i-1,j,sum)),recur(grid,i,j+1,sum)),recur(grid,i,j-1,sum)); 
    }
    int getMaximumGold(vector<vector<int>>& grid) {
        int res=0;
        for(int i=0;i<grid.size();++i){
            for(int j=0;j<grid[0].size();++j){
                res = max(res,recur(grid,i,j,0));
            }
        }
        return res;
    }
};

官方题解:

该时间复杂度为指数级别,分析意义不大。复杂度分析:为什么dfs不会超时 - 黄金矿工 - 力扣(LeetCode) (leetcode-cn.com)

​ 我们首先在m x n个网格内枚举起点。只要格子内的数大于0,它就可以作为起点进行开采。
记枚举的起点为(i.,j),我们就可以从(i,j)开始进行递归+回溯,枚举所有可行的开采路径。我们用递归函数dfs(z, y, gold)进行枚举,其中(z,y)表示当前所在的位置,gold表示在开采位置(z,g)之前,已经拥有的黄金数量。根据题目的要求,我们需要进行如下的步骤:

黄金矿工
class Solution {
private:
    static constexpr int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};

public:
    int getMaximumGold(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        int ans = 0;

        function<void(int, int, int)> dfs = [&](int x, int y, int gold) {
            gold += grid[x][y];
            ans = max(ans, gold);

            int rec = grid[x][y];
            grid[x][y] = 0;

            for (int d = 0; d < 4; ++d) {
                int nx = x + dirs[d][0];
                int ny = y + dirs[d][1];
                if (nx >= 0 && nx < m && ny >= 0 && ny < n && grid[nx][ny] > 0) {
                    dfs(nx, ny, gold);
                }
            }

            grid[x][y] = rec;
        };

        for (int i = 0; i < m; ++i) {
            for (int j = 0; j < n; ++j) {
                if (grid[i][j] != 0) {
                    dfs(i, j, 0);
                }
            }
        }

        return ans;
    }
};

临时将数组对应元素置0递归结束再还原,避免再次创建数组。

2-6 唯一元素的和


今天的每日一题是:1748. 唯一元素的和 - 力扣(LeetCode) (leetcode-cn.com)

方法一:哈希、两次遍历

class Solution {
public:
    int sumOfUnique(vector<int>& nums) {
        unordered_map<int,int> mark;
        int sum=0;
        for(int &n:nums){
            mark[n]++;
        }
        for(auto x:mark){
            if(x.second == 1){
                sum+=x.first;
            }
        }
        return sum;
    }
};

值得注意的是x为&[a,b];

方法二:哈希、一次遍历

需要特别的判断存在的哈希是否为1,避免多次减重复数。

class Solution {
public:
    int sumOfUnique(vector<int>& nums) {
        unordered_map<int,int> state;
        int sum=0;
        for(int n:nums){
            if(!state.count(n)){
                state[n]=1;
                sum+=n;
            }
            else if(state[n]==1){
                state[n]=2;
                sum-=n;
            }
        }
        return sum;
    }
};

2-7 最长快乐字符串(x)【重做】


今天的每日一题是:1405. 最长快乐字符串 - 力扣(LeetCode) (leetcode-cn.com)

class Solution {
public:
    string longestDiverseString(int a, int b, int c) {
        string res;
        vector<pair<int,char>> arr ={{a,'a'},{b,'b'},{c,'c'}};

        while (true)
        {
            sort(arr.begin(),arr.end(),[](const pair<int,char> &p1,const pair<int,char> &p2){
                return p1.first>p2.first;
            });

            bool hasNext = false;
            for(auto &[freq,ch]:arr)
            {
                int m = res.size();
                if(freq <= 0)
                {
                    break;
                }
                if(m>=2 && res[m-2] == ch && res[m-1] ==ch)
                {
                    continue;
                }
                hasNext = true;
                res.push_back(ch);
                freq--;
                break;
            }
            if(!hasNext){
                break;
            }
        }
        return res;
    }
};

2-8 网格照明(困难)(待理解)


今天的每日一题是:1001. 网格照明 - 力扣(LeetCode) (leetcode-cn.com)

需要注意,打开的灯是 照亮了 相同行列对角线的位置,而不是把那些位置的灯也打开了

diagonal:对角线

antiDiagonal:斜对角线

class Solution {
public:
    vector<int> gridIllumination(int n, vector<vector<int>> &lamps, vector<vector<int>> &queries) {
        unordered_map<int, int> row, col, diagonal, antiDiagonal;
        auto hash_p = [](const pair<int, int> &p) -> size_t {
            static hash<long long> hash_ll;
            return hash_ll(p.first + (static_cast<long long>(p.second) << 32));
        };
        unordered_set<pair<int, int>, decltype(hash_p)> points(0, hash_p);
        for (auto &lamp : lamps) {
            if (points.count({lamp[0], lamp[1]}) > 0) {
                continue;
            }
            points.insert({lamp[0], lamp[1]});
            row[lamp[0]]++;
            col[lamp[1]]++;
            diagonal[lamp[0] - lamp[1]]++;
            antiDiagonal[lamp[0] + lamp[1]]++;
        }
        vector<int> ret(queries.size());
        for (int i = 0; i < queries.size(); i++) {
            int r = queries[i][0], c = queries[i][1];
            if (row.count(r) > 0 && row[r] > 0) {
                ret[i] = 1;
            } else if (col.count(c) > 0 && col[c] > 0) {
                ret[i] = 1;
            } else if (diagonal.count(r - c) > 0 && diagonal[r - c] > 0) {
                ret[i] = 1;
            } else if (antiDiagonal.count(r + c) > 0 && antiDiagonal[r + c] > 0) {
                ret[i] = 1;
            }
            for (int x = r - 1; x <= r + 1; x++) {
                for (int y = c - 1; y <= c + 1; y++) {
                    if (x < 0 || y < 0 || x >= n || y >= n) {
                        continue;
                    }
                    auto p = points.find({x, y});
                    if (p != points.end()) {
                        points.erase(p);
                        row[x]--;
                        col[y]--;
                        diagonal[x - y]--;
                        antiDiagonal[x + y]--;
                    }
                }
            }
        }
        return ret;
    }
};

易理解版本:

using LL = long long;
int dir[][2]={{-1,-1},{-1,0},{-1,1},{0,-1},{0,0},{0,1},{1,-1},{1,0},{1,1}};
class Solution {
public:
    vector<int> gridIllumination(int n, vector<vector<int>>& lamps, vector<vector<int>>& queries) {
        // 存储灯所在行、列、主对角线、副对角线的光的数量
        unordered_map<int,int> row,col,left,right;
        // 用来存储灯的坐标点的,便于在询问的时候,删除光的八个方向包括该光本身是否存在灯,然后将存在的灯熄灭
        set<LL> point; 
        LL N=n;
        auto change=[&](int x,int y,int c){
            row[x]+=c,col[y]+=c,right[x-y]+=c,left[x+y]+=c;
        };
        // 遍历灯:存储以上数据结构
        for(vector<int>& l:lamps){
            int x=l[0],y=l[1];
            // 重复点直接跳过
            if(point.count(x*N+y))continue;
            point.insert(x*N+y);
            change(x,y,1);
        }
        vector<int> res;
        for(vector<int>& q:queries){
            int x=q[0],y=q[1];
            // 判断(x,y)所在行、列、对角线是否存在光
            if(row[x]||col[y]||right[x-y]||left[x+y])res.push_back(1);
            else {res.push_back(0);continue;}
            // 然后将光所在点的8个方向包括该点本身的所有灯进行关闭
            for(int i=0;i<9;++i){
                int nx=x+dir[i][0],ny=y+dir[i][1];
                if(nx<0||ny<0||nx>=n||ny>=n)continue;
                // 灯存在,则进行删除灯,并关闭行列对角线上的光
                if(point.count(nx*N+ny)){
                    point.erase(nx*N+ny);
                    change(nx,ny,-1);
                }
            }
        }
        return res;
    }
};

2-9 差的绝对值为 K 的数对数目


今天的每日一题是:2006. 差的绝对值为 K 的数对数目 - 力扣(LeetCode) (leetcode-cn.com)

该题很容易先想到暴力法,即双层循环

方法一:哈希表两次遍历

遍历全部元素,哈希表计数。

再次遍历,查找并统计在该基础上多k的元素个数。

class Solution {
public:
    int countKDifference(vector<int>& nums, int k) {
        unordered_map<int,int> mp;
        for(int n:nums)
        {
            mp[n]++;
        }
        int res=0;
        for(int n:nums)
        {
            int fd=n+k;
            if(mp.count(fd))
            {
                res+=mp[fd];
            }
        }
        return res;
    }
};

方法二:哈希表一次遍历

顺序遍历,每次找能达到对应abs(k)元素的个数

i,j应该为自动顺序。即不重复即可

class Solution {
public:
    int countKDifference(vector<int>& nums, int k) {
        int res=0,n=nums.size();
        unordered_map<int,int> cnt;
        for(int i=0;i<n;++i)
        {
            res += (cnt.count(nums[i] - k)? cnt[nums[i] - k] : 0);
            res += (cnt.count(nums[i] + k)? cnt[nums[i] + k] : 0);
            ++cnt[nums[i]];
        }
        return res;
    }
};

2-10 最简分数 (X)


今天的每日一题是:1447. 最简分数 - 力扣(LeetCode) (leetcode-cn.com)

方法一:暴力枚举+数学

本题枚举出 2~denominator 作为分母, 枚举 1~numerator 作为分子。同时确保分子分母之间最小公约数为1,即分数为最简形式。

重点为如何解决分数可化问题,需要避免将其加入到返回结果中。

__gcd(denominator , numerator) == 1 ; 作为判断即可得出。(注意其前为两个下划线)

其内部实现类似以下三种:

 int gcd(int a, int b) { // 欧几里得算法
        return b == 0 ? a : gcd(b, a % b);
    }
int gcd(int a, int b) { // 更相减损法
        while (true) {
            if (a > b) a -= b;
            else if (a < b) b -= a;
            else return a;
        }
    }
 int gcd(int a, int b) { // stein
        if (a == 0 || b == 0) return Math.max(a, b);
        if (a % 2 == 0 && b % 2 == 0) return 2 * gcd(a >> 1, b >> 1);
        else if (a % 2 == 0) return gcd(a >> 1, b);
        else if (b % 2 == 0) return gcd(a, b >> 1);
        else return gcd(Math.abs(a - b), Math.min(a, b));
    }

解题代码如下:

class Solution {
public:
    vector<string> simplifiedFractions(int n) {
        vector<string> res;
        for(int denominator=2; denominator<=n; ++denominator)
        {
            for(int numerator=1; numerator<denominator; ++numerator)
            {
                if(__gcd(numerator,denominator) == 1)
                {
                    res.emplace_back(to_string(numerator) + "/" +to_string(denominator));
                }
            }
        }
        return res;
    }
};

本题值得注意的一个小技巧:向字符数组中添加由散乱字符组成的字符串的方法。

res.emplace_back(to_string(numerator) + “/” +to_string(denominator));

该题测试用例 n = 1~100,直接暴力不会超时。

2-11 学生分数的最小差值


1984. 学生分数的最小差值 - 力扣(LeetCode) (leetcode-cn.com)

该题很容易想到滑动窗口,但需要先进行排序。

方法一:排序+滑动窗口

时间复杂度:O(nlogn)**,其中 nn 是数组 \textit{nums}nums 的长度。排序需要的时间为 O(n \log n)O(nlogn),后续遍历需要的时间为 O(n)。

空间复杂度:O(logn),即为排序需要使用的栈空间。

class Solution {
public:
    int minimumDifference(vector<int>& nums, int k) {
        //滑动窗口
        int i=0,j=k-1,sum=INT_MAX,len=nums.size();
        sort(nums.begin(),nums.end());
        while(j<len)
        {
            sum=min(sum,nums[j]-nums[i]);
            i++;
            j++;
        }
        return sum;
    }
};

官方版本:

class Solution {
public:
    int minimumDifference(vector<int>& nums, int k) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        int ans = INT_MAX;
        for (int i = 0; i + k - 1 < n; ++i) {
            ans = min(ans, nums[i + k - 1] - nums[i]);
        }
        return ans;
    }
};

本题需要仔细审题,切莫认为其选定的元素按照顺序排列。

如下代码,实际效果不合题目所述

class Solution {
public:
    int minimumDifference(vector<int>& nums, int k) {
        //滑动窗口
        int i=0,j=k-1,sum=INT_MAX,len=nums.size();
        
        while(j<len)
        {
        int high=*max_element(nums.begin()+i,nums.begin()+i+k);
        int low=*min_element(nums.begin()+i,nums.begin()+i+k);
        sum=min(sum,high-low);
        i++;
        j++;
        }
        return sum;
    }
};

2-12 飞地的数量(?)


1020. 飞地的数量 - 力扣(LeetCode) (leetcode-cn.com)

经典搜索类型题目

通过题目,很容易想到遍历边界上当元素,根据边界上的陆地为源进行扩展

方法一:深度优先dfs

使用递归方法进行DFS

创建一个visted布尔类型数组,用于最终的遍历求和。

值得注意的是,visited的创建问题,其于成员函数内进行了空间初始化。

class Solution {
private:
    int m,n;
    vector<vector<bool>> visited;
public:
    vector<vector<int>> dirs={{-1,0},{1,0},{0,-1},{0,1}};
    void dfs(const vector<vector<int>>& grid, int row, int col)
    {
        if(row<0 || row>=m || col<0 || col>=n || grid[row][col]==0 || visited[row][col])
        {
            return;
        }
        visited[row][col]=true;
        for(auto & dir:dirs)
        {
            dfs(grid, row+dir[0], col+dir[1]);
        }
    }
    int numEnclaves(vector<vector<int>>& grid) {
        m=grid.size();
        n=grid[0].size();
        visited=vector<vector<bool>>(m,vector<bool>(n,false));
        for(int i=0;i<m;i++)
        {
            dfs(grid, i, 0);
            dfs(grid, i, n-1);
        }
        for(int j=0;j<n;j++)
        {
            dfs(grid, 0, j);
            dfs(grid, m-1, j);
        }
        int enclaves=0; //保存答案
        for(int i=1;i<m-1;i++)
        {
            for(int j=1;j<n-1;j++)
            {
                if(grid[i][j]==1 && !visited[i][j])
                {
                    enclaves++;
                }
            }
        }
        return enclaves;
    }
};

间复杂度:O(mn),其中 m 和 n 分别是网格 grid 的行数和列数。深度优先搜索最多访问每个单元格一次,需要 O(mn) 的时间,遍历网格统计飞地的数量也需要 O(mn) 的时间。

空间复杂度:O(mn),其中 m 和 n 分别是网格 grid 的行数和列数。空间复杂度主要取决于 visited 数组和递归调用栈空间,空间复杂度是 O(mn)

方法二:广度优先BFS

使用与方法一相同的思路,利用栈来进行BFS。

值得注意的是:vector<vector> visited = vector<vector>(m, vector(n, false));

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

    int numEnclaves(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector<vector<bool>> visited = vector<vector<bool>>(m, vector<bool>(n, false));
        queue<pair<int,int>> qu;
        for (int i = 0; i < m; i++) {
            if (grid[i][0] == 1) {
                visited[i][0] = true;
                qu.emplace(i, 0);
            }
            if (grid[i][n - 1] == 1) {
                visited[i][n - 1] = true;
                qu.emplace(i, n - 1);
            }
        }
        for (int j = 1; j < n - 1; j++) {
            if (grid[0][j] == 1) {
                visited[0][j] = true;
                qu.emplace(0, j);
            }
            if (grid[m - 1][j] == 1) {
                visited[m - 1][j] = true;
                qu.emplace(m - 1, j);
            }
        }
        while (!qu.empty()) {
            auto [currRow, currCol] = qu.front();	//注意这里进行了副本的拷贝
            qu.pop();
            for (auto & dir : dirs) {
                int nextRow = currRow + dir[0], nextCol = currCol + dir[1];
                if (nextRow >= 0 && nextRow < m && nextCol >= 0 && nextCol < n && grid[nextRow][nextCol] == 1 && !visited[nextRow][nextCol]) {
                    visited[nextRow][nextCol] = true;
                    qu.emplace(nextRow, nextCol);
                }
            }
        }
        int enclaves = 0;
        for (int i = 1; i < m - 1; i++) {
            for (int j = 1; j < n - 1; j++) {
                if (grid[i][j] == 1 && !visited[i][j]) {
                    enclaves++;
                }
            }
        }
        return enclaves;
    }
};

方法三:并查集

并查集的核心思想是计算网格中的每个陆地单元格所在的连通分量。对于网格边界上的每个陆地单元格,其所在的连通分量中的所有陆地单元格都不是飞地。如果一个陆地单元格所在的连通分量不同于任何一个网格边界上的陆地单元格所在的连通分量,则该陆地单元格是飞地。

并查集的做法是,遍历整个网格,对于网格中的每个陆地单元格,将其与所有相邻的陆地单元格做合并操作。由于需要判断每个陆地单元格所在的连通分量是否和网格边界相连,因此并查集还需要记录每个单元格是否和网格边界相连的信息,在合并操作时更新该信息。

在遍历网格完成并查集的合并操作之后,再次遍历整个网格,通过并查集中的信息判断每个陆地单元格是否和网格边界相连,统计飞地的数量。

class UnionFind {
public:
    UnionFind(const vector<vector<int>> & grid) {
        int m = grid.size(), n = grid[0].size();
        this->parent = vector<int>(m * n);
        this->onEdge = vector<bool>(m * n, false);
        this->rank = vector<int>(m * n);
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    int index = i * n + j;
                    parent[index] = index;
                    if (i == 0 || i == m - 1 || j == 0 || j == n - 1) {
                        onEdge[index] = true;
                    }
                }
            }
        }
    }

    int find(int i) {
        if (parent[i] != i) {
            parent[i] = find(parent[i]);
        }
        return parent[i];
    }

    void uni(int x, int y) {
        int rootx = find(x);
        int rooty = find(y);
        if (rootx != rooty) {
            if (rank[rootx] > rank[rooty]) {
                parent[rooty] = rootx;
                onEdge[rootx] = onEdge[rootx] | onEdge[rooty];
            } else if (rank[rootx] < rank[rooty]) {
                parent[rootx] = rooty;
                onEdge[rooty] = onEdge[rooty] | onEdge[rootx];
            } else {
                parent[rooty] = rootx;
                onEdge[rootx] = onEdge[rootx] | onEdge[rooty];
                rank[rootx]++;
            }
        }
    }

    bool isOnEdge(int i) {
        return onEdge[find(i)];
    }
private:
    vector<int> parent;
    vector<bool> onEdge;
    vector<int> rank;    
};

class Solution {
public:
    int numEnclaves(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        UnionFind uf(grid);
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 1) {
                    int index = i * n + j;
                    if (j + 1 < n && grid[i][j + 1] == 1) {
                        uf.uni(index, index + 1);
                    }
                    if (i + 1 < m && grid[i + 1][j] == 1) {
                        uf.uni(index, index + n);
                    }
                }
            }
        }
        int enclaves = 0;
        for (int i = 1; i < m - 1; i++) {
            for (int j = 1; j < n - 1; j++) {
                if (grid[i][j] == 1 && !uf.isOnEdge(i * n + j)) {
                    enclaves++;
                }
            }
        }
        return enclaves;
    }
};

2-17 骑士在棋盘上的概率(X)


骑士在棋盘上的概率 - 骑士在棋盘上的概率 - 力扣(LeetCode) (leetcode-cn.com)

方法一:动态规划

详情见题解:骑士在棋盘上的概率 - 骑士在棋盘上的概率 - 力扣(LeetCode) (leetcode-cn.com)

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];
    }
};

时间复杂度:O(K * N^2)

空间复杂度:O(K * N^2)

2-19 煎饼排序(X)


969. 煎饼排序 - 力扣(LeetCode) (leetcode-cn.com)

方法一:类选择排序

每次都将最大元素放在子数组队尾,由后到前进行排序。

一次移动通常需要进行两次反转:1、反转0~max 2、反转0~n(子数组最后一个位置元素)

这样就能够将对应最大元素移动到队尾。

class Solution {
public:
    vector<int> pancakeSort(vector<int>& arr) {
        vector<int> res;
        for(int n=arr.size(); n>1; n--)
        {
            int index = max_element(arr.begin(),arr.begin()+n) - arr.begin();
            if(index == n-1)//若当前最大位于队尾,无须反转
            {
                continue;
            }
            //两次反转,将最大元素反转到队尾
            reverse(arr.begin(), arr.begin()+index+1);
            reverse(arr.begin(), arr.begin()+n);
            res.push_back(index+1); //存储两次反转位置k
            res.push_back(n);
        }
        return res;
    }
};

时间复杂度:O(n^2),其中 n 是数组 arr 的大小。总共执行至多 n−1 次查找最大值,至多 2×(n−1) 次反转数组,而查找最大值的时间复杂度是 O(n),反转数组的时间复杂度是 O(n),因此总时间复杂度是 O(n^2)。

空间复杂度:O(1)。返回值不计入空间复杂度。

方法二:冒泡排序

详解见:【宫水三叶】冒泡排序运用题 - 煎饼排序 - 力扣(LeetCode) (leetcode-cn.com)

class Solution {
    public List<Integer> pancakeSort(int[] arr) {
        int n = arr.length;
        int[] idxs = new int[n + 10];
        for (int i = 0; i < n; i++) idxs[arr[i]] = i;
        List<Integer> ans = new ArrayList<>();
        for (int i = n; i >= 1; i--) {
            int idx = idxs[i];
            if (idx == i - 1) continue;
            if (idx != 0) {
                ans.add(idx + 1);
                reverse(arr, 0, idx, idxs);
            }
            ans.add(i);
            reverse(arr, 0, i - 1, idxs);
        }
        return ans;
    }
    void reverse(int[] arr, int i, int j, int[] idxs) {
        while (i < j) {
            idxs[arr[i]] = j; idxs[arr[j]] = i;
            int c = arr[i];
            arr[i++] = arr[j];
            arr[j--] = c;
        }
    }
}

2-20 1比特与2比特字符(X)


717. 1比特与2比特字符 - 力扣(LeetCode) (leetcode-cn.com)

该题需要注意的是需要按照数组中元素的顺序来进行组合,而不是计算数组中0与1的个数自定义组合。

方法一:正序遍历

从头遍历数组,维护索引i。

当遇到1开头代表2比特字符,走两步。

当遇到0开头代表1比特字符,只能走一步。

当最后遇到的是1开头的则i = n,为false。(n-2 + 2)

当最后遇到的是0开头的则i = n-1,为true。(n-2 +1)

class Solution {
public:
    bool isOneBitCharacter(vector<int>& bits) {
        int n=bits.size(),i=0;
        while(i<n-1)
        {
            i += bits[i]+1;
        }
        return i == n-1;
    }
};

时间O(N),空间O(1)。

方法二:倒序遍历

因为最后一个必定为0,故从后向前找到倒数第二个0。(即从n-2开始)

计算中间1的个数,若为偶数则代表最后返回单个0为true,否为0与最后一个1匹配返回一个2比特 10 为false;

sum = n-2 - i

通过将sum % 2来返回答案,因为其中-2 % 2=0,故sum可被简化:sum = n - i;

class Solution {
public:
    bool isOneBitCharacter(vector<int>& bits) {
        int n=bits.size(),i=n-2;
        while(i>=0 && bits[i])
        {
            --i;
        }
        return (n-i)%2 == 0;
    }
};

时间O(N),空间O(1);

2-21 推多米诺(X)


838. 推多米诺 - 力扣(LeetCode) (leetcode-cn.com)

该题分为两种解法,BFS和双指针。

方法一:双指针模拟

基本思路:寻找 . 两边最近被推动的骨牌,根据寻找到的对应状态来决定 . 的变化。

该方法即是以上思路的扩展,从索引0开始,可以假设left = ‘L’,寻找第一个不为 . 的right。

若越界仍未找到,可假设right = ‘R’。【假设操作不会影响两者间 . 的变化】

class Solution {
public:
    string pushDominoes(string dominoes) {
        int n = dominoes.size(), i = 0;
        char left = 'L';
        while (i < n) {
            int j = i;
            while (j < n && dominoes[j] == '.') { // 找到一段连续的没有被推动的骨牌
                j++;
            }
            char right = j < n ? dominoes[j] : 'R';
            if (left == right) { // 方向相同,那么这些竖立骨牌也会倒向同一方向
                while (i < j) {
                    dominoes[i++] = right;
                }
            } else if (left == 'R' && right == 'L') { // 方向相对,那么就从两侧向中间倒
                int k = j - 1;
                while (i < k) {
                    dominoes[i++] = 'R';
                    dominoes[k--] = 'L';
                }
            }
            left = right;
            i = j + 1;
        }
        return dominoes;
    }
};

时间O(N),空间O(1)。

方法二:BFS广度优先搜索

根据方向传导规律,1秒后旁边的骨牌被传到,开始向某一方向倾倒。

我们用一个队列 q 模拟搜索的顺序;数组 time 记录骨牌翻倒或者确定不翻倒的时间,翻倒的骨牌不会对正在翻倒或者已经翻倒的骨牌施加力;数组 force 记录骨牌受到的力,骨牌仅在受到单侧的力时会翻倒。

class Solution {
public:
    string pushDominoes(string dominoes) {
        int n = dominoes.size();
        queue<int> q;
        vector<int> time(n, - 1);
        vector<string> force(n);
        for (int i = 0; i < n; i++) {
            if (dominoes[i] != '.') {
                q.emplace(i);
                time[i] = 0;
                force[i].push_back(dominoes[i]);
            }
        }

        string res(n, '.');
        while (!q.empty()) {
            int i = q.front();
            q.pop();
            if (force[i].size() == 1) {
                char f = force[i][0];
                res[i] = f;
                int ni = (f == 'L') ? (i - 1) : (i + 1);
                if (ni >= 0 and ni < n) {
                    int t = time[i];
                    if (time[ni] == -1) {
                        q.emplace(ni);
                        time[ni] = t + 1;
                        force[ni].push_back(f);
                    } else if(time[ni] == t + 1) {
                        force[ni].push_back(f);
                    }
                }
            }
        }
        return res;
    }
};

复杂度双O(N)

2-22 好子集的数目(困难)


1994. 好子集的数目 - 力扣(LeetCode) (leetcode-cn.com)

方法一:状态压缩动态规划

详解见:好子集的数目 - 好子集的数目 - 力扣(LeetCode) (leetcode-cn.com)

class Solution {
private:
    static constexpr array<int, 10> primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
    static constexpr int num_max = 30;
    static constexpr int mod = 1000000007;

public:
    int numberOfGoodSubsets(vector<int>& nums) {
        vector<int> freq(num_max + 1);
        for (int num: nums) {
            ++freq[num];
        }

        vector<int> f(1 << primes.size());
        f[0] = 1;
        for (int _ = 0; _ < freq[1]; ++_) {
            f[0] = f[0] * 2 % mod;
        }
        
        for (int i = 2; i <= num_max; ++i) {
            if (!freq[i]) {
                continue;
            }
            
            // 检查 i 的每个质因数是否均不超过 1 个
            int subset = 0, x = i;
            bool check = true;
            for (int j = 0; j < primes.size(); ++j) {
                int prime = primes[j];
                if (x % (prime * prime) == 0) {
                    check = false;
                    break;
                }
                if (x % prime == 0) {
                    subset |= (1 << j);
                }
            }
            if (!check) {
                continue;
            }

            // 动态规划
            for (int mask = (1 << primes.size()) - 1; mask > 0; --mask) {
                if ((mask & subset) == subset) {
                    f[mask] = (f[mask] + static_cast<long long>(f[mask ^ subset]) * freq[i]) % mod;
                }
            }
        }

        int ans = 0;
        for (int mask = 1, mask_max = (1 << primes.size()); mask < mask_max; ++mask) {
            ans = (ans + f[mask]) % mod;
        }
        
        return ans;
    }
};

2-23 仅仅反转字母


917. 仅仅反转字母 - 力扣(LeetCode) (leetcode-cn.com)

方法一:双指针

需要注意的是while条件判断中需要i<j以避免当字符串中无字母时发生越界,if判断中为i > = j是为了充当结束条件。

若只为 i == j则以下测试用例执行错误:

"a-bC-dEf-ghIj"

发生溢出

class Solution {
public:
    string reverseOnlyLetters(string s) {
        int i=0,j=s.size()-1;
        while(true)
        {
            while(i<j && !isalpha(s[i])) i++;
            while(i<j && !isalpha(s[j])) j--;
            if(i>=j) break;
            else swap(s[i++],s[j--]);
        }
        return s;
    }
};

时间复杂度:O(n),其中 nn 是字符串 ss 的长度。反转过程需要 O(n),C 语言计算字符串长度需要 O(n)。

空间复杂度:O(1)或 O(n)。某些语言字符串不可变,需要 O(n) 的额外空间。

2-24 球会落何处(!)


1706. 球会落何处 - 力扣(LeetCode) (leetcode-cn.com)

方法一:模拟

球停止移动分为两种情况:

1、卡在V型通道

2、卡在边界

这里需要注意的时判断是否形成V形条件

row[col] != dir

其中col为当前元素的左邻右邻

当前通道向右(即元素值为1),此时是需要考虑其右邻元素是否为-1。(左邻不会与其形成V)

当前通道向左(即元素值为-1),此时只需要考虑其左邻元素是否为1。(右邻不影响)。

class Solution {
public:
    vector<int> findBall(vector<vector<int>> &grid) {
        int n = grid[0].size();
        vector<int> ans(n, -1);
        for (int j = 0; j < n; ++j) {
            int col = j; // 球的初始列
            for (auto &row : grid) {
                int dir = row[col];
                col += dir; // 移动球
                if (col < 0 || col == n || row[col] != dir) { // 到达侧边或 V 形
                    col = -1;
                    break;
                }
            }
            if (col >= 0) {  // 成功到达底部
                ans[j] = col;
            }
        }
        return ans;
    }
};

时间复杂度:O(mn),其中 mm 和 nn 是网格的行数和列数。外循环消耗 O(n)O(n),内循环消耗 O(m)O(m)。

空间复杂度:O(1)。返回值不计入空间。

ps:需要注意的是最后判断内不要写成ans[col] = col。否者由于col改变!=j,修改错误。

2-25 复数乘法 (!)


537. 复数乘法 - 力扣(LeetCode) (leetcode-cn.com)

了解公式 (a+bi) * (c+di) = ac + adi + bci -bd = (ac - bd) + (ad + bc )*i

方法一:模拟解析字符串

需要注意的是**stoi(num.substr(right, n-right))**中不包括i

此时right为虚部首个数字字母索引 (如:1+1i,right = 2)

但其范围包括i,但i并不在0~9的范围内,故不含i。

正确范围应为:substr(right, n-right-1);

### 解题思路

1. 双指针解析字符串表示为(实部,虚部);
2. 根据公式计算乘积;

$$ (a+bi)(c+di) = ac + (ad + bc)*i + bd*i^2 = ac - bd + (ad+bc)*i $$

### 代码

```cpp
class Solution {
public:
    string complexNumberMultiply(string num1, string num2) {
        int a = 0, b= 0, c= 0, d = 0;
        // 解析字符:双指针
        getVal(num1, a, b);
        getVal(num2, c, d);

        // 计算乘积公式为:(a+bi)(c+di) = ac + (ad + bc)*i + bd *i^2 = ac - bd + (ad+bc)i
        int x = a * c - b * d;
        int y = a*d+b*c;
        return to_string(x) +"+"+ to_string(y) + "i";
    }

    void getVal(string num, int &x, int &y){

        int n= num.size();
        int left = 0, right = 1; //right = 1 跳过第一个数符号判断
        while(right < n && num[right] != '+' && num[right] != '-'){
            right++;
        }
        x = stoi(num.substr(left, right-left));

        right++; // 跳过两数之间的符号
        
        y =  stoi(num.substr(right, n-right));
        return;
    }
};

双O(1)

方法二:正则表达式

见:复数乘法 - 复数乘法 - 力扣(LeetCode) (leetcode-cn.com)

class Solution {
public:
    string complexNumberMultiply(string num1, string num2) {
        regex re("\\+|i"); 
        vector<string> complex1(sregex_token_iterator(num1.begin(), num1.end(), re, -1), std::sregex_token_iterator());
        vector<string> complex2(sregex_token_iterator(num2.begin(), num2.end(), re, -1), std::sregex_token_iterator());
        int real1 = stoi(complex1[0]);
        int imag1 = stoi(complex1[1]);
        int real2 = stoi(complex2[0]);
        int imag2 = stoi(complex2[1]);
        return to_string(real1 * real2 - imag1 * imag2) + "+" + to_string(real1 * imag2 + imag1 * real2) + "i";
    }
};

方法三:流和复数类

class Solution {
public:
    string complexNumberMultiply(string num1, string num2) {
        auto get = [](string& s){
            complex<int> x;
            stringstream ss(s);
            int i, r; char c;
            ss >> r >> c >> i;
            return complex<int>(r, i);
        };
        auto a = get(num1) * get(num2);
        return to_string(a.real()) + '+' + to_string(a.imag()) + 'i';
    }

2-26 增量元素之间的最大差值(X)


2016. 增量元素之间的最大差值 - 力扣(LeetCode) (leetcode-cn.com)

很容易想到双层for循环固定i找j,但该时间复杂度为O(N^2)。

需要思考如何使用一次遍历解决该问题。

方法一:前缀最小值

从前向后遍历j,不断维护j前的最小元素premin。

class Solution {
public:
    int maximumDifference(vector<int>& nums) {
        int ans=-1,premin=nums[0];
        for(int x:nums)
        {
            x>premin? ans = max(ans, x - premin) : premin = x;
        }
        return ans;
    }
};

时间O(N)

方法二:单调栈

class Solution {
public:
    int maximumDifference(vector<int>& nums) {
        stack<int> S;
        int maxx = -1;
        S.push(nums[nums.size()-1]);
        int Sbottom = S.top();
        for(int i = nums.size()-2;i>=0;i--){
            while(!S.empty() && nums[i] > S.top()){
                if(S.size()>1)
                    maxx = max(maxx, Sbottom - S.top());
                S.pop();
            }
            S.push(nums[i]);
            if(S.size()==1) Sbottom = nums[i];
        }
        if(S.size()>1)
            maxx = max(maxx, Sbottom - S.top());
        return maxx==0?-1:maxx;
    }
};

2-27 最优除法(X)


553. 最优除法 - 力扣(LeetCode) (leetcode-cn.com)

方法一:数学

首先想到 x / y :当x尽可能大、y尽可能小时,结果最大。

根据规则,可知 max(x) = nums0,min(y) = ( nums1 / nums2 / nums3 /…/ numsn)

class Solution {
public:
    string optimalDivision(vector<int>& nums) {
        int n = nums.size();
        if(n == 1) return to_string(nums[0]);
        if(n == 2) return to_string(nums[0]) + "/" + to_string(nums[1]);
        string res;
        res.append(to_string(nums[0])+"/("+to_string(nums[1]));
        for(int i = 2; i<n; i++)
        {
            res.append("/"+to_string(nums[i]));
        }
        res.append(")");
        return res;
    }
};

时间O(N),空间O(1)

方法二:动态规划

该方法详情见:最优除法 - 最优除法 - 力扣(LeetCode) (leetcode-cn.com)

struct Node {
    double maxVal, minVal;
    string minStr, maxStr;
    Node() {
        this->minVal = 10000.0;
        this->maxVal = 0.0;
    }
};

class Solution {
public:
    string optimalDivision(vector<int>& nums) {
        int n = nums.size();
        vector<vector<Node>> dp(n, vector<Node>(n));

        for (int i = 0; i < n; i++) {
            dp[i][i].minVal = nums[i];
            dp[i][i].maxVal = nums[i];
            dp[i][i].minStr = to_string(nums[i]);
            dp[i][i].maxStr = to_string(nums[i]);
        }
        for (int i = 1; i < n; i++) {
            for (int j = 0; j + i < n; j++) {
                for (int k = j; k < j + i; k++) {
                    if (dp[j][j + i].maxVal < dp[j][k].maxVal / dp[k + 1][j + i].minVal) {
                        dp[j][j + i].maxVal = dp[j][k].maxVal / dp[k + 1][j + i].minVal;
                        if (k + 1 == j + i) {
                            dp[j][j + i].maxStr = dp[j][k].maxStr + "/" + dp[k + 1][j + i].minStr;
                        } else {
                            dp[j][j + i].maxStr = dp[j][k].maxStr + "/(" + dp[k + 1][j + i].minStr + ")";
                        }
                    }
                    if (dp[j][j + i].minVal > dp[j][k].minVal / dp[k + 1][j + i].maxVal) {
                        dp[j][j + i].minVal = dp[j][k].minVal / dp[k + 1][j + i].maxVal;
                        if (k + 1 == j + i) {
                            dp[j][j + i].minStr = dp[j][k].minStr + "/" + dp[k + 1][j + i].maxStr; 
                        } else {
                            dp[j][j + i].minStr = dp[j][k].minStr + "/(" + dp[k + 1][j + i].maxStr + ")"; 
                        }
                    }
                }
            }
        }
        return dp[0][n - 1].maxStr;
    }
};

双复杂度皆为O(n^3)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值