leetcode 第 301场周赛

总结

比较简单的周赛一般就不会写题解了,比如上周的周赛和这周的acwing周赛,这周的leetcode周赛思维难度还是挺大的,基本没有送分题。

题目列表

1.装满杯子需要的最短总时长

题目描述

现有一台饮水机,可以制备冷水、温水和热水。每秒钟,可以装满 2 杯 不同 类型的水或者 1 杯任意类型的水。
给你一个下标从 0 开始、长度为 3 的整数数组 amount ,其中 amount[0]、amount[1] 和 amount[2] 分别表示需要装满冷水、温水和热水的杯子数量。返回装满所有杯子所需的 最少 秒数。
示例 1:
输入:amount = [1,4,2]
输出:4
解释:下面给出一种方案:
第 1 秒:装满一杯冷水和一杯温水。
第 2 秒:装满一杯温水和一杯热水。
第 3 秒:装满一杯温水和一杯热水。
第 4 秒:装满一杯温水。
可以证明最少需要 4 秒才能装满所有杯子。
示例 2:
输入:amount = [5,4,4]
输出:7
解释:下面给出一种方案:
第 1 秒:装满一杯冷水和一杯热水。
第 2 秒:装满一杯冷水和一杯温水。
第 3 秒:装满一杯冷水和一杯温水。
第 4 秒:装满一杯温水和一杯热水。
第 5 秒:装满一杯冷水和一杯热水。
第 6 秒:装满一杯冷水和一杯温水。
第 7 秒:装满一杯热水。
示例 3:
输入:amount = [5,0,0]
输出:5
解释:每秒装满一杯冷水。
提示:
amount.length == 3
0 <= amount[i] <= 100

分析

第一道是思维题,也可以说是考察贪心思想。有两种倒水方法,同时倒两杯和只倒一杯,为了总时间最少,我们尽可能的增加倒两杯水的次数,减少倒一杯水的次数。

有个相对来说不难想到的方法是每次选择还需要倒的三种类型水中杯子数量最多的两个,这样可以避免杯子数量少的较早被倒满。由于本题数据范围较小,只有100,可以直接模拟,每次选择杯子数量最多的两个倒水,同时尝试更新剩下的杯子中最多和次多杯子的数量,当第二多数量的杯子的数量减小到0,说明只剩下最多数量的杯子了,总时间上加上剩下的杯子数量即可。

本题还可以直接推公式求解,但是我觉得周赛时不一定能想到怎么去推公式。贪心的思想还是和上面一样,每次选取数量较大的两个类型杯子倒水。设初始情况下数量最多的杯子是x,次多的是y,最少的是z。

如果y + z <= x,显然前y + z次我们可以每次倒一杯x类型的水和一杯y或者z类型的水,将y和z类型的杯子都装满,剩下的x - y - z的杯子只能单独倒了,因此总的倒水时间就是x。

如果y + z > x,我们可以先同时倒y和z类型的水,让y + z的数量尽可能的降低到x,然后再用x时间并行的倒完剩下的水。也就是说,我们需要先并行的倒完y + z - x杯的水,如果y + z - x是偶数,我们消耗(y + z - x) / 2秒就可以完成,如果是奇数的话,我们用(y + z - x) / 2秒倒完y + z - x - 1杯的水,剩下的x + 1和x杯水用x秒时间并行到,1秒时间串行倒即可。

代码

直接模拟

class Solution {
public:
    int fillCups(vector<int>& amount) {
        sort(amount.begin(),amount.end());
        int x = amount[2],y = amount[1], z = amount[0];
        int res = 0;
        while(y) {
            x--;
            y--;
            res++;
            if(y < z)   swap(y,z);
            if(x < y)   swap(x,y);
        }
        res += x;
        return res;
    }
};

公式法

class Solution {
public:
    int fillCups(vector<int>& amount) {
        sort(amount.begin(),amount.end());
        int x = amount[2],y = amount[1], z = amount[0];
        int res = 0;
        if(y + z <= x)  return x;
        else {
            int t = y + z - x;
            res += t / 2;
            if(t & 1)   res++;
            res += x;
        }
        return res;
    }
};

2.无限集中的最小数字

题目描述

