LeetCode 89 双周赛

2437. 有效时间的数目

给你一个长度为 5 的字符串 time ,表示一个电子时钟当前的时间,格式为 "hh:mm"最早 可能的时间是 "00:00"最晚 可能的时间是 "23:59"

在字符串 time 中,被字符 ? 替换掉的数位是 未知的 ,被替换的数字可能是 09 中的任何一个。

请你返回一个整数 answer ,将每一个 ? 都用 09 中一个数字替换后,可以得到的有效时间的数目。

示例

输入:time = "?5:00"
输出:2
解释:我们可以将 ? 替换成 0 或 1 ,得到 "05:00" 或者 "15:00" 。注意我们不能替换成 2 ,因为时间 "25:00" 是无效时间。所以我们有两个选择。

思路

思路一:分类讨论,又称 O ( 1 ) O(1) O(1) 战神,哈哈哈 ,一堆if else

// C++
class Solution {
public:
    int countTime(string s) {
        int ans = 1;
        char h1 = s[0], h2 = s[1];
        if (h1 == '?') {
            if (h2 == '?') ans *= 24;
            else if (h2 <= '3') ans *= 3; // 0 1 2
            else ans *= 2; // 0 1 
        } else {
            if (h2 == '?') {
                //x?
                if (h1 == '2') ans *= 4; //0 1 2 3
                else ans *= 10;
            }
        }
        
        char m1 = s[3], m2 = s[4];
        if (m1 == '?') {
            if (m2 == '?') ans *= 60;
            else ans *= 6; // 0 1 2 3 4 5
        } else {
            if (m2 == '?') ans *= 10;
        }
        return ans;
    }
};

思路二:暴力枚举+判断有效性

所有情况一共就24 * 60 = 1440种,可以直接枚举所有情况,然后判断是否有效。

// Java
class Solution {
    public int countTime(String t) {
        int ans = 0;
        for (int h = 0; h < 24; h++) {
            for (int m = 0; m < 60; m++) {
                String s = String.format("%02d:%02d", h, m);
                boolean valid = true;
                for (int i = 0; i < 5; i++) {
                    char cs = s.charAt(i), ct = t.charAt(i);
                    if (ct != '?' && ct != cs) {
                        valid = false;
                        break;
                    }
                }
                if (valid) ans++;
            }
        }
        return ans;
    }
}

2438. 二的幂数组中查询范围内的乘积

给你一个正整数 n ,你需要找到一个下标从 0 开始的数组 powers ,它包含 最少 数目的 2 的幂,且它们的和为 npowers 数组是 非递减 顺序的。根据前面描述,构造 powers 数组的方法是唯一的。

同时给你一个下标从 0 开始的二维整数数组 queries ,其中 queries[i] = [left_i, right_i] ,其中 queries[i] 表示请你求出满足 left_i <= j <= right_i 的所有 powers[j] 的乘积。

请你返回一个数组 answers ,长度与 queries 的长度相同,其中 answers[i]是第 i 个查询的答案。由于查询的结果可能非常大,请你将每个 answers[i] 都对 10^9 + 7 取余

提示:

  • 1 <= n <= 10^9
  • 1 <= queries.length <= 10^5
  • 0 <= start[i] <= end[i] < powers.length

示例

输入:n = 15, queries = [[0,1],[2,2],[0,3]]
输出:[2,4,64]
解释:
对于 n = 15 ,得到 powers = [1,2,4,8] 。没法得到元素数目更少的数组。
第 1 个查询的答案:powers[0] * powers[1] = 1 * 2 = 2 。
第 2 个查询的答案:powers[2] = 4 。
第 3 个查询的答案:powers[0] * powers[1] * powers[2] * powers[3] = 1 * 2 * 4 * 8 = 64 。
每个答案对 10^9 + 7 得到的结果都相同,所以返回 [2,4,64] 。

思路

题目的描述挺抽象的:对于一个正数n,要找到包含最少数目的2的幂,且它们和为n。用人话来说,就是找到n的二进制表示。比如n = 15,其二进制表示为1111,对应的powers数组,对应的就是[2^0, 2^1, 2^2, 2^3]。比如n = 9,其二进制表示为1001,对应的powers数组,就是[2^0, 2^3]

然后要求解的是对于每个区间[l, r],对区间内的每个坐标j,求解所有powers[j] 的乘积。而我们观察到,powers数组中全是2的幂,多个2的幂的乘积,其实可以转变为幂次的和。

比如 2 0 × 2 2 × 2 3 × 2 5 = 2 0 + 2 + 3 + 5 2^0 × 2^2 × 2^3 × 2^5 = 2^{0 + 2 + 3 + 5} 20×22×23×25=20+2+3+5

