当前位置: 首页 > article >正文

Leetcode 第425场周赛分析总结

Leetcode 第425场周赛

好久没有参加周赛了,手生了许多
在这里插入图片描述

Q1. 最小正和子数组

AC代码

class Solution {
public:
    int minimumSumSubarray(vector<int>& nums, int l, int r) {
        int n = nums.size();
        vector<int> sum(n + 1);
        for (int i = 0; i < n; ++i) {
            sum[i + 1] = sum[i] + nums[i];
        }
        int ans = -1;
        bool flag = false;
        for (int len = l; len <= r; ++len) {
            for (int i = 0, j = len - 1; j < n; ++i, ++j) {
                int tmp_sum = sum[j + 1] - sum[i];
                if (tmp_sum <= 0) continue;
                if (flag == false) {
                    ans = tmp_sum;
                    flag = true;
                } else {
                    ans = min(ans, tmp_sum);
                }
            }
        }
        return ans;
    }
};

分析总结

题目描述看完发现不是非常trivial,看了一眼数据范围,觉得不要多想直接暴力好了。闪过念头不会把数据范围变成1e5就是第三题吧,AC之后去看了一眼发现不是松了一口气。简单想了一下感觉可能和滑动窗口有关,但是却没有发现单调性,没有什么思路。

下来认真看了灵神的视频,发现果真复杂。发现自己目前存在一个问题,对于不能一眼看出思路的算法题会产生抗拒,不愿意静下心认真思考解决的方案,似乎遇到困难就会缩起来。这可能是之前刷题的时候总是苛求自己,导致闪回,遇到自己能力的边界不是一件可耻的事情。

认真分析后发现,我们要求的区间 [ i , j ) [i, j) [i,j)和要满足下面的条件:
s u m [ j ] − s u m [ i ] > 0 l ≤ j − i ≤ r sum[j]-sum[i]>0\\l\leq j - i \leq r sum[j]sum[i]>0ljir
对于这种有两个变量的问题,我们可以先固定一个变量,考虑另一个变量进行简化,例如我们这里固定j(枚举右、维护左)
s u m [ i ] < s u m [ j ] j − r ≤ i ≤ j − l sum[i] < sum[j]\\j-r\leq i \leq j-l sum[i]<sum[j]jrijl
即我们要在区间 [ j − r , j − l ] [j-r, j-l] [jr,jl]中找到比 s u m [ j ] sum[j] sum[j]小的最大的 s u m [ i ] sum[i] sum[i],为了得到这个信息,我们可以维护一个有序区间,在里面进行二分查找,而随着 j j j的遍历,我们也要动态更新这个有序区间的值,我们可以用红黑树数据结构来维护。

class Solution {
public:
    int minimumSumSubarray(vector<int>& nums, int l, int r) {
        int n = nums.size();
        vector<int> sum(n + 1);
        for (int i = 0; i < n; ++i) {
            sum[i + 1] = sum[i] + nums[i];
        }
        int ans = -1;
        bool flag = false;
        multiset<int> segment;
        for (int j = l; j <= n; ++j) {
            segment.insert(sum[j - l]);
            auto iter = segment.lower_bound(sum[j]);
            if (iter != segment.begin()) {
                iter--; // < sum[j]
                if (!flag) {
                    flag = true;
                    ans = sum[j] - *iter;
                } else {
                    ans = min(ans, sum[j] - *iter);
                }
            }
            if (j >= r) {
                segment.erase(segment.find(sum[j - r]));
            }
        }
        return ans;
    }
};

思路清晰以后我尝试实现了一下,发现存在错误,经过仔细的分析都没有找出来问题在哪里。去研究了一下灵神的代码,发现我基本上写的和他一样,就是有一个小的地方,在erase的时候他是先find一下,再erase,而我是直接erase对应的值。刚开始我还没有注意这个问题,过了一会才想到可能是multiset在erase对应key的时候会删除所有的key,而我们正确的是只删除一个。由于之前没有怎么使用过multiset,所以忽略了这个问题。

“纸上得来终觉浅,绝知此事要躬行”。

Q2. 重排子字符串以形成目标字符串

AC代码

class Solution {
public:
    bool isPossibleToRearrange(string s, string t, int k) {
        map<string, int> cnt_s, cnt_t;
        int step = s.size() / k;
        for (int i = 0, j = 0; j < k; i += step, j++) {
            cnt_s[s.substr(i, step)]++;
            cnt_t[t.substr(i, step)]++;
        }
        return cnt_s == cnt_t;
    }
};

分析总结

觉得这个题更适合放到第一题,很快注意到分块的方式是固定的,那么我们只需要统计两个字符串中每个块出现的次数即可。注意C++中unordered_map不支持==运算符(很合理)。

看了一下灵神的代码,他是先放到vector里面再进行排序,时间复杂度和map是一样的,可能常数更小一些。

