LeetCode 第204场周赛 题解

这次第四题是赛后才写出来的

重复至少 K 次且长度为 M 的模式

a.题目

给你一个正整数数组 arr,请你找出一个长度为 m 且在数组中至少重复 k 次的模式。

模式 是由一个或多个值组成的子数组(连续的子序列),连续 重复多次但 不重叠 。 模式由其长度和重复次数定义。

如果数组中存在一个至少重复 k 次且长度为 m 的模式,则返回 true ,否则返回 false 。

示例 1

输入:arr = [1,2,4,4,4,4], m = 1, k = 3
输出:true
解释:模式 (4) 的长度为 1 ,且连续重复 4 次。注意,模式可以重复 k 次或更多次,但不能少于 k 次。

示例 2

输入:arr = [1,2,1,2,1,1,1,3], m = 2, k = 2
输出:true
解释:模式 (1,2) 长度为 2 ,且连续重复 2 次。另一个符合题意的模式是 (2,1) ,同样重复 2 次。

示例 3

输入:arr = [1,2,1,2,1,3], m = 2, k = 3
输出:false
解释:模式 (1,2) 长度为 2 ,但是只连续重复 2 次。不存在长度为 2 且至少重复 3 次的模式。

示例 4

输入:arr = [1,2,3,1,2], m = 2, k = 2
输出:false
解释:模式 (1,2) 出现 2 次但并不连续,所以不能算作连续重复 2 次。

示例 5

输入:arr = [2,2,2,2], m = 2, k = 3
输出:false
解释:长度为 2 的模式只有 (2,2) ,但是只连续重复 2 次。注意,不能计算重叠的重复次数。

提示

  • 2 <= arr.length <= 100
  • 1 <= arr[i] <= 100
  • 1 <= m <= 100
  • 2 <= k <= 100

a.分析

直接模拟就完事了
数据范围不大 这种题细节比较多 怎么清晰怎么写 不要老想着压缩时间和空间复杂度

模拟根据以下几步:

  • 首先可以肯定 m*k>数组长度 肯定是不行的
  • 枚举出所有长度为m的子串
    • 为了清晰明了 可以把这个子串放在vector<int> p 中作为某个子串 就不用双指针那些省空间但是麻烦自己的了
  • 对于每个p 都去向后匹配
    • 定义pos为匹配到第几个 当pos==m的时候 表示匹配成功一次 计数增加 pos归0 继续向后匹配
    • 当某个时候的p[pos]和当前数字不一样的时候 直接break就好了 因为要求是连续重复的

具体模拟看代码

总的时间复杂度为O(n^2)

a.参考代码

class Solution {
public:
    bool containsPattern(vector<int>& arr, int m, int k) {
        if(m*k>arr.size())return false;	//不可能
        int ans=0;	//全局计数
        for(int i=0;i+m-1<arr.size();i++){
            vector<int> p;	//枚举从i开始的长度为m的子串
            for(int j=i;j<i+m;j++)p.push_back(arr[j]);
            int cnt=1,pos=0;	//计数和匹配位置
            for(int j=i+m;j<arr.size();j++){
                if(p[pos]!=arr[j])break;
                pos++;
                if(pos==p.size()){	//匹配到一组了
                    cnt++;
                    pos=0;
                }
            }
            ans=max(ans,cnt);
        }
        return ans>=k;
    }
};

乘积为正数的最长子数组长度

b.题目

给你一个整数数组 nums ,请你求出乘积为正数的最长子数组的长度。

一个数组的子数组是由原数组中零个或者更多个连续数字组成的数组。

请你返回乘积为正数的最长子数组长度。

示例 1

输入:nums = [1,-2,-3,4]
输出:4
解释:数组本身乘积就是正数,值为 24 。

示例 2

输入:nums = [0,1,-2,-3,-4]
输出:3
解释:最长乘积为正数的子数组为 [1,-2,-3] ,乘积为 6 。
注意,我们不能把 0 也包括到子数组中,因为这样乘积为 0 ,不是正数。

示例 3

输入:nums = [-1,-2,-3,0,1]
输出:2
解释:乘积为正数的最长子数组是 [-1,-2] 或者 [-2,-3] 。

示例 4

输入:nums = [-1,2]
输出:1

示例 5

输入:nums = [1,2,3,5,-6,4,0,10]
输出:4

提示

  • 1 <= nums.length <= 10^5
  • -10^9 <= nums[i] <= 10^9

b.分析

