Leetcode 第426场周赛分析总结

在这里插入图片描述

3370. 仅含置位位的最小整数

AC代码

class Solution {
public:
    int smallestNumber(int n) {
        int x = 1;
        while (x - 1 < n) {
            x <<= 1;
        }
        return x - 1;
    }
};

分析总结

也可以先直接获取n的长度,然后计算得到,这样时间复杂度由O(logn)优化为O(1)

在C++20中可以使用头文件中的std::bit_width函数,传入一个无符号整数,会返回其所需的最小位数(也就是最高位1的位数)

如果题目要求大于n的数,我的第一思路是判断n是不是2的幂次减1,分类讨论。灵神说不必如此,相当于我们已经拥有了求大于等于n的能力,那么对于大于n的情况,我们只需要求大于等于n+1,即在整数域上:> x相当于 >= x + 1,这是一种思维的转换能力。

class Solution {
public:
    int smallestNumber(int n) {
        return (1 << std::bit_width(static_cast<unsigned>(n))) - 1;
    }
};

3371. 识别数组中的最大异常值

AC代码

class Solution {
public:
    int getLargestOutlier(vector<int>& nums) {
        int n = nums.size();
        ranges::sort(nums);
        int sum = reduce(nums.begin(), nums.end());
        auto check = [&](int idx) -> bool {
            //[0, idx) [idx + 1, n)
            int seg_sum = sum - nums[idx];
            if (seg_sum % 2 != 0) return false;
            int target_sum = seg_sum / 2;
            int l = lower_bound(nums.begin(), nums.end(), target_sum) - nums.begin();
            int r = upper_bound(nums.begin(), nums.end(), target_sum) - nums.begin();
            //[l, r)
            if (l < r) {
                if (r <= idx || l > idx || r - l > 1 || l != idx) return true;
                else return false;
            } else {
                return false;
            }
        };
        for (int i = n - 1; i >= 0; --i) {
            if (check(i)) {
                return nums[i];
                break;
            }
        }
        return nums[0];
    }
};

分析总结

比赛过程中,我的第一想法是,整个数组的和等于异常值和剩下数字的和以及剩下的数字组成,那么我可以遍历异常值,判断剩下的数字是否满足情况,由于要快速判断一个数字是否存在,又忘记了使用哈希表,所以就先进行排序,从大往小遍历异常值,时间复杂度是O(nlogn),空间复杂度O(1)。当时能够很快AC我还有点开心,觉得还是有点复杂的。

听了灵神的解析才发现自己马达马达达内。最重要的一点是判断一个数字是否出现应该考虑使用哈希表加速,其次我也没有进行数学抽象分析。要是考虑到哈希表就不会写出这么复杂的代码,通过复杂的二分判断数字是否存在。

class Solution {
public:
    int getLargestOutlier(vector<int>& nums) {
        int n = nums.size();
        int sum = 0;
        unordered_map<int, int> cnt;
        for (auto x : nums) {
            sum += x;
            ++cnt[x];
        }
        int ans = numeric_limits<int>::min();
        for (auto x : nums) {
            --cnt[x];
            int t = sum - 2 * x;
            if (cnt[t]) {
                ans = max(ans, t);
            }
            ++cnt[x];
        }
        return ans;
    }
};

灵神留了一个思考题,考虑当数组已经有序的情况下如何使用空间复杂度为O(1)的算法解决问题。假设特殊数字的和是 s s s,异常值是 x x x,数组的和是 s u m sum sum,那么问题就是要找到 2 s + x = s u m 2s + x =sum 2s+x=sum x x x,要使得 x x x尽可能大,对于升序数组,我们从右到左遍历 x x x,从左往右遍历 s s s

  • 如果此时 2 s + x > s u m 2s+x>sum 2s+x>sum,则剩下的 s s s(只会更大)都不满足情况,可以将 x x x向左移动;
  • 如果此时 2 s + x < s u m 2s+x<sum 2s+x<sum,则剩下的 x x x(只会更小)都不满足情况,可以将 s s s向右移动;

