LeetCode 第188场周赛 题解

吐槽
最后一题没看清楚 切了k次 草

a.用栈操作构建数组

a.题目

给你一个目标数组 target 和一个整数 n。每次迭代,需要从 list = {1,2,3…, n} 中依序读取一个数字。
请使用下述操作来构建目标数组 target :

  • Push:从 list 中读取一个新元素, 并将其推入数组中。
  • Pop:删除数组中的最后一个元素。
  • 如果目标数组构建完成,就停止读取更多元素。

题目数据保证目标数组严格递增,并且只包含 1 到 n 之间的数字。
请返回构建目标数组所用的操作序列。
题目数据保证答案是唯一的。

示例 1

输入:target = [1,3], n = 3
输出:[“Push”,“Push”,“Pop”,“Push”]
解释:
读取 1 并自动推入数组 -> [1]
读取 2 并自动推入数组,然后删除它 -> [1]
读取 3 并自动推入数组 -> [1,3]

示例 2

输入:target = [1,2,3], n = 3
输出:[“Push”,“Push”,“Push”]

示例 3

输入:target = [1,2], n = 4
输出:[“Push”,“Push”]
解释:只需要读取前 2 个数字就可以停止。

示例 4

输入:target = [2,3,4], n = 4
输出:[“Push”,“Pop”,“Push”,“Push”,“Push”]

提示

  • 1 <= target.length <= 100
  • 1 <= target[i] <= 100
  • 1 <= n <= 100
  • target 是严格递增的

a.分析

看了下题目的示例我们可以想到 正如题目所说的用栈操作 那么就当然开一个出来模拟
那么给定数字1~n怎样才能得到目标数组呢
需要的留下 不需要的丢掉
没错 以上一句话就是重点 根据这句话模拟一遍就能够得到目标数组
还有个重点就是 我们看到了示例3 它要求如果中途得到了目标数组就没必要再继续下去啦 因为再继续下去也只是加入丢弃加入丢弃

实现模拟的过程

  • 当然要开一个栈 初始为空
  • 对1~n按顺序加入
  • 如果加入之后的栈顶和目标数组对应位置不一样 那么就pop掉就好了 (对应位置显然为当前栈的大小 比如栈里面已经有两个了 那么就看target[1])
  • 如果发现栈的大小和目标数组一样大且最后一个也一样 那么就直接完成了

总的时间复杂度是只有遍历的O(n),push pop 一个元素最多一次

a.参考代码

class Solution {
public:
    vector<string> buildArray(vector<int>& target, int n) {
        vector<string> ans;
        stack<int> stk;
        for(int i=1;i<=n;i++)
        {
            while(stk.size()&&stk.top()!=target[stk.size()-1]){		//比赛时候脑抽了 其实一次最多pop掉一个 因为多了肯定之前就被pop掉
                stk.pop();
                ans.push_back("Pop");
            }
            if(stk.size()==target.size())break;	//已经完成了
            ans.push_back("Push");
            stk.push(i);
        }
        return ans;
    }
};

b.形成两个异或相等数组的三元组数目

b.题目

给你一个整数数组 arr
现需要从数组中取三个下标 ijk ,其中 (0 <= i < j <= k < arr.length) 。
a 和 b 定义如下:

  • a = arr[i] ^ arr[i + 1] ^ … ^ arr[j - 1]
  • b = arr[j] ^ arr[j + 1] ^ … ^ arr[k]

注意:^ 表示 按位异或 操作。
请返回能够令 a == b 成立的三元组 (i, j , k) 的数目。

示例 1

输入:arr = [2,3,1,6,7]
输出:4
解释:满足题意的三元组分别是 (0,1,2), (0,2,2), (2,3,4) 以及 (2,4,4)

示例 2

输入:arr = [1,1,1,1,1]
输出:10

示例 3

输入:arr = [2,3]
输出:0

示例 4

输入:arr = [1,3,5,7,9]
输出:3

示例 5

输入:arr = [7,11,12,9,5,2,7,17,22]
输出:8

提示

  • 1 <= arr.length <= 300
  • 1 <= arr[i] <= 10^8

b.分析

看到数组长度最多只有300 我立刻就想到了直接暴力
毕竟暴力把i j k 都尝试下 2.7e7这样数据量还是可以过的
那么既然想到了暴力 继续思考 会发现不仅仅只有枚举区间的复杂度 还有把区间的a和b求出来的时间
如果在枚举完确定区间后再去把区间内一个个异或的话 这样的复杂度就得再乘个300是不能接受的