那么我们可以将powers数组转变为存储2的幂,比如对n = 9powers数组我们不存[2^0, 2^3],而存[0, 3],只把幂次数存下来。那么对于每个区间[l, r],就可以用前缀和来将时间复杂度优化到 O ( 1 ) O(1) O(1),求出幂次数后,再使用快速幂求出结果。(其实这道题因为数据范围比较小,也可以不用快速幂)

// C++  568ms
class Solution {
public:
    vector<int> productQueries(int n, vector<vector<int>>& queries) {
        int MOD = 1e9 + 7;
        vector<int> p;
        for (int i = 0; i < 30; i++) {
            if (n >> i & 1) p.push_back(i);
        }
        n = p.size();
        for (int i = 1; i < n; i++) p[i] += p[i - 1]; // 前缀和
        
        vector<int> ans;
        for (auto &q : queries) {
            int l = q[0], r = q[1];
            int mi = l == 0 ? p[r] : p[r] - p[l - 1]; // 2的多少次方
            // 计算2的mi次方
            int t = 1;
            for (int i = 0; i < mi; i++) {
                t = (t * 2) % MOD;
            }
            ans.push_back(t);
        }
        return ans;
    }
};
// C++ 
// 快速幂 296ms
typedef long long LL;
class Solution {
public:

    int qmi(int a, int b, int c) {
        LL res = 1;
        while (b) {
            if (b & 1) res = res * a % c;
            a = (LL)a * a % c;
            b >>= 1;
        }
        return (int) res;
    }

    vector<int> productQueries(int n, vector<vector<int>>& queries) {
        int MOD = 1e9 + 7;
        int p[30], k = 0;
        for (int i = 0; i < 30; i++) {
            if (n >> i & 1) p[k++] = i;
        }

        for (int i = 1; i < k; i++) p[i] += p[i - 1]; // 前缀和
        
        vector<int> ans;
        for (auto &q : queries) {
            int l = q[0], r = q[1];
            int mi = l == 0 ? p[r] : p[r] - p[l - 1]; // 2的多少次方
            // 计算2的mi次方
            int t = qmi(2, mi, MOD);
            ans.push_back(t);
        }
        return ans;
    }
};

2439. 最小化数组中的最大值

给你一个下标从 0 开始的数组 nums ,它含有 n 个非负整数。

每一步操作中,你需要:

  • 选择一个满足 1 <= i < n 的整数 i ,且 nums[i] > 0
  • nums[i] 减 1 。
  • nums[i - 1] 加 1 。

你可以对数组执行 任意 次上述操作,请你返回可以得到的 nums 数组中 最大值 最小 为多少。

示例

输入:nums = [3,7,1,6]
输出:5
解释:
一串最优操作是:
1. 选择 i = 1 ,nums 变为 [4,6,1,6] 。
2. 选择 i = 3 ,nums 变为 [4,6,2,5] 。
3. 选择 i = 1 ,nums 变为 [5,5,2,5] 。
nums 中最大值为 5 。无法得到比 5 更小的最大值。
所以我们返回 5 。

思路

我的思路

题目中的操作,实际上是可以把数从右往左匀一下。比如[5, 8],则可以把右侧的8减1,将左侧的5加1。即可以从右侧更大的数,匀给左侧更小的数,这样就能使得最大值变小。

通过举例子观察,可以发现规律,只要存在一个单调递增的区间,我们就可以把这个区间中右侧的数,匀给左侧更小的数,最终使得整个区间非常平均。比如有个区间[1 2 5 9 20],我们只需要计算一个平均数,这里是37/5=7...2,则对于这个区间,我们可以最终将它变成[8 8 7 7 7]

于是,周赛当晚,我初步的想法是,遍历一次nums数组,对于每个单调递增的区间,计算出这个区间的平均值,再对所有单调递增区间的平均值,取一个max

第一版代码:

// C++
typedef long long LL;
class Solution {
public:
    int minimizeArrayValue(vector<int>& nums) {
        int ans = 0, cnt = 1;
        LL sum = nums[0];
        // 统计单调递增区间, 并求出平均值
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i - 1] < nums[i]) {
                sum += nums[i];
                cnt++;
            } else {
                LL t = sum / cnt;
                if (sum % cnt != 0) t++;
                ans = max(ans, (int)t);
                cnt = 1;
                sum = nums[i];
            }
        }
        return ans;
    }
};

提交发现WA了,根据错误样例[13,13,20,0,8,9,9]发现,不需要严格单调递增,只需要单调非递减即可。于是修改代码