我本以为这就是一个简单的双指针,于是写了一下结果WA了。苦思冥想之际我发现,这个问题和普通的有序数组两数之和不同的地方在于,这里没有对称性。在两数之和中, x + y > t a r g e t x+y>target x+y>target y + x > t a r g e t y+x>target y+x>target是对称的,因此当两个指针相遇的时候我们可以判断数组中没有满足条件的数。但是对于这里而言, 2 s + x > s u m 2s+x>sum 2s+x>sum不代表 s + 2 x > s u m s+2x>sum s+2x>sum,因此即使两个指针相遇仍然需要继续移动。难点就在于当指针相遇的时候应该移动哪个指针呢?

要是两个指针相遇的时候不等于 s u m sum sum,我们仍然可以按照上面的规则移动。可是如果当相遇的时候等于 s u m sum sum,应该移动哪一个指针呢?实践发现,移动右指针可以AC,移动左指针就会WA,我觉得AC可能是数据的缘故,并不表示正确性。

class Solution {
public:
    int getLargestOutlier(vector<int>& nums) {
        int n = nums.size();
        int sum = std::reduce(nums.begin(), nums.end());
        sort(nums.begin(), nums.end());
        int l = 0, r = n - 1;
        while (l < n && r >= 0) {
            if (l == r) {
                --r;
            }
            int t = 2 * nums[l] + nums[r];
            if (t > sum) {
                --r;
            } else if (t < sum) {
                ++l;
            } else {
                return nums[r];
            }
        }
        return -1;
    }
};

因此,我觉得对于这种没有对称性的情况,即使存在单调性,也并不适合使用双指针。

3372. 连接两棵树后最大目标节点数目 I

题目要求树上距离每个节点小于等于k的节点数目,在计算之前我们可以添加一个连边将树1和树2连接起来。经过分析不难发现这个连边方案是确定的:

  • 树1上的点肯定是当前计算的目标节点,因为从其他节点出发只会导致距离目标节点更远
  • 树2上的点应该是一个距离它小于等于k-1的节点数目最大的节点

那么问题就变成了,我们如何快速的计算树上一个节点到其他节点的距离。如果直接从某一个节点出发进行DFS(也可以BFS,不过使用DFS编码更简单,因为可以直接使用函数栈存储信息),时间复杂度是O(n),由于需要对每个节点计算,总的时间复杂度是 O ( n 2 + m 2 ) O(n^2+m^2) O(n2+m2)

class Solution {
public:
    vector<int> maxTargetNodes(vector<vector<int>>& edges1, vector<vector<int>>& edges2, int k) {
        auto get_dfs = [](decltype(edges1) edges) -> auto {
            int n = edges.size() + 1;
            vector graph(n, vector<int>());
            for (auto &&edge : edges) {
                auto u = edge[0], v = edge[1];
                graph[u].push_back(v);
                graph[v].push_back(u);
            }
            function<int(int, int, int)> dfs;
            dfs = [graph = std::move(graph), &dfs](int u, int fa, int d) -> int {
                if (d < 0) return 0;
                int ans = 1;
                for (auto v : graph[u]) {
                    if (v == fa) continue;
                    ans += dfs(v, u, d - 1);
                }
                return ans;
            };
            return dfs;
        };
        int maxn2 = 0;
        if (k > 0) {
            auto dfs = get_dfs(edges2);
            int n = edges2.size() + 1;
            for (int i = 0; i < n; ++i) {
                maxn2 = max(maxn2, dfs(i, -1, k - 1));
            }
        }
        auto dfs = get_dfs(edges1);
        int n = edges1.size() + 1;
        vector<int> ans(n, 0);
        for (int i = 0; i < n; ++i) {
            ans[i] = dfs(i, -1, k) + maxn2;
        }
        return ans;
    }
};

这里我想模仿灵神在视频中提到的闭包的技巧,这样可以将建图和dfs的逻辑绑定起来,避免在调用的时候传递参数。实际在实现的时候我有些疑惑到底怎样在闭包函数中捕获局部变量和自身。在动态内存分配的语言中,我们不需要考虑捕获,只要直接使用,就会有C++中引用捕获的语义,而语言自身通过引用计数保证了局部变量的内存不会被错误的释放。可是在C++中这一切都是确定性的,没有额外的垃圾回收,需要我们自己保证内存的安全性,就必须要考虑捕获的变量生存周期。

