LeetCode 第201场周赛 题解

今天的还是挺简单的

整理字符串

a.题目

给你一个由大小写英文字母组成的字符串 s 。
一个整理好的字符串中,两个相邻字符 s[i] 和 s[i + 1] 不会同时满足下述条件:

  • 0 <= i <= s.length - 2
  • s[i] 是小写字符,但 s[i + 1] 是相同的大写字符;反之亦然 。

请你将字符串整理好,每次你都可以从字符串中选出满足上述条件的 两个相邻 字符并删除,直到字符串整理好为止。
请返回整理好的 字符串 。题目保证在给出的约束条件下,测试样例对应的答案是唯一的。
注意:空字符串也属于整理好的字符串,尽管其中没有任何字符。

示例 1

输入:s = “leEeetcode”
输出:“leetcode”
解释:无论你第一次选的是 i = 1 还是 i = 2,都会使 “leEeetcode” 缩减为 “leetcode” 。

示例 2

输入:s = “abBAcC”
输出:""
解释:存在多种不同情况,但所有的情况都会导致相同的结果。例如:
“abBAcC” --> “aAcC” --> “cC” --> “”
“abBAcC” --> “abBA” --> “aA” --> “”

示例 3

输入:s = “s”
输出:“s”

提示:
1 <= s.length <= 100
s 只包含小写和大写英文字母

a.分析

这个反之亦然真的sb 意思就是aA和Aa都会被消
这题就无脑模拟消消乐了
会从中间删除 所以用链表这个数据结构
由于我忘了STL的list的erase参数了 所以就手撕了

当找到某个字符的后面±32和它自己一样的话 那么就可以消除
就把这两个字符都删除了 删除的顺序不影响消消乐

我在比赛的时候 删完之后的指针是重头开始再找的 其实完全没必要
删完之后指针返回前面一个就好了 因为消消乐只会影响它前后两个字符

所以总的时间复杂度是O(n)的 而我比赛时候的代码是O(n^2)的 无所谓 无脑写对就行 怎么快怎么写

a.参考代码

class Solution {
public:
    struct node{	//熟练的链表节点
        char val;
        node* last;
        node* next;
        node(char c){
            val=c;
            last=nullptr;
            next=nullptr;
        }
    };
    node* prehead;
    string makeGood(string s) {
        prehead=new node(0);
        node* p=prehead;
        for(int i=0;i<s.size();i++){	//建双向链表
            p->next=new node(s[i]);
            p->next->last=p;
            p=p->next;
        }
        bool flag=true;		//不需要再消了
        while(flag){
            flag=false;
            p=prehead->next;	//从头开始 其实没必要
            while(p){
                if(p->next){
                    if(p->val-32==p->next->val||p->val+32==p->next->val){	//要消了
                        p->last->next=p->next->next;
                        if(p->next->next)p->next->next->last=p->last;
                        flag=true;
						//其实这里写上p=p->last即可 上面的那句重头开始就不用写了
                        break;
                    }
                }
                p=p->next;
            }
        }
        string ans="";
        p=prehead->next;
        while(p){	//看下还剩下什么
            ans.push_back(p->val);
            p=p->next;
        }
        return ans;
    }
};

找出第 N 个二进制字符串中的第 K 位

b.题目

给你两个正整数 n 和 k,二进制字符串 Sn 的形成规则如下:

  • S1 = “0”
  • 当 i > 1 时,Si = Si-1 + “1” + reverse(invert(Si-1))

其中 + 表示串联操作,reverse(x) 返回反转 x 后得到的字符串,而 invert(x) 则会翻转 x 中的每一位(0 变为 1,而 1 变为 0)
例如,符合上述描述的序列的前 4 个字符串依次是:

  • S1 = “0”
  • S2 = “011”
  • S3 = “0111001”
  • S4 = “011100110110001”

请你返回 Sn 的 第 k 位字符 ,题目数据保证 k 一定在 Sn 长度范围以内。

示例 1

输入:n = 3, k = 1
输出:“0”
解释:S3 为 “0111001”,其第 1 位为 “0” 。

示例 2