看上去nums的数很大 但是没有关系 答案的乘积和数字无关 只和符号有关

这题直觉用动态规划做 为什么呢 因为区间和连续的数字的关系非常的明显
要是某个区间的乘积为正最大长度知道了 那么和这个区间连续的数字只会有两种情况:

  • 正正得正
  • 正负得负

前者就是我们想要的 后者是我们不想要的 只要是第一种情况的话 那么区间的长度就会+1 所以这个是可以通过dp来递推出来的

但是显然还存在负负得正这种情况 所以我们顺手把负数乘积的区间最大长度也做出来

因此 定义a[i]为以i为结尾的最大乘积为正的长度
定义b[i]为以i为结尾的最大乘积为负的长度
题目要求的答案即为max(a[i]) 即以某个为结尾的最长区间

那么状态转移方程也显而易见:

if(nums[i]>0){
	a[i]=a[i-1]+1;
	if(b[i-1])b[i]=b[i-1]+1;
}
else if(nums[i]<0){
	if(b[i-1])a[i]=b[i-1]+1;
	b[i]=a[i-1]+1;
}

这里有一个需要注意的就是 要判断下前一个长度为0的特殊情况
因为当前面不存在负数乘积的时候 一个正数是不能单独构成负数乘积
同理 当前面不存在正数乘积的时候 一个负数是不能单独构成正数乘积

额外要注意的就是当nums[i]为0的时候 a和b都会归0 也就是不处理

总的时间复杂度为O(n)

b.参考代码

class Solution {
public:
    int getMaxLen(vector<int>& nums) {
        int n=nums.size();
        vector<int> a(n+1,0),b(n+1,0);
        int ans=0;
        if(nums[0]>0){	//特殊处理0号位 方便递推
            a[0]=1;
            ans=1;
        }
        else if(nums[0]<0)b[0]=1;
        for(int i=1;i<n;i++){
            if(nums[i]>0){
                a[i]=a[i-1]+1;
                if(b[i-1])b[i]=b[i-1]+1;
            }
            else if(nums[i]<0){
                if(b[i-1])a[i]=b[i-1]+1;
                b[i]=a[i-1]+1;
            }
            ans=max(ans,a[i]);
        }
        return ans;
    }
};

使陆地分离的最少天数

c.题目

给你一个由若干 0 和 1 组成的二维网格 grid ,其中 0 表示水,而 1 表示陆地。岛屿由水平方向或竖直方向上相邻的 1 (陆地)连接形成。

如果 恰好只有一座岛屿 ,则认为陆地是 连通的 ;否则,陆地就是 分离的 。

一天内,可以将任何单个陆地单元(1)更改为水单元(0)。

返回使陆地分离的最少天数。

示例 1

输入:grid = [[0,1,1,0],[0,1,1,0],[0,0,0,0]]
输出:2
解释:至少需要 2 天才能得到分离的陆地。
将陆地 grid[1][1] 和 grid[0][2] 更改为水,得到两个分离的岛屿。

示例 2

输入:grid = [[1,1]]
输出:2
解释:如果网格中都是水,也认为是分离的 ([[1,1]] -> [[0,0]]),0 岛屿。

示例 3

输入:grid = [[1,0,1,0]]
输出:0
示例 4
输入:grid = [[1,1,0,1,1],
[1,1,1,1,1],
[1,1,0,1,1],
[1,1,0,1,1]]
输出:1

示例 5

输入:grid = [[1,1,0,1,1],
[1,1,1,1,1],
[1,1,0,1,1],
[1,1,1,1,1]]
输出:2

提示

  • 1 <= grid.length, grid[i].length <= 30
  • grid[i][j] 为 0 或 1

c.分析

这题比赛时候我用的是暴力 赛后发现可以取巧 且暴力能过也是因为这个巧处
先说结论: 取巧的是任意图最多变掉两个一定能够让其分割 我的暴力想法就是最多变4个肯定可以分割

说下原来的思路 本来是想着有没有办法能够贪心地去变陆地 发现并不能思考出一个比较好的方法
然后看了下数据 900矩阵的大小 可以尝试下暴力尝试出所有情况 而且我当时想的是最多只有四层 用BFS搜4层的应该也是可以过的

搜索要做以下准备:

  • 一个check()函数 表示可以判断某个图当前是否为分块
    • 对该图用dfs进行遍历分块 因为dfs入口一次总能把某个连通块遍历完 那么进入了几次dfs就证明有多少个连通块
    • 当连通块为0或2及以上即返回true