对于普通的局部变量,我们肯定不能进行引用捕获,当get_dfs函数返回后,dfs捕获的引用将会失效,因此这里我们使用移动捕获,通过移动拷贝将局部变量保存在dfs函数对象中。

对于需要返回的函数对象本身,因为我们需要在lambda表达式中访问自身,所以也需要捕获。可是如果进行值捕获的话,我们在初始化dfs的过程中dfs本身是一个不完全类型,是不能拷贝的(实际测试值捕获dfs会报错),因此只能进行引用捕获。但是捕获一个局部变量(dfs本身作为局部变量)不会造成悬挂引用吗?实际上的确不会,原因是返回值作为一个特殊的局部变量其实是保存在上一层函数栈中,不会随着函数的调用结束而释放内存,具体的细节和RVO(返回值优化)等C++底层技术和编译原理有关。

分析总结

比赛中最终这道题还是没有做出来,觉得不应该。其实核心的思路当时已经思考清楚了,但是在考虑实现计算距离每个节点不超过k的节点数目时,我想着应该使用一种时间复杂度更优秀的做法,因为对于每个节点都要计算,直接简单对每个节点遍历好像会超时。但实际上当时没有认真看数据范围,只是凭感觉会超时(要是问我为什么,只能说比赛的时候脑子抽了)。

想来想去好像没有更好的做法,就去问了一下GPT,它告诉我可以先以某个节点为根节点(例如0号节点)遍历树,得到每个节点的深度和父亲,然后再求解任意两个节点的最近公共祖先,然后任意两点之间的距离可以通过 d i s [ i ] [ j ] = d e p t h [ i ] + d e p t h [ j ] − 2 ∗ d e p t h [ l c a [ i ] [ j ] ] dis[i][j]=depth[i]+depth[j]-2*depth[lca[i][j]] dis[i][j]=depth[i]+depth[j]2depth[lca[i][j]]计算,我一听很有道理呀,然后就哼哧哼哧实现,结果一直报错。下来再看才发现求解lca的时候写错了,修改以后就对了。

class Solution {
    vector<vector<int>> buildGraph(const vector<vector<int>>& edges) {
        int n = edges.size() + 1;
        vector graph(n, vector<int>());
        for (auto &&edge : edges) {
            int u = edge[0], v = edge[1];
            graph[u].push_back(v);
            graph[v].push_back(u);
        }
        return graph;
    }
    vector<vector<int>> getDis(const vector<vector<int>>& graph) {
        int n = graph.size();
        vector dis(n, vector<int>(n, 0));
        vector depth(n, 0);
        vector father(n, -1);
        vector lca(n, vector<int>(n, -1));

        function<void(int, int, int)> dfs;
        dfs = [&](int u, int fa, int d) {
            father[u] = fa;
            depth[u] = d;
            for (auto v : graph[u]) {
                if (v == fa) continue;
                dfs(v, u, d + 1);
            }
        };
        dfs(0, -1, 0);
        father[0] = 0;
        /*
        for (int i = 0; i < n; ++i) {
            cout << "depth " << i << ": " << depth[i] << endl;
        }
        */

        function<int(int, int)> get_lca;
        get_lca = [&](int u, int v) -> int {
            int ret = lca[u][v];
            if (ret != -1) return ret;
            if (depth[u] > depth[v]) {
                ret = get_lca(father[u], v);
            } else if (depth[u] < depth[v]) {
                ret = get_lca(father[v], u);
            } else {
                if (u == v) {
                    ret = u;
                } else {
                    ret = get_lca(father[u], father[v]);
                }
            }
            lca[u][v] = ret;
            lca[v][u] = ret;
            return ret;
        };

        for (int i = 0; i < n; ++i) {
            dis[i][0] = 1;//(i, i)
            for (int j = i + 1; j < n; ++j) {
                int d = depth[i] + depth[j] - 2 * depth[get_lca(i, j)];
                //cout << depth[i] << " " << depth[j] << " " <<depth[get_lca(i, j)] << endl;
                //cout << i << "---" << j << " : " << d << endl;
                dis[i][d]++;
                dis[j][d]++;
            }
        }
        
        return dis;
    }
public:
    vector<int> maxTargetNodes(vector<vector<int>>& edges1, vector<vector<int>>& edges2, int k) {
        auto graph1 = buildGraph(edges1);
        auto graph2 = buildGraph(edges2);
        int n = graph1.size();
        int m = graph2.size();
        vector ans(n, 0);
        auto dis1 = getDis(graph1);
        auto dis2 = getDis(graph2);
        int max_target2 = [&]() -> int {
            int t = min(m, k);
            int ret = -1;
            for (int i = 0; i < m; ++i) {
                ret = max(ret, reduce(dis2[i].begin(), dis2[i].begin() + t));
            }
            return ret;
        }();
        //cout << "max_target2 = " << max_target2 << endl;
        int t = min(n, k + 1);
        for (int i = 0; i < n; ++i) {
            ans[i] = reduce(dis1[i].begin(), dis1[i].begin() + t) + max_target2;
        }
        return ans;
    }
};