Q3. 最小数组和

这道题刚开始看到,注意到数据范围比较小,然后考虑到对于每个数字我们有做或者不做op的四种情况,其实这个时候就应该想到DP的。但是粗略整体一想,觉得每种操作会导致数组的状态发生变化,没有无后效性,就去哼哧哼哧构造贪心,最终花了一个多小时,对着算例不断在屎上雕花,也没有解决掉。

最主要的,我没有尝试去思考子问题,对于数组,如果我们按照一定的顺序思考(从左到右),那么当前进行了op后,右边剩下的区域和剩下的op就构成了子问题,由于是最小值,所以满足最优性,同时前面的决策也不会对后面的决策产生影响,同样满足无后效性,因此可以用记忆化搜索解决。

这道题觉得在自己的能力范围内,但是我对着贪心钻牛角尖,没有尝试认真的思考,最终放弃。应该对自己更有信心一些,更耐心一些。

class Solution {
public:
    int minArraySum(vector<int>& nums, int k, int op1, int op2) {
        int n = nums.size();

        vector memo(n, vector(op1 + 1, vector<int>(op2 + 1, -1)));
        function<int(int, int, int)> dfs;
        dfs = [&](int idx, int op1, int op2) -> int {
            if (idx == n) {
                return 0;
            }
            int& ret = memo[idx][op1][op2];
            if (ret != -1) return ret;
            ret = nums[idx] + dfs(idx + 1, op1, op2);
            if (op1 > 0) {
                int x = nums[idx];
                x = (x + 1) / 2;
                ret = min(ret, x + dfs(idx + 1, op1 - 1, op2));
                if (op2 > 0 && x >= k) {
                    ret = min(ret, x - k + dfs(idx + 1, op1 - 1, op2 - 1));
                }
            }
            if (op2 > 0 && nums[idx] >= k) {
                int x = nums[idx] - k;
                ret = min(ret, x + dfs(idx + 1, op1, op2 - 1));
                if (op1 > 0) {
                    ret = min(ret, (x + 1) / 2 + dfs(idx + 1, op1 -1, op2 - 1));
                }
            }
            return ret;
        };
        return dfs(0, op1, op2);
    }
};

认真拜读了一下灵神的代码,发现他对于memo的类型使用了C++17的模板参数类型推导,简化了代码。对于递归函数对象的调用,我认为的写法更优雅一些,他是直接将lambda表达式作为第一个参数传入,这样虽然增加了参数传递的开销,但是lambda表达式的调用开销比function对象小,性能差异可能要在特定场景下进行测试。

Q4. 移除边之后的权重最大和

2611. 老鼠和奶酪

直观上有贪心的性质,先得到两种老鼠吃奶酪的差值,然后选择差值最大的k个1号吃,剩下的2号吃。

学习了一下使用ranges::nth_elementreduce函数,前者可以让我们以O(n)的时间复杂度获取到数组中前k大元素,后者会进行归约,默认会进行求和,并且支持并行加速(需要手动传递参数)。

class Solution {
public:
    int miceAndCheese(vector<int>& reward1, vector<int>& reward2, int k) {
        for (int i = 0; i < reward1.size(); ++i) reward1[i] -= reward2[i];
        ranges::nth_element(reward1, reward1.end() - k);
        return reduce(reward2.begin(), reward2.end()) + reduce(reward1.end() - k, reward1.end());
    }
};

随着C++的快速发展,C++代码越来越优雅了。

我观察到,其实我们对于每一个奶酪,是在决策要不要选择让1号吃掉,那么同样可以动态规划,但是时间复杂度是O(n * k),这里n和k都是1e5的,会超时。

这也对应了灵神提到的,面对一个题目首先思考暴力搜索,如果不行考虑记忆化搜索,如果还是不行就必须考虑其他方面了,是用某种数据结构加速操作还是考虑贪心策略。

class Solution {
public:
    int miceAndCheese(vector<int>& reward1, vector<int>& reward2, int k) {
        int n = reward1.size();
        vector memo(n, vector(k + 1, -1));
        function<int(int, int)> dfs;
        dfs = [&](int idx, int k) -> int {
            if (idx == n) {
                return k == 0 ? 0 : numeric_limits<int>::min();
            }
            int &ret = memo[idx][k];
            if (~ret) {
                return ret;
            }
            ret = reward2[idx] + dfs(idx + 1, k);
            if (k) {
                ret = max(ret, reward1[idx] + dfs(idx + 1, k - 1));
            }
            return ret;
        };
        return dfs(0, k);
    }
};

分析总结

第四题当时没有来得及看,不过我觉得即使当时有时间应该也很难做出来,这道题在我的能力的边界。

首先最重要的应该是熟悉树形搜索的框架,对于树形搜索,我们从任意一个节点出发,在不访问父节点的情况下,不用担心节点的重复访问,因为没有环。