现有一个包含所有正整数的集合 [1, 2, 3, 4, 5, …] 。
实现 SmallestInfiniteSet 类:
SmallestInfiniteSet() 初始化 SmallestInfiniteSet 对象以包含 所有 正整数。
int popSmallest() 移除 并返回该无限集中的最小整数。
void addBack(int num) 如果正整数 num 不 存在于无限集中,则将一个 num 添加 到该无限集中。
示例:
输入
[“SmallestInfiniteSet”, “addBack”, “popSmallest”, “popSmallest”, “popSmallest”, “addBack”, “popSmallest”, “popSmallest”, “popSmallest”]
[[], [2], [], [], [], [1], [], [], []]
输出
[null, null, 1, 2, 3, null, 1, 4, 5]
解释
SmallestInfiniteSet smallestInfiniteSet = new SmallestInfiniteSet();
smallestInfiniteSet.addBack(2); // 2 已经在集合中,所以不做任何变更。
smallestInfiniteSet.popSmallest(); // 返回 1 ,因为 1 是最小的整数,并将其从集合中移除。
smallestInfiniteSet.popSmallest(); // 返回 2 ,并将其从集合中移除。
smallestInfiniteSet.popSmallest(); // 返回 3 ,并将其从集合中移除。
smallestInfiniteSet.addBack(1); // 将 1 添加到该集合中。
smallestInfiniteSet.popSmallest(); // 返回 1 ,因为 1 在上一步中被添加到集合中,
// 且 1 是最小的整数,并将其从集合中移除。
smallestInfiniteSet.popSmallest(); // 返回 4 ,并将其从集合中移除。
smallestInfiniteSet.popSmallest(); // 返回 5 ,并将其从集合中移除。
提示:
1 <= num <= 1000
最多调用 popSmallest 和 addBack 方法 共计 1000 次

分析

这次的T2算得上本次周赛的唯一一道送分题了。数据范围只有1000,平方级别的算法都可以解决。最简单的解法就是使用hash表,插入一个元素就将该元素的hash值置为true,删除最小元素,就从头开始遍历找到第一个哈希值为true的元素,进行懒惰删除,也就是将哈希值置为false。

更加高效点的解法就是使用set存储,插入操作自动去重了,删除操作由于set自动排序,删除set里第一个元素就可以了。

代码

哈希解法

class SmallestInfiniteSet {
public:
    SmallestInfiniteSet() {
        memset(st,true,sizeof st);
    }
    
    int popSmallest() {
        for(int i = 1;i < 1005;i++) {
            if(st[i]) {
                st[i] = false;
                return i;
            }
        }
        return -1;
    }
    
    void addBack(int num) {
        st[num] = true;
    }
private:
    bool st[1005];
};

/**
 * Your SmallestInfiniteSet object will be instantiated and called as such:
 * SmallestInfiniteSet* obj = new SmallestInfiniteSet();
 * int param_1 = obj->popSmallest();
 * obj->addBack(num);
 */

set解法

class SmallestInfiniteSet {
public:
   SmallestInfiniteSet() {
       for(int i = 1;i <= 1000;i++)    s.insert(i);
   }
   
   int popSmallest() {
       int res = *s.begin();
       s.erase(s.begin());
       return res;
   }
   
   void addBack(int num) {
       s.insert(num);
   }
private:
   set<int> s;
};

3.移动片段得到字符串

题目描述

给你两个字符串 start 和 target ,长度均为 n 。每个字符串 仅 由字符 ‘L’、‘R’ 和 ‘_’ 组成,其中:

字符 ‘L’ 和 ‘R’ 表示片段,其中片段 ‘L’ 只有在其左侧直接存在一个 空位 时才能向 左 移动,而片段 ‘R’ 只有在其右侧直接存在一个 空位 时才能向 右 移动。
字符 ‘_’ 表示可以被 任意 ‘L’ 或 ‘R’ 片段占据的空位。
如果在移动字符串 start 中的片段任意次之后可以得到字符串 target ,返回 true ;否则,返回 false 。
示例 1:
输入:start = “L__R__R”, target = “L______RR”
输出:true
解释:可以从字符串 start 获得 target ,需要进行下面的移动:

  • 将第一个片段向左移动一步,字符串现在变为 “L___R__R_” 。
  • 将最后一个片段向右移动一步,字符串现在变为 “L___R___R” 。
  • 将第二个片段向右移动散步,字符串现在变为 “L______RR” 。

可以从字符串 start 得到 target ,所以返回 true 。
示例 2:
输入:start = “R_L_”, target = “__LR”
输出:false
解释:字符串 start 中的 ‘R’ 片段可以向右移动一步得到 “RL” 。
但是,在这一步之后,不存在可以移动的片段,所以无法从字符串 start 得到 target 。
示例 3:
输入:start = “R", target = "R
输出:false
解释:字符串 start 中的片段只能向右移动,所以无法从字符串 start 得到 target 。
提示:
n == start.length == target.length
1 <= n <= 105
start 和 target 由字符 ‘L’、‘R’ 和 ‘_’ 组成