求解lca的时候用了记忆化。实际上认真思考这种方式的时间复杂度也是 O ( n 2 ) O(n^2) O(n2)的,甚至常数更大。

比赛的时候编码想着把建图和搜索的逻辑封装起来,通过参数传递。与闭包的方式比起来,这种方式耦合度更低,但是对于这个具体的场景会有参数传递的麻烦,可见编码的高级技巧可以简化工作量,闭包函数作为一种比较高级的编码技巧我还是不太熟悉。

另一方面也暴露出我对树上搜索还是不够熟悉,这种简单的操作本来应该直接写出来的。

3373. 连接两棵树后最大目标节点数目 II

这道题比赛的时候完全没有看,觉得主要的难点相比第三题在于,意识到一个树上距离某个节点距离为偶数的所有节点形成的集合互相满足距离为偶数,剩下的节点形成了另一个互相满足距离为偶数的集合。

那么在树1上,答案就固定的分成两种,我们只需要对节点进行染色分类,记录每种颜色节点的数目和每个节点的颜色。剩下的问题是如何和树2进行连边:

  • 我们可以从距离树1目标节点x为偶数(例如从x出发)连边,目标就是找到树2上距离为奇数的节点数目最大的节点
  • 也可以从距离树1目标节点x为奇数(例如从x的某个相连的节点出发)连边,目标就是找到树2上距离为偶数的节点数目最大的节点

无论哪种情况,这个数目都是固定的,和x的选取没有关系,其实就是两种颜色集合大小的较大值。

class Solution {
public:
    vector<int> maxTargetNodes(vector<vector<int>>& edges1, vector<vector<int>>& edges2) {
        auto aux = [](decltype(edges1) edges) -> pair<vector<int>, vector<int>> {
            int n = edges.size() + 1;
            vector graph(n, vector<int>());
            for (auto &&edge : edges) {
                int u = edge[0], v = edge[1];
                graph[u].push_back(v);
                graph[v].push_back(u);
            }

            vector colors(n, -1);
            vector cnt(2, 0);

            function<void(int,int,int)> dfs;
            dfs = [&](int u, int fa, int c) {
                colors[u] = c;
                cnt[c]++;
                for (auto v : graph[u]) {
                    if (v == fa) continue;
                    dfs(v, u, c ^ 1);
                }
            };
            dfs(0, -1, 0);

            return {colors, cnt};
        };
        auto [colors1, cnt1] = aux(edges1);
        auto [colors2, cnt2] = aux(edges2);

        int maxn2 = max(cnt2[0], cnt2[1]);
        int n1 = edges1.size() + 1;
        vector ans(n1, 0);
        for (int i = 0; i < n1; ++i) {
            ans[i] = cnt1[colors1[i]] + maxn2;
        }
        
        return ans;
    }
};

分析总结

看了一下灵神的C++实现,基本上相同,不过我用一个数组保存了每个节点的染色信息,所以不需要第二次BFS。

虽然实际写出来后觉得没有想象中那么难,但是关键的思路转换,以及在时间较为紧张的情况下保持清晰的头脑可能才是更重要的能力,这种能力需要不断在实践中磨练。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/947817.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【从零开始入门unity游戏开发之——unity篇05】unity6基础入门——运行游戏按钮、Game游戏窗口和Project项目窗口介绍