那么接下来就是用BFS暴搜
这里既然是暴搜 那么BFS 的queue就是要保存某个图的状态
因此我直接用了queue<vector<vector<int>>> q来跑bfs 相当暴力

对于某个图g 尝试对其所有陆地尝试变水 变成gg 然后去check(gg)
成功的话就返回层数 否则就把当前gg塞进队列中继续搜下一层

这样的时间复杂度是 O(nm(BFS树节点数)) 因为一次check就要遍历整个图
然后看下bfs四层一共能生成多少种情况 相当爆炸

赛后发现 最多就只会变两次
因为一个复杂岛屿肯定存在直角:
11
1
那么最多两次就能把那个角孤立出来
因此只需要去看下有没有一次就能割出分块的情况 也就是BFS的第一层
然后就不需要知道bfs的第二层是通过什么顺序切出分块的了 因为肯定可以切出
总的时间复杂度为O((nm)^2) 因为要对所有陆地进行一次check (也就是bfs的第一层)

c.参考代码

暴力

int vis[31][31];
class Solution {
public:
    int n,m;
    int minDays(vector<vector<int>>& grid) {
        n=grid.size();
        m=grid[0].size();
        queue<vector<vector<int>>> q;
        q.push(grid);
        int ans=0;
        if(check(grid))return 0;
        while(q.size()){	//BFS暴搜出所有顺序情况
            ans++;
            int t=q.size();
            while(t--){
                auto g=q.front();
                q.pop();
                for(int i=0;i<n;i++)
                    for(int j=0;j<m;j++){
                        if(!g[i][j])continue;
                        vector<vector<int>> gg=g;
                        gg[i][j]=0;
                        if(check(gg))return ans;
                        q.push(gg);
                    }
            }
        }
        return ans;
    }
    bool check(vector<vector<int>> &g){
        memset(vis,0,sizeof vis);
        int cnt=0;
        for(int i=0;i<n;i++)
            for(int j=0;j<m;j++)
                if(g[i][j]&&!vis[i][j]){ //计算连通块的数量
                    dfs(g,i,j);
                    cnt++;
                    if(cnt>1)return true;
                }
        if(!cnt)return true;
        return false;
    }
    void dfs(vector<vector<int>> &g,int i,int j){	//遍历跑连通块
        if(i<0||j<0||i>=n||j>=m)return;
        if(vis[i][j])return;
        if(!g[i][j])return;
        vis[i][j]=true;
        dfs(g,i+1,j);
        dfs(g,i-1,j);
        dfs(g,i,j+1);
        dfs(g,i,j-1);
    }
};

取巧

int vis[31][31];
class Solution {
public:
    int n,m;
    int minDays(vector<vector<int>>& grid) {
        n=grid.size();
        m=grid[0].size();
        int ans=0;
        if(check(grid))return 0;
        for(int i=0;i<n;i++)	//取巧 遍历所有第一次能变水的
            for(int j=0;j<m;j++)
                if(grid[i][j]){
                    vector<vector<int>> g=grid;
                    g[i][j]=0;
                    if(check(g))return 1;	//假如可以 就1
                }
        return 2;	//不可以 总存在直角能够2次就删除
    }
    bool check(vector<vector<int>> &g){
        memset(vis,0,sizeof vis);
        int cnt=0;
        for(int i=0;i<n;i++)
            for(int j=0;j<m;j++)
                if(g[i][j]&&!vis[i][j]){
                    dfs(g,i,j);
                    cnt++;
                    if(cnt>1)return true;
                }
        if(!cnt)return true;
        return false;
    }
    void dfs(vector<vector<int>> &g,int i,int j){
        if(i<0||j<0||i>=n||j>=m)return;
        if(vis[i][j])return;
        if(!g[i][j])return;
        vis[i][j]=true;
        dfs(g,i+1,j);
        dfs(g,i-1,j);
        dfs(g,i,j+1);
        dfs(g,i,j-1);
    }
};

将子数组重新排序得到同一个二叉查找树的方案数

d.题目

给你一个数组 nums 表示 1 到 n 的一个排列。我们按照元素在 nums 中的顺序依次插入一个初始为空的二叉查找树(BST)。请你统计将 nums 重新排序后,统计满足如下条件的方案数:重排后得到的二叉查找树与 nums 原本数字顺序得到的二叉查找树相同。

比方说,给你 nums = [2,1,3],我们得到一棵 2 为根,1 为左孩子,3 为右孩子的树。数组 [2,3,1] 也能得到相同的 BST,但 [3,2,1] 会得到一棵不同的 BST 。