分析

这题做起来也不轻松,读完题可以分析出的性质有:start如果可以通过移动变成target,那么它们当中的‘L’、‘R’和‘_’的字符个数必然相同,L和R的先后顺序也要相同。

下面采用两种方法来解决本题:

暴力模拟

思维难度最低的解法就是直接模拟,就像串匹配算法那样逐个字符比较。相等则比较下一个字符,不等则分情况讨论。需要注意的是,字符不匹配时我们只需要考虑有可能通过移动使不匹配的位置重新匹配的情况,对于其它情况直接返回false即可。

如果start当前字符是’_“,与target对应字符不匹配,那么target字符是‘R’时显然无法完成匹配,因为失配字符前面的字符都是匹配的,我们无法从start当前字符的左边找到多余的‘R’移动过来;如果target对应的字符是‘L’,那么我们可以尝试在start当前字符的后面找到最近的一个‘L’左移过来。比如”_ _ _ L"将L移动到最左边就是“L _ _ _”,其效果等价于将L与失配的那个‘_’交换。如果在start失配字符的后面没有找到’L’或者在找到‘L’之前找到了‘R’,这意味着同样没有‘L’可以左移过来完成匹配,因为‘L’无法穿越‘R’字符。

如果start当前字符是‘R’,与target对应字符不匹配,那么在target字符是L时显然无法完成匹配,只有在target字符是‘_’时,可以尝试将‘R’右移来完成匹配,这就有三种情况了:
如果‘R’右边的字符是’L’,R不能右移,就此失配。
如果‘R’右边的字符是‘_’,R右移一下,相当于与’_'交换了位置就可以匹配了。
如果‘R’右边的字符还是‘R’,这种情况就要继续向右遍历直到找到‘_’了。举个例子,“RRR_”,如果想要将‘_’移动到最左边,需要先将最后一个R右移,然后是倒数第二个,然后才是第一个,最后得到“_RRR”,我们在实现时只需要将第一个‘R’与‘_’交换即可。如果连续的R后面找不到‘_’或者后面是‘L’,也就失配了。

如果start当前的字符是‘L’,与target对应字符不匹配,由于左边的字符已经匹配,‘L’无法左移,所以就此失配。

看起来要分这么多种情况讨论,实际在实现时正如前面说的只要考虑有可能匹配的情况,不可能匹配的情况一律else返回false即可。还需要注意的是本题字符串的规模是10w,而我们在匹配时如果遇见“_ _ _ _ _ LLLLL”和“LLLLL _ _ _ _ _”这种情况,第一个字符失配,向右找到‘L’与之交换,第二个又失配,继续向右找‘L’,这样遍历的时间复杂度就是O(n2),显然会超时。解决办法也很简单,第一次向右找到最近的‘L’后,记录下找到‘L’的下标,下次遇见同样的失配场景直接在上次找到‘L’的位置后面找即可。在‘R’的后面查找‘_’也是一样,每次找到目标就记录下来,这样一来不会重复遍历,时间复杂度就降低到线性了。

暴力模拟法代码如下:

class Solution {
public:
    bool canChange(string start, string target) {
        int n = start.size();
        int last1 = 0,last2 = 0;
        for(int i = 0;i < n;i++) {
            if(start[i] == target[i])  continue;
            else {
                if(start[i] == '_' && target[i] == 'L') {
                    int p = max(i+1,last1);
                    while(p < n) {
                        if(start[p] == 'L') {
                            swap(start[i],start[p]);
                            last1 = p + 1;
                            break;
                        }
                        else if(start[p] == 'R')    return false;
                        p++;
                    }
                    if(p == n)  return false;
                }
                else if(start[i] == 'R' && target[i] == '_') {
                    int p = max(i + 1,last2);
                    while(p < n && start[p] == 'R')  p++;
                    if(p < n && start[p] == '_') {
                        swap(start[p],start[i]);
                        last2 = p + 1;
                    }
                    else    return false;
                }
                else    return false;
            }
        }
        return true;
    }
};

双指针模拟