文章目录 运行游戏按钮、Game游戏窗口和Project项目窗口一、运行游戏按钮二、Game游戏窗口1、右上角设置1.1 如果没有相机渲染则发出警告1.2 在”编程模式”下清除每一帧1.3 窗口最大化 2、上方工具&#xff08;1&#xff09;切换手机模拟器&#xff08;2&#xff09;切换不同显…

九、Vue 事件处理器

文章目录 前言一、基础事件绑定:v-on 指令二、方法调用:组织有序的交互逻辑三、事件修饰符阻止冒泡与默认事件捕获与自身触发单次触发与鼠标按键区分四、按键修饰符前言 在 Vue.js 的交互世界里,事件处理器起着举足轻重的作用,它让页面从静态展示迈向动态交互,精准捕捉用户…

【项目】基于趋动云平台的Stable Diffusion开发

【项目】基于趋动云平台的Stable Diffusion开发 &#xff08;一&#xff09;登录趋动云&#xff08;二&#xff09;创建项目&#xff1a;&#xff08;三&#xff09;初始化开发环境&#xff1a;&#xff08;四&#xff09;运行代码&#xff08;五&#xff09;运行模型 &#xf…

VSCode下配置Blazor环境 断点调试Blazor项目

VSCode下使用Blazor的环境配置和插件推荐 Blazor是一种用于构建交互式Web UI的.NET框架&#xff0c;它可以让你使用C#、Razor和HTML进行Web开发&#xff0c;而不需要JavaScript。在这篇文章中&#xff0c;我们将介绍如何在VSCode中配置Blazor环境&#xff0c;并推荐一些有用的…

word文档中的文档网格——解决相同行间距当显示出不同行间距的情况

1 问题 被一个行间距调疯了&#xff0c;就是样式改了没用&#xff0c;格式刷刷了没用。就是肉眼可以看出行间距完全不一样。 2 解决方法 1&#xff09;修改论文正文(即出现问题文本的样式)样式&#xff1a;样式>修改>格式>段落>缩进和间距>取消"如果定义了…

ubuntu如何禁用 Snap 更新

.禁用 Snap 更新&#xff08;通过修改 snapd 配置&#xff09; 打开并编辑 /etc/apt/apt.conf.d/50unattended-upgrades文件。 这个文件控制自动更新的行为。 sudo vim /etc/apt/apt.conf.d/50unattended-upgrades 里面有一行将里面的auto改为false即可禁用更新&#xff1a;…

UniApp 原生插件开发指南

一、UniApp 原生插件开发引言 在当今的移动应用开发领域&#xff0c;跨平台开发已成为主流趋势&#xff0c;而 UniApp 作为一款强大的跨平台开发框架&#xff0c;备受开发者青睐。它凭借 “一套代码&#xff0c;多端运行” 的特性&#xff0c;极大地提高了开发效率&#xff0c…

JVM实战—9.线上FGC的几种案例