输入:n = 4, k = 11
输出:“1”
解释:S4 为 “011100110110001”,其第 11 位为 “1” 。

示例 3

输入:n = 1, k = 1
输出:“0”

示例 4

输入:n = 2, k = 3
输出:“1”

提示

  • 1 <= n <= 20
  • 1 <= k <= 2^(n - 1)

b.分析

想了一下 极限把整个s20模拟出来 总长度也不过是2^20方以内
因为每次是把长度加倍然后+1

所以可以把整个sn模拟出来

这里不能模拟reverse操作 因为这个操作会把复杂度升到O(n^2)

+1然后前面顺序反转并把数字翻转 等价于:

  • 先pushback1
  • 把前面的倒序遍历pushback 就完成了顺序翻转操作
  • 在倒序遍历pushback的过程中遇到1push的是0 反之亦然

总的时间复杂度是sn最后的长度 因为我们进行了那么多次push构造出来

b.参考代码

class Solution {
public:
    char findKthBit(int n, int k) {
        string s="0";
        for(int i=2;i<=n;i++)	//递推
            s=f(s);
        return s[k-1];	//k位
    }
    string& f(string &s){	//递推的操作 写成函数逻辑清楚
        s+="1";
        for(int i=s.size()-2;i>=0;i--)
            if(s[i]=='1')s.push_back('0');
            else s.push_back('1');
        return s;
    }
};

和为目标值的最大数目不重叠非空子数组数目

c.题目

给你一个数组 nums 和一个整数 target 。
请你返回 非空不重叠 子数组的最大数目,且每个子数组中数字和都为 target 。

示例 1

输入:nums = [1,1,1,1,1], target = 2
输出:2
解释:总共有 2 个不重叠子数组(加粗数字表示) [1,1,1,1,1] ,它们的和为目标值 2 。

示例 2

输入:nums = [-1,3,5,1,4,2,-9], target = 6
输出:2
解释:总共有 3 个子数组和为 6 。
([5,1], [4,2], [3,5,1,4,2,-9]) 但只有前 2 个是不重叠的。

示例 3

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

示例 4

输入:nums = [0,0,0], target = 0
输出:3

提示

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 0 <= target <= 10^6

c.分析

观察示例2 知道题目的做法可以是把全部和为target的子数组都找出来 然后再筛掉重叠的

那么找子数组可以用前缀和的方式

假设以某个位置j为结尾的子数组 可能会有多个子数组i~j的和为target (因为数组有负数)
但是此时其实只需要拿到最近的i即可 也就是i越大越好 原因是因为贪心 区间尽可能短 那么能不重叠的区间就肯定多点

所以对于固定的位置j为结尾 假设其前缀和为x 那么只需要找到前缀和为y=x-target的位置i即可 这个i尽可能大 那么此时x-y==target
至于怎么找到对应的i 只需要以前缀和的值为键丢进hashmap就行了

把所有区间找到之后 也是一个经典的贪心问题
不重叠的选最多 直接对区间右侧排序 右侧一样的话就对长度排序
然后一路选就行了 证明略

总的时间复杂度为O(nlogn) 被排序卡了
不过应该有不排序的贪心方法

c.参考代码

class Solution {
public:
    int maxNonOverlapping(vector<int>& nums, int target) {
        int n=nums.size();
        unordered_map<int,vector<int>> mp;	//某个前缀和的i的位置
        vector<int> sum(n,0);	//前缀和
        sum[0]=nums[0];
        mp[sum[0]].push_back(0);
        for(int i=1;i<nums.size();i++){
            sum[i]=sum[i-1]+nums[i];
            mp[sum[i]].push_back(i);	//记录位置
        }
        vector<pair<int,int>> v;
        for(int i=0;i<sum.size();i++){
            if(sum[i]==target)v.push_back({0,i});	//区间符合
            int l=-1;
            for(auto j:mp[sum[i]-target])	//找到符合的区间
                if(j<i)l=j;
                else break;
            if(l!=-1)v.push_back({l+1,i});
        }
        sort(v.begin(),v.end(),[](pair<int,int> a,pair<int,int> b){
           if(a.second!=b.second)return a.second<b.second;
            return a.first>b.first;
        });		//排序后贪心
        int ans=0;
        int now=-1;
        for(int i=0;i<v.size();i++){	//按顺序加入答案
            if(v[i].first>now){
                ans++;
                now=v[i].second;
            }
        }
        return ans;
    }
};