接下来就是思考我们希望从子树中获取什么信息,这应该是树形搜索的关键。对于这道题,我们可以对每一条边去判断删除和不删除两种情况,那么我们可以获取子树根节点:

  • 最大连接边是k时子树最大权重(对应删除从当前节点到子树根节点连边)
  • 最大连接数是k-1时子树最大权重(对应不删除)

两种信息。

另一个关键的点是,我们获取了所有子树的信息后,如何向上返回这两种信息。这就要用到类似上面老鼠和奶酪的思想。我们可以先选择删除所有和子树的连边,然后再贪心的选择不删除情况下增量最大的最多k(-1)条边

class Solution {
public:
    long long maximizeSumOfWeights(vector<vector<int>>& edges, int k) {
        using ll = long long;
        int n = edges.size() + 1;

        //build graph
        vector graph(n, vector<pair<int, ll>>());
        for (auto &&edge : edges) {
            int u = edge[0], v = edge[1];
            ll w = edge[2];
            graph[u].emplace_back(v, w);
            graph[v].emplace_back(u, w);
        }

        using RetType = pair<ll, ll>;   //<connect, disconnect>
        //vector memo(n, RetType{-1, -1});
        function<RetType(int, int)> dfs;
        
        dfs = [&](int u, int father) -> RetType {
            vector<ll> diff;
            ll sum = 0;
            for (auto [v, w] : graph[u]) {
                if (v == father) continue;
                auto [con, discon] = dfs(v, u);
                sum += discon;
                auto d = con - discon + w;
                if (d > 0) {
                    diff.push_back(d);
                }
            }
            //most choose k edge connect
            ranges::sort(diff, greater());
            int n = min((int)diff.size(), k - 1);
            for (int i = 0; i < n; ++i) {
                sum += diff[i];
            }
            return RetType{sum, diff.size() >= k ? sum + diff[k - 1] : sum};
        };
        auto [r0, r1] = dfs(0, -1);
        return r1;
    }
};

整个代码还是相对比较直观的,在听了灵神的思路后我自己实现了一下。结果在返回的时候遇到了bug,我想着先获取n=min(n, k),然后再遍历累加前n-1个,最后再判断加上最后一个元素。可是实际的逻辑应该是先选前k-1个,再判断能不能选第k个。这其中细微的差异在于当n < k的时候,第n个元素仍然属于前k-1个,应该属于连接数不超过k-1的情况。

这告诫我在实现代码的时候应该遵守KISS的原则,按照逻辑用代码去描述,然后再考虑是否能够优化。一上来就使用比较trick的写法可能在逻辑上隐藏不容易发现的bug。

虽然他们把这种题目叫做树形DP,可是我并没有看到DP的影子。DP的最核心的点应该是有重复的子问题,我们通过记忆化去加速搜索,而对树进行搜索的时候并不会有重复访问同一个节点的情况,我认为叫树形搜索更加合适。

周赛总结

我认为我目前的能力应该足够应付绝大多数的周赛题目,因此应该更加自信大胆的去思考,而不是稍微卡壳就想要放弃。下次周赛加油~


http://www.kler.cn/a/419955.html

相关文章:

  • arkTS:持久化储存UI状态的基本用法(PersistentStorage)
  • Linux 各个目录作用
  • Ollama是什么
  • idea打jar包或引入包
  • Vulnhub靶场 Matrix-Breakout: 2 Morpheus 练习
  • C++分治思想
  • 力扣1382:将二叉搜索树便平衡
  • Scala的模式匹配变量类型
  • 方寸 i560 安全存储加密芯片 SoC 存储安全芯片技术手册
  • Ubuntu24.04配置DINO-Tracker
  • php+Mysql单页支持不同数据结构不同查询条件查搜多表实例
  • 006 MATLAB编程基础
  • ansible自动化运维(一)配置主机清单
  • 在html页面显示一个变量,而这个变量中有xss脚本,如何安全的把这个变量原样展示出来
  • 360笔试题之LINUX和UNIX篇
  • 数据结构——排序第三幕(深究快排(非递归实现)、快排的优化、内省排序,排序总结)超详细!!!!
  • 【Debug】hexo-github令牌认证 Support for password authentication was removed
  • Node.js-Mongodb数据库
  • 电脑显示器拔插DVI线后副屏不显示
  • 【机器学习】机器学习学习笔记 - 监督学习 - 逻辑回归分类朴素贝叶斯分类支持向量机 SVM (可分类、可回归) - 04
  • K8S版本和istio版本的对照关系
  • 数学建模——Topsis法
  • scrapy豆瓣爬虫增强-批量随机请求头
  • 使用pyQT完成简单登录界面
  • 【k8s】监控K8S集群
  • 现代应用程序中基于 Cell 架构的安全防护之道