• LeetCode——Weekly Contest 320(附动态规划解题思路)


    LeetCode周赛第320场记录

    质量还不错的一场周赛,也可以学到不少知识。

    2475. 数组中不等三元组的数目(排序+荷兰国旗问题)

    在这里插入图片描述
    这道题非常简单,就是从头向后一一找出不含重复数字的三元组。我在比赛时直接写了一个三重循环暴力来解,这道题的数据规模是 1 0 2 10^2 102,因此 O ( n 3 ) O(n^3) O(n3)的复杂度是可以安全过的,但是暴力法不是最优解

    以下是暴力枚举的代码:

    class Solution {
    public:
        int unequalTriplets(vector<int>& nums) {
            int Ans = 0, n = nums.size();
    		for(int i = 0 ; i < n ; ++i)
    			for(int j = i + 1 ; j < n ; ++j)
    				if(nums[i] != nums[j])
    					for(int k = j + 1 ; k < n ; ++k)
    						if(nums[k] != nums[i] && nums[k] != nums[j])
    							++Ans;		
    		return Ans;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这道题还可以使用类似于荷兰国旗问题的思路来求解,因为三个数字的绝对数值不重要,所以对于每个特定数字而言,其对于答案的贡献是:

    比它小的数字个数 × \times × 自身个数 × \times × 比它大的数字个数

    可见整个区间又被分为了三个部分:

    1.[0, Left)区间表示比它小的元素区间

    2.[Left, i)表示当前元素的区间

    3.[i, n)表示比当前元素大的区间

    注意为了统一,我统统使用左闭右开区间来进行区间表示,这样写出的代码如下:

    这种算法的思维难度还是不低的,时间复杂度也降低至 O ( n log ⁡ n ) O(n\log{n}) O(nlogn)(排序算法复杂度)。

    int unequalTriplets(vector<int>& nums) {
            int Ans = 0;
            sort(nums.begin(), nums.end());
            int n = nums.size();
            int Left = 0;
            for(int i = 1 ; i < n  ; ++i)
                if(nums[i] != nums[i - 1])
                {
                    Ans += Left * (i - Left) * (n - i);
                    Left = i ;
                }
            return Ans;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    2476. 二叉搜索树最近节点查询(中序遍历+二分)

    在这里插入图片描述
    见到二叉搜索树一定要想到性质:中序遍历有序

    在比赛时做这道题,我一开始还是在使用传统的递归算法来处理,写完之后发现会超时,于是更换思路到中序遍历。第二个思路就是理所当然的将二叉搜索树进行中序遍历,但是遇到二叉搜索树应该第一次就想到这种做法,因为它直接将树的问题转化为了线性序列问题

    对上述二叉搜索树进行中序遍历之后,就可以按照题意来找Min和Max了,这里可以使用二分查找来找到这个数字,C++ STL里的lower_bound和upper_bound函数可以很好地完成这个任务,但是这个地方的逻辑也比较微妙,处理不好很容易犯错,下面给出代码:

    class Solution {
    	vector<int> InOrder{};
    	void DFS(TreeNode* root)
    	{
    		if(root)
    		{
    			DFS(root->left);
    			InOrder.push_back(root->val);
    			DFS(root->right);
    		}
    	}
    public:
        vector<vector<int>> closestNodes(TreeNode* root, vector<int>& queries) {
    		vector<vector<int>> Ans;
            DFS(root);                      // 得到二叉搜索树的中序遍历序列
    		int n = InOrder.size();         
    		for(auto Each : queries)        // O(m)
    		{
    			auto MinIter = upper_bound(InOrder.begin(), InOrder.end(), Each);   
    			auto MaxIter = lower_bound(InOrder.begin(), InOrder.end(), Each);    
    			/*
    				注意这里的处理逻辑:
    				Min要使用upper_bound来找
    				Max要使用lower_bound来找
    				同时还要判断是否是特殊的迭代器,因为这可能意味着它们没有找到解
    			*/
    			Ans.push_back({MinIter == InOrder.begin() ? -1 : *(MinIter - 1), MaxIter == InOrder.end() ? -1 : *MaxIter});
    		}
    		return Ans;
    
            // complexity : O(n + m * logn)
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    2477. 到达首都的最少油耗(转化+多叉树的DFS)

    在这里插入图片描述
    这道题主要考察问题的转化,题目本身不难但是很容易会错意,我一开始想把这道题当成图的单源点最短路径问题,使用Dijkstra算法来解。但这是不对的,因为虽然每条路费一升汽油可以当作边的权重,但是这里还有一个座位seats的限制量我不知道该如何转化。

    其实这就是一个简单的多叉树的DFS问题,一辆车载下尽可能多的代表就是最省油的做法,所以对于图中的(多叉树中的)每一个节点而言,将它所连接的孩子节点的代表完全聚合就是最简单的做法,多余的车可以停在父节点处不动,它们并不额外消耗汽油。

    所以这个问题转化完成之后就成为了一颗多叉树的遍历问题,从任何一个节点出发,统计它的孩子节点数量到本地完成汇聚之后再通往它的父节点,一层层汇聚上去直到根节点(首都)即可。这里需要注意,树作为一种图的特殊情况,多叉树的遍历也是不需要开辟Visited数组的,因为它们不会有环的出现:

    重要知识点,多叉树的遍历不需要开辟Visited的数组(因为无环)

    只需要记录下父节点编号,在遍历时剔除掉即可

    另外一个需要注意的点是,如何对除法做上取整,这里可以参考这篇博客

    // a,b均为整数,a除以b(上取整)的方法是:
    x = (a + b - 1) / b
    
    • 1
    • 2

    给出这道题的完整代码如下:

    class Solution {
    public:
        long long minimumFuelCost(vector<vector<int>>& roads, int seats) {
            int n = roads.size() + 1;
    
            // 建图
            vector<int> e[n];
            for (auto &road : roads) {
                e[road[0]].push_back(road[1]);
                e[road[1]].push_back(road[0]);
            }
    
            long long ans = 0;
            // DFS 统计子树大小,同时统计答案, fa是父节点编号,遍历时避开它即可
            function<int(int, int)> dfs = [&](int sn, int fa) {
                int ret = 1;
                for (int fn : e[sn]) if (fn != fa) {
                    // 计算 sn -> fn 这条边的贡献
                    int t = dfs(fn, sn);
                    ans += (t + seats - 1) / seats;
                    // 更新子树节点大小
                    ret += t;
                }
                return ret;	// 返回结果到上一层
            };
            dfs(0, -1);
            return ans;
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    2478. 完美分割的方案数

    在这里插入图片描述

    这又是一道动态规划的题目,在bilibili上看了灵茶山艾府的讲解视频,他在视频中讲解了求解动态规划问题的思考方法,这里要简单做个记录。

    解决动态规划问题的思考步骤:
    Q1.问题中存在哪些变量?
    A1.本题中有分割子串的个数k,原字符串的长度n,每个子字符串的最小长度MinLength.
    
    Q2.在上述的基础上复述问题,并且替换变量
    A2.把一个长为j的字符串,分割出i段子字符串的合法方案数
    
    Q3.想最后一步发生了什么?(*)
    A3.最后一步我们分割出来了一个子字符串,长度为x,且它是原字符串的一个后缀
    
    Q4.去掉最后一步,问题规模缩小了,原问题会变成什么样?
    A4.把一个长度为j-x的字符串,分割成i-1段的合法方案数
    
    Q5.得到状态转移方程
    A5.从A2中可以得到,f[i][j]表示把字符串s的前j个字符分割成i段的合法方案数
    (为什么是”前“j个字符,因为在A3中我们去掉的是原字符串的后缀)
    从A4中可以得到,f[i][j] += f[i - 1][j'],其中j'是i-1段的结束下标,因此这里需要枚举j'
    j'需要满足一些限制:
    	1. j - j' + 1 >= minLength
    	2. s[j']是质数,s[j]不是质数
    
    Q6.初始值和答案分别对应于f的什么?
    f[0][0] = 1;
    Ans = f[k][n] 	
    
    Q7.是否可以优化转移方程?
    滚动数组 or others?
    本题中j在变大的时候j'也在变大(因为要满足最短长度的限制),所以可以有优化手段->前缀和优化
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    在完成上述的分析过程之后,下面直接给出本题的代码(copyright:灵茶山艾府):
    这道题可以和Weekly Contest318的最小移动总距离一起服用,尝试使用上述的流程来解决之

    class Solution {
        const int MOD = 1e9 + 7;
    
        bool is_prime(char c) {
            return c == '2' || c == '3' || c == '5' || c == '7';
        }
    
        // 判断是否可以在 j-1 和 j 之间分割(开头和末尾也算)
        bool can_partition(string &s, int j) {
            return j == 0 || j == s.length() || !is_prime(s[j - 1]) && is_prime(s[j]);
        }
    
    public:
        int beautifulPartitions(string &s, int k, int l) {
            int n = s.length();
            if (k * l > n || !is_prime(s[0]) || is_prime(s[n - 1])) // 剪枝
                return 0;
            int f[k + 1][n + 1]; memset(f, 0, sizeof(f));
            f[0][0] = 1;
            for (int i = 1; i <= k; ++i) {
                int sum = 0;
                // 优化:枚举的起点和终点需要给前后的子串预留出足够的长度
                for (int j = i * l; j + (k - i) * l <= n; j++) {
                    if (can_partition(s, j - l)) sum = (sum + f[i - 1][j - l]) % MOD; // 这里的j-l可以看成j’,双指针
                    if (can_partition(s, j)) f[i][j] = sum;
                }
            }
            return f[k][n];
        }
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
  • 相关阅读:
    Swing基本组件的用法(一)
    OpManager-系统性能监控工具
    LeetCode-102-二叉树的层序遍历
    Android12之强弱智能指针sp/wp循环引用死锁问题(一百六十六)
    聚合物纳米材料造影剂供应|超声联合载血卟啉PLGA造影剂|携抗HER-2抗体PLGA高分子纳米超声造影剂
    JS-谈谈你所理解的闭包
    Mac电脑Dock窗口预览工具 DockView
    计算机网络TCP篇之流量控制
    Maven 入门教程
    源码漏洞扫描
  • 原文地址:https://blog.csdn.net/zzy980511/article/details/128039226