很快看到了异或 一个区间异或出来其实就是一个数 然后异或拥有性质

  • 一个数异或它自身=0
  • 一个数异或0不变
    那么我们可以推论出 区间a~b的异或X 异或上 区间a~c的异或Y 得出来的就是X^Y=c+1~b的异或Z
    因为Y^Z=X 根据性质 XY=YZ^Y=Z
    根据我们的推论 可以发现 区间的异或可以直接用前缀和思想来预处理

暴力枚举i,j,k的时间复杂度是O(n^3) 前缀和预处理O(n)之后在暴力里面获取区间异或是O(1)
所以总的时间复杂度是O(n^3)

b.参考代码

class Solution {
public:
    int countTriplets(vector<int>& arr) {
        int n=arr.size();
        vector<int> pre;
        pre.push_back(arr[0]);
        for(int i=1;i<n;i++)
            pre.push_back(pre[i-1]^arr[i]);		//预处理
        int ans=0;
        for(int i=0;i<n-1;i++)	//其实ijk枚举的顺序没有关系
            for(int k=i+1;k<n;k++)
                for(int j=i+1;j<=k;j++)
                {
                    int a=i?pre[j-1]^pre[i-1]:pre[j-1];	//公式直接获取就完事了
                    int b=pre[k]^pre[j-1];
                    if(a==b)ans++;	//计数
                }
        return ans;
    }
};

c.收集树上所有苹果的最少时间

c.题目

给你一棵有 n 个节点的无向树,节点编号为 0n-1 ,它们中有一些节点有苹果。通过树上的一条边,需要花费 1 秒钟。你从 节点 0 出发,请你返回最少需要多少秒,可以收集到所有苹果,并回到节点 0。
无向树的边由 edges 给出,其中 edges[i] = [fromi, toi] ,表示有一条边连接 fromtoi 。除此以外,还有一个布尔数组 hasApple ,其中 hasApple[i] = true 代表节点 i 有一个苹果,否则,节点 i 没有苹果。

示例 1

输入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,true,true,false]
输出:8
解释:上图展示了给定的树,其中红色节点表示有苹果。一个能收集到所有苹果的最优方案由绿色箭头表示。

示例 2

输入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,false,true,false]
输出:6
解释:上图展示了给定的树,其中红色节点表示有苹果。一个能收集到所有苹果的最优方案由绿色箭头表示。

示例 3

输入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,false,false,false,false,false]
输出:0

提示

  • 1 <= n <= 10^5
  • edges.length == n-1
  • edges[i].length == 2
  • 0 <= fromi, toi <= n-1
  • fromi < toi
  • hasApple.length == n

c.分析

在纸上自己多画几个样例之后会发现重要的结论:

  • 没有所谓的最少遍历方式 你只能一步一步的找到苹果然后一步一步返回到0
  • 多余的步是不能走的
    • 走重复了
    • 走多余了

因为你是不能跳着走的 所以没有什么取巧的需要 也就是不用其他算法预判
那么只需要解决不走多余的步就行了

不走重复
显然 这个你任选一种遍历树的方法都不会重复节点 这里层次遍历应该是不太合适的
不走多余
观察后发现 只要我们能预先直到某个子树下完全没有苹果 那么就肯定不走

分析完了后我们可以用dfs序来不走重复 然后不走多余的预先我们可以采用递归的判断
所以做法就是:

  • 询问这个节点的子节点有没有苹果
    • 有 加上子节点这条路的边长度
    • 没有 直接忽略
  • 这个节点是不是子节点都没有苹果 递归边界
    • 没有
      • 如果该节点有苹果 返回1
      • 没有 返回0
    • 有 那么这个节点的总边长度+1 因为该节点必须作为遍历子节点的经过 (虽然无用) 所以要+1表示伸上去一条边

那么显然这个从0出发回到0意味着每个边都要走两次
所以直接把最后得出来的边数结果×2
当然 根节点有苹果情况要特判 因为根节点并没有伸上去的一条边

总的时间复杂度是dfs遍历树的O(n) n为节点数

c.参考代码

class Solution {
public:
    vector<bool> apple;	//全局有没有苹果
    vector<vector<int>> tree;	//二维数组模拟保存树
    int minTime(int n, vector<vector<int>>& edges, vector<bool>& hasApple) {
        apple=hasApple;
        tree=vector<vector<int>>(n);
        for(int i=0;i<edges.size();i++)
            tree[edges[i][0]].push_back(edges[i][1]);	//建树
        int ans=(dfs(0)-1)*2;
        if(ans<0)return 0;	//根节点特判 有可能只有根节点有
        return ans;
    }
    int dfs(int t)
    {
        int ret=0;
        for(int i=0;i<tree[t].size();i++)	//遍历子树
            ret+=dfs(tree[t][i]);
        if(!ret&&apple[t])ret=1;	//如果子树都没有但自己有
        else if(ret)ret++;	//子树有 那么自己要作为一个路过的节点
        return ret;
    }
};