大纲 1.如何优化每秒十万QPS的社交APP的JVM性能(增加S区大小 优化内存碎片) 2.如何对垂直电商APP后台系统的FGC进行深度优化(定制JVM参数模版) 3.不合理设置JVM参数可能导致频繁FGC(优化反射的软引用被每次YGC回收) 4.线上系统每天数十次FGC导致频繁卡顿的优化(大对象问题…

电脑找不到mfc110.dll文件要如何解决?Windows缺失mfc110.dll文件快速解决方法

一、mfc110.dll文件的重要性 mfc110.dll&#xff0c;全称Microsoft Foundation Class Library 110&#xff0c;是Microsoft Visual C Redistributable for Visual Studio 2012的一部分。这个动态链接库&#xff08;DLL&#xff09;文件对于支持基于MFC&#xff08;Microsoft F…

《机器学习》——数据标准化(0~1标准化,z标准化)

文章目录 数据标准化一、什么是标准化二、常用标准化0~1标准化z标准化 三、注意事项 数据标准化 一、什么是标准化 数据标准化是一种数据预处理技术&#xff0c;用于将数据按照一定的规则进行变换&#xff0c;使得不同特征或变量具有可比性和一致性。作用 消除量纲影响 在实际…

【Vim Masterclass 笔记02】第3章:Vim 核心知识 + L08:Vim 核心浏览命令 + L09:Vim 核心浏览命令同步练习

文章目录 Section 3&#xff1a;Vim Essentials&#xff08;Vim 核心知识&#xff09;S03L08 Essential Navigation Commands1 光标的上下左右移动2 上 / 下翻页3 基于单词前移4 基于单词后移5 重新定位视图中的文本&#xff08;页面重绘&#xff09;6 定位到所在行的行首7 光标…

2025工作管理综合指南:Jira、Confluence等Atlassian工具套件在工作管理中的应用

在高效的工作场所中&#xff0c;沟通、协作与协调是驱动团队效能与生产力提升的核心要素。企业需构建无缝信息流、顺畅的交接与标准化的流程&#xff0c;以确保无论团队采用何种工作模式——面对面、远程或混合——都能实现高效运作。一套强大的工作管理解决方案&#xff0c;作…

MyBatis-plus sql拦截器

因为业务需求&#xff0c;重新写了一套数据权限。项目中用的是mybtis-plus&#xff0c;正好MyBatis-Plus提供了插件数据权限插件 | MyBatis-Plus&#xff0c;那就根据文档来实现这个需求。 实现&#xff1a; 实现MultiDataPermissionHandler 首先创建MultiDataPermissionHan…

vue导入导出excel、设置单元格文字颜色、背景色、合并单元格(使用xlsx-js-style库)

npm i xlsx-js-style <template><button click"download">下载 Excel 表格</button><el-table :data"tableData" style"width: 100%"><el-table-column prop"date" label"日期" width"180…

Ansys Discovery 中的网格划分方法:探索模式

本篇博客文章将介绍 Ansys Discovery 中可用于在探索模式下进行分析的网格划分方法。我们将在下一篇博客中介绍 Refine 模式下的网格划分技术。 了解 Discovery Explore 模式下的网格划分 网格划分是将几何模型划分为小单元以模拟系统在不同条件下的行为的过程。这是通过创建…

Android布局layout的draw简洁clipPath实现圆角矩形布局,Kotlin

Android布局layout的draw简洁clipPath实现圆角矩形布局&#xff0c;Kotlin 通常&#xff0c;如果要把一个相对布局&#xff0c;FrameLayout&#xff0c;或者线性布局等这样的布局变成具有圆角或者圆形的布局&#xff0c;需要增加一个style&#xff0c;给它设置圆角&#xff0c;…

【中间件】docker+kafka单节点部署---zookeeper模式

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言消息中间件介绍1. KRaft模式2. zookeeper模式2.1. 单节点部署安装验证 前言 最近生产环境上准备部署ELFK日志监控&#xff0c;先在测试环境部署单节点kafka验证…

38 Opencv HOG特征检测

文章目录 HOGDescriptor 构造函数setSVMDetector 设置支持向量机&#xff08;SVM&#xff09;检测器&#xff0c;用于目标检测。compute 用于计算图像区域的HOG描述符。detectMultiScale 多尺度检测目标。示例 HOGDescriptor 构造函数 HOGDescriptor(); HOGDescriptor(const S…

音视频-----RTSP协议 音视频编解码

流媒体协议详解&#xff1a;RTSP、RTP、RTCP、SIP、SDP、RTMP、WebRTC、WebSocket-CSDN博客 上文讲解比较清楚 多媒体编解码基础知识 一文详解WebRTC、RTSP、RTMP、SRT-腾讯云开发者社区-腾讯云 RTP :(Real-time Transport Protocol)是用于Internet上针对多媒体数据流的一种传…

著名的软件都用什么语言编写?

你是否曾经好奇&#xff0c;身边那些耳熟能详的软件&#xff0c;它们究竟是用什么语言编写的&#xff1f;从操作系统到浏览器、从数据库到编程工具&#xff0c;每一款软件背后都承载着开发者们的智慧与技术选型。那么&#xff0c;究竟哪些编程语言成就了这些世界级的软件呢&…