开头我们说到,start如果可以通过移动变成target,那么它们当中的‘L’、‘R’和‘_’的字符个数必然相同,L和R的先后顺序也要相同。如果要想start可以移动变成target,还有最后一个性质就是start中的‘L’和‘R’与target中对应字符的位置应该满足先后顺序,比如start=“_ _ L_R_ _”,target=“L_ _ _ _ _ R”,之所以start能移动得到target,是因为start中的‘L’位于target中‘L’的右边,start中的‘R’位于target中‘R’的左边。

因此我们在匹配时可以使用两个指针p和q分别指向start和target,p和q先找到两个字符串中第一个非‘_’的字符。

  • 如果p和q指向的字符不同,说明start和target中L和R的先后顺序不同,不匹配。
  • 如果指向的都是‘L’,但是p < q,说明p指向的‘L’无法左移到q的位置,不匹配;
  • 如果指向的都是‘R’,但是p > q,说明p指向的‘R’无法右移到q的位置,不匹配。

p和q指向的字符满足先后顺序后,p和q可以继续右移比较,如果最后p先到达字符串末尾,q还没到达末尾,此时只要q后面的字符都是‘_’说明是匹配的,否则不匹配,q先到达末尾的情况也是一样。

class Solution {
public:
    bool canChange(string start, string target) {
        int n = start.size();
        int p = 0, q = 0;
        while(p < n || q < n) {
            while(p < n && start[p] == '_')  p++;
            while(q < n && target[q] == '_')    q++;
            if(p < n && q < n) {
                if(start[p] != target[q])   return false;
                if(start[p] == 'L' && p < q)    return false;
                if(start[p] == 'R' && p > q)    return false;
                p++,q++;
            }
            else {
                while(p < n && start[p] == '_') p++;
                while(q < n && target[q] == '_')    q++;
                if(p < n || q < n)  return false;
            }
        }
        return true;
    }
};

4.统计理想数组的数目

题目描述

给你两个整数 n 和 maxValue ,用于描述一个 理想数组 。

对于下标从 0 开始、长度为 n 的整数数组 arr ,如果满足以下条件,则认为该数组是一个 理想数组 :

每个 arr[i] 都是从 1 到 maxValue 范围内的一个值,其中 0 <= i < n 。
每个 arr[i] 都可以被 arr[i - 1] 整除,其中 0 < i < n 。
返回长度为 n 的 不同 理想数组的数目。由于答案可能很大,返回对 109 + 7 取余的结果。
示例 1:
输入:n = 2, maxValue = 5
输出:10
解释:存在以下理想数组:

  • 以 1 开头的数组(5 个):[1,1]、[1,2]、[1,3]、[1,4]、[1,5]
  • 以 2 开头的数组(2 个):[2,2]、[2,4]
  • 以 3 开头的数组(1 个):[3,3]
  • 以 4 开头的数组(1 个):[4,4]
  • 以 5 开头的数组(1 个):[5,5]

共计 5 + 2 + 1 + 1 + 1 = 10 个不同理想数组。
示例 2:

输入:n = 5, maxValue = 3
输出:11
解释:存在以下理想数组:

  • 以 1 开头的数组(9 个):
    • 不含其他不同值(1 个):[1,1,1,1,1]
    • 含一个不同值 2(4 个):[1,1,1,1,2], [1,1,1,2,2], [1,1,2,2,2], [1,2,2,2,2]
    • 含一个不同值 3(4 个):[1,1,1,1,3], [1,1,1,3,3], [1,1,3,3,3], [1,3,3,3,3]
  • 以 2 开头的数组(1 个):[2,2,2,2,2]
  • 以 3 开头的数组(1 个):[3,3,3,3,3]

共计 9 + 1 + 1 = 11 个不同理想数组。
提示:

2 <= n <= 104
1 <= maxValue <= 104

分析

本题也是比较少见的hard的DP问题。看见题目最容易想到的DP解法是用f[i][j]表示以j为最后一个数字的长度为i的理想数组的数目,问题规模和最大的数都是1w,也就是数组的规模最少是108,一个int类型数据需要4B,而108 = 100 * 106约等于100 * 220,也就是说存储空间最少要400MB,空间会超限;再来看下时间上,状态转移方程为f[i][j] = sum(f[i-1][k]),其中k为j的约数,枚举i、j、k需要立方级别的复杂度,就算优化约数的枚举也无法做的平方级别的时间复杂度,也就是说,最常见的DP解法空间上和时间上都是不可行的。