请你返回重排 nums 后,与原数组 nums 得到相同二叉查找树的方案数。

由于答案可能会很大,请将结果对 10^9 + 7 取余数。

示例 1

输入:nums = [2,1,3]
输出:1
解释:我们将 nums 重排, [2,3,1] 能得到相同的 BST 。没有其他得到相同 BST 的方案了。

示例 2

输入:nums = [3,4,5,1,2]
输出:5
解释:下面 5 个数组会得到相同的 BST:
[3,1,2,4,5]
[3,1,4,2,5]
[3,1,4,5,2]
[3,4,1,2,5]
[3,4,1,5,2]

示例 3

输入:nums = [1,2,3]
输出:0
解释:没有别的排列顺序能得到相同的 BST 。

示例 4

输入:nums = [3,1,2,5,4,6]
输出:19

示例 5

输入:nums = [9,4,2,1,3,6,5,7,8,14,11,10,12,13,16,15,17,18]
输出:216212978
解释:得到相同 BST 的方案数是 3216212999。将它对 10^9 + 7 取余后得到 216212978。

提示

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= nums.length
  • nums 中所有数 互不相同 。

d.分析

这里首先得明白BST的构造

给定一个数组arr 那么arr[0]肯定就是这棵BST的根
然后后面可以根据和arr[0]根的大小相比 来分是左边还是右边

比如nums = [3,1,2,5,4,6] 那么3即这棵树的根
然后显然[1,2]是在为在左边的子树 [5,4,6]是在右边的子树

不难发现 左右子树的根是不能被改变的 因为BST的左右子树也是BST
因此不能把[1,2]的根1换成[2,1] 也不能把[5,4,6]的5进行内部移位

所以 这里就有一个解决问题的要点
BST的根是不能被重排序的 (BST的左右子树也是BST)

那么什么才可以重新排序呢 我们发现 左右子树[1,2]和[5,4,6]
左右子树出现的顺序是没有关系的 也就是12546和15462和54126都是没关系的 只要保证子树内部的顺序没有变化即可(此处先不考虑[5,4,6]的46作为左右子树可以换的情况)

那么在保证左右子树在各自的固定顺序下 左右相互之间任意顺序的排列情况共有 C(5,2)=10种 也就是5个位置 挑任意两个位置给[1,2] 其他放给[5,4,6]

但是这10种显然不是答案 因为很明显[5,4,6]也是单独可以作为一棵BST作为子问题的 可以很快发现 4和6是作为根5的左右子树 其有C(2,1)=2种选法

所以最后答案就是10×2-1=19了 记得要减去本来的1个的排序

通过上述例子可以总结出该题做法

  • 显然通过确定根之后 左右两棵子BST也能确定 左右两棵子BST是子问题 因此可以通过递归来解决
  • 某棵BST的左右子树顺序可以任意排序 为C(BST.size()-1,left.size()) 这里-1是因为要除去根 由于是组合数且left+right=BST-1 所以用left和right没啥区别
  • 左右子树排序完之后 我们可以发现子树的子树也可以重排 于是我们乘上递归的左右子树排序的方案数

具体看代码

总的时间复杂度为O(n^2) 因为每次遍历和根比较出左右子树要O(n) 然后每次深度递归子树 每次都会减少一个根 所以最多递归n层O(n)

d.参考代码

int C[1005][1005];
bool GETC=false;
const int mod=1e9+7;
class Solution {
public:
    int numOfWays(vector<int>& nums) {
        if(!GETC){		//杨辉三角算组合数 n方可以算出来
            GETC=true;
            for(int i=0;i<=1000;i++)
                for(int j=0;j<=i;j++)
                    if(!j||i==j)C[i][j]=1;
                    else C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
        }
        return (dfs(nums)+mod-1)%mod;	//直接返回这个大问题
    }
    int dfs(vector<int> nums){
        if(!nums.size())return 1;	//没有树 递归出口
        int root=nums[0];	//当前的根
        vector<int> left,right;
        for(int i=1;i<nums.size();i++){	//去把左右子树都算出来
            if(nums[i]<root)left.push_back(nums[i]);
            else right.push_back(nums[i]);
        }
        return 1ll*C[nums.size()-1][left.size()]%mod*dfs(left)%mod*dfs(right)%mod;	//根据公式算出当前的左右子树可以排序的方案数和子树的子树的方案数 然后相乘
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值