typedef long long LL;
class Solution {
public:
    int minimizeArrayValue(vector<int>& nums) {
        int ans = 0, cnt = 1;
        LL sum = nums[0];
        // 统计单调递增区间, 并求出平均值
        for (int i = 1; i < nums.size(); i++) {
            if (nums[i - 1] <= nums[i]) { // 单调非递减即可
                sum += nums[i];
                cnt++;
            } else {
                LL t = sum / cnt;
                if (sum % cnt != 0) t++;
                ans = max(ans, (int)t);
                cnt = 1;
                sum = nums[i];
            }
        }
        return ans;
    }
};

提交又WA了,错误样例是[4,7,2,2,9,19,16,0,3,15]

观察这组数据,发现只做一次遍历是不行的,模拟一下我的处理过程,从左往右遍历,第一个非递减区间是[4,7],将其变成[5,6];第二非递减区间是[2,2,9,19],将其变成[8,8,8,8];第三个非递减区间[16],不处理;第四个非递减区间[0,3,15],变成[6,6,6]

那么最终整个数组是:[5,6,8,8,8,8,16,6,6,6],我们发现这个数组还可以继续操作,区间[5,6,8,8,8,8,16]是非递减区间,可以匀成[8,8,8,8,9,9,9]。最终数组是[8,8,8,8,9,9,9,6,6,6]。我们发现,需要对数组做多次遍历。直到什么时候停止呢?

假设某次遍历时,我们分别遇到了3个单调非递减区间,分别是s1s2s3,在每个区间内,我们可以直接将数字非常平均的分到每个位置,假设称这个操作为匀了一下。那么对s1 匀了一下后,能得到s1区间内的一个最大值,设其为m1,那么同理,对于s2,在匀了一下后,我们能得到一个最大值m2,对s3能得到一个m3,则只要后面的区间的最大值,大于前面区间的最大值,说明这个区间还可以往前。比如我们在匀了一下后,s1 = [5,5,5,6,6]s2 = [7,7,7,7,8],那么还需要进行下一次遍历。直到某一次遍历时,从左往右的每个非递减区间的m,满足m1 >= m2 >= m3 >= m4 >= ....,即,直到满足后出现的区间的最大值,不会超过前面区间的最大值。则可以停止操作。可以比较粗糙的看出,遍历整个数组的次数,不会超过 l o g n logn logn,所以总的时间复杂度不会超过 O ( n l o g n ) O(nlogn) O(nlogn)

// C++ 136ms
typedef long long LL;
class Solution {
public:
    int minimizeArrayValue(vector<int>& nums) {
        int ret = 1e9;
        bool stop = false;
        // 一直统计到, 整个区间是非递增, 结束
        while (!stop) {
            stop = true;
            int ans = -1;
            LL sum = nums[0];
            int left = 0;
            // 统计单调递增区间, 并求出平均值
            for (int i = 1; i < nums.size(); i++) {
                if (nums[i - 1] <= nums[i]) {
                    sum += nums[i];
                }
                if (nums[i - 1] > nums[i] || (nums[i - 1] <= nums[i] && i == nums.size() - 1)) {
                    int cnt = i - left;
                    if (nums[i - 1] <= nums[i] && i == nums.size() - 1) cnt++;
                    LL t = sum / cnt;
                    int needPlus = sum % cnt;
                     // printf("i = %d, cnt = %d, t = %lld, needPlus = %d\n", i, cnt, t, needPlus);
                    for (int k = 0; k < cnt; k++) {
                        if (k < needPlus) nums[k + left] = (int)t + 1;
                        else nums[k + left] = (int) t;
                    }
                    if (needPlus > 0) t++;
                    if (ans != -1 && t > ans) stop = false; // 后面的连续递增序列平均值大于前面的, 则下次还要继续迭代
                    ans = max(ans, (int)t);
                    left = i;
                    sum = nums[i];
                }
            }
             // for (int i = 0; i < nums.size(); i++) printf("%d ", nums[i]);
             // cout << endl;
            ret = min(ret, ans);
        }
        return ret;
    }
};

这道题在周赛当晚WA了3次,在最后10分钟时才提交成功,真是非常艰辛。

二分

其实本题还可以用二分来做。我们每次检查某个值,是否能够被整个数组所承受。

所谓一个数能被整个数组所承受的意思是:给定一个数x,我们判断一下数组中的每个元素,能否通过操作,变成<= x

若对于数组中的每个数,都能通过操作,最终变成<= x,那么整个数组的最大值就是<= x的。