本题的数据规模虽然是1w,但是构成的理想数组里至多有14个不同的数字。因为213 = 8192,再乘2就超过了1w,含14个不同数字的理想数组是[20, 21, …, 213]。原问题是求长度为n的理想数组的个数,有多少个不同的数不确定,这些不同的数各出现了多少次也不确定,这给我们分类讨论增加了难度。我们可以先固定一个维度,比如先假设这个理想数组中不同的数字有k个。假设理想数组里面每个数都各不相同,如果我们知道了长度为k的,里面元素各不相同的理想数组的个数,能否求出由k个数字构成的长度为n的理想数组的个数呢?

比如[1,2,4]构成一个长度为3的理想数组,那么由这三个数构成的长度为10的理想数组的个数是多少呢?显然我们可以枚举每个数字最后出现的位置,4最后出现的位置就是末尾,因此不用枚举,而1和2出现的位置可以在1到9的任意两个位置,也就是一共C(9,2)种不同的排列方式,因此,由k个不同数字构成的长度为n的理想数组的排列方式有C(n-1,k-1)种。

我们剩下要做的就是求出由各不相同数字构成的理想数组的数目,为了表达方便,下面将各个数字不相同的理想数组数目简称为不相同数组数目。原问题的规模是n,我们加上了数字各不相同的条件,问题规模最大变成了14,令f[i][j]表示最后一个数字是i且长度为j的不同数组的数目。状态转移方程为f[i][j] = sum(f[k][j-1]),其中k是i的约数,求解约数不太方便,因此我们不妨将状态转移方程写成f[ki][j+1] += f[i][j]的形式,举个例子,如果要求f[6][2],原来状态的转移方向是先求出6的约数为1,2,3,6,然后f[6][2] = f[1][1] + f[2][1] + f[3][1] + f[6][1]。现在的状态转移方向是枚举f[2][1]的时候,就将f[2][1]的值加到f[4][2],f[6][2],f[8][2]等状态的上面。

for(int i = 1;i <= maxValue;i++)    f[i][1] = 1;
for(int j = 1;j < 14;j++) {
		for(int i = 1;i <= maxValue;i++) {
        		for(int k = 2;k * i <= maxValue;k++) {
        				f[k * i][j + 1] = (f[k * i][j + 1] + f[i][j]) % MOD;
        		}
         }
 }

设问题的最大数字是m,那么末尾是1的状态需要转移到的状态有m个,末尾是2的状态需要转移到的状态有m / 2个,末尾为3的状态需要转移到的状态有m / 3个,m + m / 2 + m / 3 + m / 4 +…+ 1 = m * (1 + 1/2 + 1/3 +1/4+…+1/m),后面调和级数的和是lnm级别的,因此总的状态数是14 * m * lnm,m最大是1w,这个状态数量还是不大的。

求出了所有的f[i][j]也就是知道了由j个不同数字构成的长度为j的末尾元素是i的理想数组的数量,根据前面的推论,以i为末尾的长度为j的数列,每个都能排列成C(n-1,j-1)种长度为n的理想数组,所以将每个f[i][j]乘上C(n-1,j-1),累加求下和就是最终的理想数组的数目了。

梳理下本题的思路,由于问题规模n比较大,而理想数组中不同数字的数量较少,所以可以将理想数组划分为由1,2,3,…,14个不同数字构成的理想数组这14个类别,每个类别只要求出了由k个不同数字构成长度为k的理想数组数目,就可以通过组合数公式计算出由k个不同数字构成的长度为n的理想数组数目。

代码

class Solution {
public:
    int idealArrays(int n, int maxValue) {
        const int MOD = 1e9 + 7;
        vector<vector<int> > f(maxValue + 1,vector<int>(15));
        for(int i = 1;i <= maxValue;i++)    f[i][1] = 1;
        for(int j = 1;j < 14;j++) {
            for(int i = 1;i <= maxValue;i++) {
                for(int k = 2;k * i <= maxValue;k++) {
                    f[k * i][j + 1] = (f[k * i][j + 1] + f[i][j]) % MOD;
                }
            }
        }
        vector<vector<int> > C(n + 1,vector<int>(15));
        for(int i = 0;i <= n;i++)    C[i][0] = 1;
        for(int i = 1;i <= n;i++) {
            for(int j = 1;j <= 14;j++) {
                C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % MOD;
            }
        }
        int res = 0;
        for(int i = 1;i <= maxValue;i++) {
            for(int j = 1;j <= 14;j++) {
                res = (res + (long long)f[i][j] * C[n-1][j-1]) % MOD;
            }
        }
        return res;
    }
};

你可能感兴趣的:(其它,动态规划,组合数)