d.切披萨的方案数

d.题目

给你一个 rows x cols 大小的矩形披萨和一个整数 k ,矩形包含两种字符: ‘A’ (表示苹果)和 ‘.’ (表示空白格子)。你需要切披萨 k-1 次,得到 k 块披萨并送给别人。
切披萨的每一刀,先要选择是向垂直还是水平方向切,再在矩形的边界上选一个切的位置,将披萨一分为二。如果垂直地切披萨,那么需要把左边的部分送给一个人,如果水平地切,那么需要把上面的部分送给一个人。在切完最后一刀后,需要把剩下来的一块送给最后一个人。
请你返回确保每一块披萨包含 至少 一个苹果的切披萨方案数。由于答案可能是个很大的数字,请你返回它对 10^9 + 7 取余的结果。

示例 1

输入:pizza = [“A…”,“AAA”,"…"], k = 3
输出:3
解释:上图展示了三种切披萨的方案。注意每一块披萨都至少包含一个苹果。

示例 2

输入:pizza = [“A…”,“AA.”,"…"], k = 3
输出:1

示例 3

输入:pizza = [“A…”,“A…”,"…"], k = 1
输出:1

提示

  • 1 <= rows, cols <= 50
  • rows == pizza.length
  • cols == pizza[i].length
  • 1 <= k <= 10
  • pizza 只包含字符 ‘A’ 和 ‘.’ 。

d.分析

看完题目之后发现其实就是个模拟
给你一个矩阵 你有以下选择

  • 在某行切一刀 剩下下面的
  • 在某列切一刀 剩下右边的

那么根据题目你还要判断下切的是否合法

  • 在某行切一刀之后上面的要至少有一个苹果
  • 在某列切一刀之后左边的要至少有一个苹果

最后就看一下最后一刀是否合法 即切剩下的那个矩形要有至少一个苹果

你会发现 这道题就这样完了
那么暴力的做法就是每一步都按照以上步骤去搜索
最后一刀的时候 如果合法就返回1 不合法就返回0
显然在每个情况

  • 对于行你有n-1种选择 n为当前情况行数
  • 对于列你有m-1种选择 m为当前情况列数

那么总的时间复杂度就是O(n+m)^k 不能接受

离答案很接近了 考虑优化
比如
我在k=10的时候把第一行切掉 然后再在k=9的时候把第一列切掉 那么就剩下了一个还能切k=8一个披萨
我在k=10的时候把第一列切掉 然后再在k=9的时候把第一行切掉 那么就剩下了一个还能切k=8一个披萨
重点
你会发现这两个披萨是一模一样的 且能切的次数也一样 所以他们有多少种方案也是一样的

所以这一个披萨就是一个子问题 而这个子问题之前可能已经解决了的 所以我们想到了动态规划
又由于我们已经用搜索的思想捋清了思路 所以就直接用记忆化搜索实现

所以我们只需要把出现过的某个披萨记录下来就行了

披萨的状态一共有O(n * m * k) 因为每次切 披萨的右下角一定是保留着的 所以我们只需要记录状态从左上哪里开始就好 还有剩下几次能切
判断是否有苹果是O(n * m)的
所以总的时间复杂度是O(n^2 * m^2 * k)

d.参考代码

int mem[51][51][11];	//记忆披萨状态
vector<string> g;
const int mod=1e9+7;
int n,m;	//原始大小的右下角是多大
class Solution {
public:
    int ways(vector<string>& pizza, int k) {
        g=pizza;
        n=pizza.size();
        m=pizza[0].size();
        memset(mem,-1,sizeof(mem));		//没有记录的用-1表示
        int ans=ms(0,0,k);	//一开始左上角是0,0
        return ans;
    }
    int ms(int row,int col,int k)
    {
        int &x=mem[row][col][k];	//简写
        if(x!=-1)return x;	//已经见过这个情况的披萨了
        if(k==1){	//最后一刀  注意 不是刀数为0的时候  是分成k块 只需要切k-1次
            if(check(row,n-1,col,m-1))return x=1;
            return x=0;
        }
        int sum=0;
        for(int i=row+1;i<n;i++)	//对于每一行都试下这样切
            if(check(row,i-1,col,m-1))sum=(sum+ms(i,col,k-1))%mod;
        for(int i=col+1;i<m;i++)	//对于每一列都试下这样切
            if(check(row,n-1,col,i-1))sum=(sum+ms(row,i,k-1))%mod;
        x=sum;
        return x;
    }
    bool check(int r1,int r2,int c1,int c2)		//暴力判断这个是否有苹果
    {
        for(int i=r1;i<=r2;i++)
            for(int j=c1;j<=c2;j++)
                if(g[i][j]=='A')return true;
        return false;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值