切棍子的最小成本

d.题目

有一根长度为 n 个单位的木棍,棍上从 0 到 n 标记了若干位置。例如,长度为 6 的棍子可以标记如下:

给你一个整数数组 cuts ,其中 cuts[i] 表示你需要将棍子切开的位置。
你可以按顺序完成切割,也可以根据需要更改切割的顺序。
每次切割的成本都是当前要切割的棍子的长度,切棍子的总成本是历次切割成本的总和。对棍子进行切割将会把一根木棍分成两根较小的木棍(这两根木棍的长度和就是切割前木棍的长度)。请参阅第一个示例以获得更直观的解释。
返回切棍子的 最小总成本 。

示例 1

输入:n = 7, cuts = [1,3,4,5]
输出:16
解释:按 [1, 3, 4, 5] 的顺序切割的情况如下所示:

第一次切割长度为 7 的棍子,成本为 7 。第二次切割长度为 6 的棍子(即第一次切割得到的第二根棍子),第三次切割为长度 4 的棍子,最后切割长度为 3 的棍子。总成本为 7 + 6 + 4 + 3 = 20 。
而将切割顺序重新排列为 [3, 5, 1, 4] 后,总成本 = 16(如示例图中 7 + 4 + 3 + 2 = 16)。

示例 2

输入:n = 9, cuts = [5,6,1,4,2]
输出:22
解释:如果按给定的顺序切割,则总成本为 25 。总成本 <= 25 的切割顺序很多,例如,[4,6,5,2,1] 的总成本 = 22,是所有可能方案中成本最小的。

提示

  • 2 <= n <= 10^6
  • 1 <= cuts.length <= min(n - 1, 100)
  • 1 <= cuts[i] <= n - 1
  • cuts 数组中的所有整数都 互不相同

d.分析

挺经典的dp问题
这个区间问题 肯定需要枚举所有情况才能知道怎样更优(直觉
所以不会存在贪心的方法

每次只会切开两半 且当前切的花费是固定的 那么假如左右两部分的花费都知道了 所以就能算出这次切的总花费

所以本质就是不断在找分割点 假设了分开后的左右两部分都已知且最小 然后对比不同分割点 找最小的分割点

所以左右两部分是个规模更小的子问题 那么dp状态就可以设计为
dp[i][j]表示cuts[i]cuts[j]这个区间的最小切割花费
为什么ij不是表示点呢 因为n是1e6 枚举所有点会爆 而且也没必要 因为没有分割点的地方是不能切的 花费肯定为0

那么递推公式就显然得出是:
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+cost
此处的cost就是ij的长度 k为分割点

那么枚举的顺序当然是先枚举长度 因为大的区间需要两个小的已知区间

初始化状态也是很直接的 dp[i][i+1]=0 因为相邻的两个切割点围起来的区间 中间不能再切了 所以肯定花费是0

总的时间复杂度是填满所有状态 O(n^2)再乘上要枚举分割点的O(n)
所以总的是O(n^3) 排序的省略了

d.参考代码

int dp[105][105];
class Solution {
public:
    int minCost(int n, vector<int>& cuts) {
        memset(dp,0x3f,sizeof(dp));		//初始化为未知的正无穷
        cuts.push_back(0);	//此处把答案的区间加上 实际无意义
        cuts.push_back(n);
        sort(cuts.begin(),cuts.end());	//先排序 把cuts数组变成线段坐标
        for(int i=0;i<cuts.size()-1;i++)dp[i][i+1]=0;	//初始化合法的为0
        for(int len=2;len<cuts.size();len++)	//枚举长度
            for(int i=0;i<cuts.size()-len;i++)	//枚举起点
                for(int j=i+len,k=i+1;k<j;k++)	//终点固定的 枚举分割点
                    dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]+cuts[j]-cuts[i]);
        return dp[0][cuts.size()-1];	//答案的子问题也就是原问题
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值