我们定义一个check函数来检测某个数x都否被数组承受。若能,则说明整个数组,在操作后的最大值,至多是x;若不能承受,则说明整个数组,在操作后的最大值,至少是x + 1。如此,满足二分的性质,则我们可以用二分来求解答案。

对于check函数的编写,有一种很巧妙的思路。我们将数组中的每个数看成是水的多少。则水只能从右侧,流向左侧(右侧数字减1,左侧数字加1,且是可以传递的)。那么对于> x的那些数,我们要想办法将其多余的水,往左流。而左侧那些原本< x的位置,便可以承接右侧流过来的水。则我们从左往右进行遍历,当遇到nums[i] < x时,表示该位置可以承接右侧流过来的多余的水,能承接的最多的量是x - nums[i];而当遇到nums[i] > x时,我们尝试将多余的水nums[i] - x流到左边去,此时看一下左侧所有为止能承接的量的总和,若能承接下这多余的水,则该位置的数,能变成x。如此遍历到末尾,如果整个过程中,每个位置都能保证至多变成<= x,那么称x能被整个数组所承受。

时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

// C++ 124ms
class Solution {
public:

    bool check(vector<int>& v, int k) {
        long sum = 0;
        for (auto &i : v) {
            if (i <= k) sum += k - i;
            else if (sum < i - k) return false; // 累积的量不能承受该位置多余的量, 直接返回false
            else sum -= i - k; // 能承受, 则从能承受的量中减去
        }
        return true;
    }

    int minimizeArrayValue(vector<int>& nums) {
        int m = 0;
        for (auto &i : nums) m = max(m, i); 
        // 开始二分
        int l = 0, r = m;
        while (l < r) {
            int mid = l + r >> 1;
            if (check(nums, mid)) r = mid;
            else l = mid + 1;
        }
        return l;
    }
};

找规律

累加并计算平均值即可。

从左往右遍历,每遍历到一个位置,将该位置的数纳入考虑。若该位置的数,大于前面的最大值,则该位置的数,可以匀给前面的所有数,计算一个平均值(向上取整),作为新的最大值即可。若遇到该位置的数,小于等于前面的最大值,则该位置的数对答案没有影响。

时间复杂度 O ( n ) O(n) O(n)

// C++  88ms
typedef long long LL;
class Solution {
public:
    int minimizeArrayValue(vector<int>& nums) {
        // 平均数向上取整
        // n/a向上取整的计算方式为 (n + a - 1) / a
        LL sum = 0, ans = 0;
        for (int i = 0; i < nums.size(); i++) {
            sum += nums[i];
            ans = max(ans, (sum + i) / (i + 1));
        }
        return (int) ans;
    }
};

2440. 创建价值相同的连通块

有一棵 n 个节点的无向树,节点编号为 0n - 1

给你一个长度为 n 下标从 0 开始的整数数组 nums ,其中 nums[i] 表示第 i 个节点的值。同时给你一个长度为 n - 1 的二维整数数组 edges ,其中 edges[i] = [ai, bi] 表示节点 aibi 之间有一条边。

你可以 删除 一些边,将这棵树分成几个连通块。一个连通块的 价值 定义为这个连通块中 所有 节点 i 对应的 nums[i] 之和。

你需要删除一些边,删除后得到的各个连通块的价值都相等。请返回你可以删除的边数 最多 为多少。

示例

img

输入:nums = [6,2,2,2,6], edges = [[0,1],[1,2],[1,3],[3,4]] 
输出:2 
解释:上图展示了我们可以删除边 [0,1] 和 [3,4] 。得到的连通块为 [0] ,[1,2,3] 和 [4] 。每个连通块的价值都为 6 。可以证明没有别的更好的删除方案存在了,所以答案为 2 。

提示:

  • 1 <= n <= 2 * 10^4
  • nums.length == n
  • 1 <= nums[i] <= 50
  • edges.length == n - 1
  • edges[i].length == 2
  • 0 <= edges[i][0], edges[i][1] <= n - 1
  • edges 表示一棵合法的树。

思路

注意是n个节点的无向树,共有n - 1条边,并且价值nums[i] > 0。问题是删除一些边,将这个树切开,形成多个连通块,需要保证每个连通块的价值都相等。由于每个连通块价值相等,不妨设其为x,设连通块个数为k,若所有节点的价值的总和为sum,容易得到这样的关系式:kx = sum。由于nums[i] > 0,则x越小,连通块的个数越多,删除的边也就越多。并且x必须是sum的一个约数。

则我们从小到大枚举sum的全部约数x,依次判断能够以x将树切割成若干个连通块,使得每个连通块中的点的价值和都等于x

因为是从小到大枚举的x,则遇到的第一个满足条件的x,就找到了答案。

问题的关键是,如何判断能否以x将树切割成若干连通块。

考虑用DFS来做。若我们当前要凑出的连通块的价值和为x,我们从叶子节点往上来考虑。

  • 叶子节点的价值若< x,则叶子节点必须与其父节点连通,以增大连通块的价值;

    叶子节点与父节点连通后,形成一颗新的子树,继续往上加节点,直到遇到形成的子树的价值和= x,进行切断

  • 若叶子节点的价值= x,则一定要进行切断,因为每个点的价值都是> 0的,新加进来的点一定会使得连通块的价值和增大;

  • 若叶子节点的价值> x,则说明无法进行划分。

归纳一下,脑子里形成这样一个画面:从每个叶子节点开始,往上走,尝试进行切断或者合并。若子树价值和< x则一定需要继续往上合并;若= x则直接切断;若> x则无法划分。

我们用一个DFS来做,计算子树对父节点的价值贡献。若子树自身价值和= x,则直接切断,相当于其对父节点的价值贡献为0;若子树自身价值和< x,则其必须要往上走,与父节点合并;若子树自身价值和> x,则无法进行划分,返回-1

我们以任意一个节点作为根节点,进行DFS,若最终返回0,说明整棵树可以被划分。

C++:

// C++
class Solution {
public:
    vector<vector<int>> ad; // 树的邻接表表示
    vector<int> nums;
    /**
    * fx是x的父节点, 加入这个参数是为了防止走回头路
    * 当然也可以用 visited 数组来实现
    * 0 以x为根节点的子树, 能够被划分
    * -1 以x为根节点的子树, 不够被划分
    * 否则返回以x为根节点的子树的价值和
    **/
    int dfs(int x, int fx, int target) {
        int res = nums[x]; // 价值和
        for (auto& i: ad[x]) {
            // 无向图, 不往回走, 遇到父节点则跳过
            if (i == fx) continue;
            // 递归对子节点进行划分
            int tmp = dfs(i, x, target);
            if (tmp == -1) return -1; // 无法划分
            res += tmp; //加上贡献, tmp只可能 < target  或者 = 0
        }
        if (res > target) return -1;
        if (res == target) return 0;
        return res;
    }

    int componentValue(vector<int>& nums, vector<vector<int>>& edges) {
        int n = nums.size();
        ad.resize(n);
        this->nums = nums;
        for (auto& e: edges) {
            int a = e[0], b = e[1];
            ad[a].push_back(b);
            ad[b].push_back(a);
        }

        int sum = 0;
        for (auto& v : nums) sum += v;

        // 枚举sum的所有约数
        for (int i = 1; i <= sum; i++) {
            if (sum % i == 0) {
                // 若能够被这个约数划分, 连通块的个数为 sum / i, 删去的边数 = 连通块个数 - 1
                if (dfs(0, -1, i) == 0) return sum / i - 1;
            }
        }
        return 0;
    }
};

Java:

// Java
class Solution {
    int[] h;
    int[] e;
    int[] ne;
    int idx = 0;

    int[] nums;

    private void add(int a, int b) {
        e[idx] = b;
        ne[idx] = h[a];
        h[a] = idx++;
    }

    private int dfs(int x, int fx, int target) {
        int res = nums[x];
        for (int i = h[x]; i != -1; i = ne[i]) {
            int u = e[i];
            if (u == fx) continue;
            int t = dfs(u, x, target);
            if (t == -1) return -1;
            res += t;
        }
        if (res > target) return -1;
        return res == target ? 0 : res;
    }

    public int componentValue(int[] nums, int[][] edges) {
        // 建图
        int n = nums.length;
        h = new int[n];
        e = new int[2 * n];
        ne = new int[2 * n];
        Arrays.fill(h, -1);
        for (int[] e : edges) {
            int a = e[0], b = e[1];
            add(a, b);
            add(b, a);
        }
        this.nums = nums;
        // 整棵树的价值和
        int sum = 0;
        for (int i : nums) sum += i;
        // 枚举约数
        for (int i = 1; i <= sum; i++) {
            if (sum % i == 0) {
                // i是一个约数, 判断是否能以i来进行划分
                if (dfs(0, -1, i) == 0) return sum / i - 1;
            }
        }
        return 0;
    }
}

总结

T1模拟;T2位运算+前缀和;T3找规律/二分;T4 图论DFS。

这次周赛,对我来说感觉难度不小。非常侥幸并吃力地做出了3题。还要继续努力啊。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值