LeetCode刷题|368最大整除子集(动态规划)

本文探讨了一道动态规划题目——最大整除子集。通过举例说明暴力枚举方法的不足,并详细解释了如何利用动态规划来解决这个问题。首先对数组进行排序,然后定义dp数组记录每个元素在序列中的位置。通过遍历数组,找到能够被前面元素整除的情况,并更新dp数组。最后,根据dp数组回溯得到最长的整除子集。

今天的每日一题还是一个动态规划题目。

题目描述

在这里插入图片描述
题目地址:最大整除子集

动态规划

如果考虑暴力法的话这道题应该怎么做呢,最简单的能想到的方法就是枚举每一个元素作为开头,然后遍历其他元素。但是这种方法并不可靠,可以举个例子看一下:
[ 9 , 18 , 54 , 90 , 108 , 180 , 360 , 540 , 720 ] [9,18,54,90,108,180,360,540,720] [9,18,54,90,108,180,360,540,720]这样一个数组,从9开始的话会得到 [ 9 , 18 , 54 , 108 , 540 ] [9,18,54,108,540] [9,18,54,108,540],但是实际上还有更长的结果 [ 9 , 18 , 90 , 180 , 360 , 720 ] [9,18,90,180,360,720] [9,18,90,180,360,720],而题目要求我们返回最长的结果,这种方法显然是不行的。
下面来开始考虑如何使用动态规划解决这个问题。这个问题是一个序列DP问题,每一个状态都依赖于与前一个状态的关系,也就是说,当前元素nums[i]能否接在nums[j]后面,取决于nums[i] % nums[j]结果是否为0。了解了这一点,那么我们定义数组dp[],dp[i]表示当前元素是所在序列的第几个位置。还是举个例子:

nums24789121620
dp12131343

定义了dp之后,要怎么求得dp[i]呢。首先我们得把数组按从小到大排个序,因为要使得nums[i] % nums[j]等于0,nums[i]就一定要比nums[j]大才行。然后依次与i前面的数进行%运算,如果运算结果为0,那么dp[i]就等于dp[j]+1,也就是在位置j的元素后面一位。但是由于前面不止一个元素,所以需要更新dp[i],选出最大的dp[i]。
得到dp数组结果,也就是上面表格的结果之后,我们根据表格内容倒着遍历一遍把最长的一个串取出来就可以了。

class Solution:
    def largestDivisibleSubset(self, nums: List[int]) -> List[int]:
        n = len(nums)
        nums.sort()
        dp = [1] * n
        maxSize = 1
        maxVal = dp[0]
        for i in range(1,n):
            for j in range(i):
                if nums[i] % nums[j] == 0:
                    dp[i] = max(dp[i], dp[j]+1) 
            if dp[i] > maxSize:
                maxSize = dp[i]
                maxVal = nums[i]
        
        res = []
        if maxSize==1:return [nums[0]]
        k = n-1
        while k >= 0:
            if dp[k] == maxSize and maxVal % nums[k] == 0:
                res.append(nums[k])
                maxVal = nums[k]
                maxSize = maxSize-1
            k = k-1
        return res

maxSize的作用是记录下最长序列的长度,在最后得到结果数组时每加入一个元素maxSize就减一。maxVal记录的是最大序列的最大的值,因为最后得到结果的时候可能会出现dp数组多个位置都是同一个数,这时候就需要用maxVal进行%运算,结果为0才是这一结果序列的元素。
最后,因为是倒着遍历,所以最后返回的结果是倒序的。

<think>首先,用户的问是关于“算法竞赛中组合学常见问及解方法”。我需要基于系统指令和提供的引用信息来构建回答。 关键指令: - 所有行内学表达式用$...$格式。 - 独立公式用$$...$$格式并单独成段。 - 使用中文回答。 - 生成相关问。 - 回答中引用的段落末尾自然地添加引用标识,例如[^1]。 - 回答结构清晰,逐步解决问,保证真实可靠。 - 参考站内引用:引用[1]、[2]、[3]提供了相关信息。 用户意图:了解算法竞赛中组合学的常见问、解技巧和相关算法。 从引用中提取相关部分: - 引用[1]:提到“素与超素表达式”,但主要标是“算法竞赛中的学问”,包括论、组合学等。 - 引用[2]:在“”部分提到“型:论、组合学、概率统计等。解思路:熟悉常见算法(如欧几里得算法、快速幂)。理解排列组合的基本原理。注意大处理和高精度计算。”这直接相关。 - 引用[3]:提到“学综合:论、组合学、概率期望等2200+”,强调了组合学在竞赛中的重要性。 我的回答结构: 1. **引言**:简要介绍组合学在算法竞赛中的作用。 2. **常见问类型**:列出组合学中的常见问,如排列组合、鸽巢原理、容斥原理等。 3. **解方法**:详细解释解技巧和相关算法,包括学公式和代码示例(如果需要)。 4. **示例**:提供一个简单的例子来演示。 5. **相关问**:生成2-3个相关问。 6. **引用**:在相关段落末尾添加引用标识。 确保学表达式格式正确: - 行内:如$C(n,k)$ - 独立:如二项式系公式$$ \binom{n}{k} = \frac{n!}{k!(n-k)!} $$ 内容大纲: - **常见问**: - 排列组合问(如计算排列、组合)。 - 鸽巢原理(抽屉原理)。 - 容斥原理(用于计)。 - 生成函(用于序列求和)。 - 图论中的组合问(如路径计)。 - 概率与组合(如期望值计算)。 - **解方法**: - 使用基本公式:如$P(n,k) = \frac{n!}{(n-k)!}$, $C(n,k) = \binom{n}{k}$。 - 动态规划:用于计算组合或解决优化问。 - 递归和分治。 - 学工具:如Lucas定理、Catalan。 - 高精度计算:处理大。 - 参考引用[2]中的解思路。 生成相关问: 1. 如何高效计算大组合模素? 2. 容斥原理在解决计中的应用实例? 3. 组合学在动态规划中的典型用例? 最后,确保回答真实可靠,基于标准算法竞赛知识。</think>### 算法竞赛中组合学常见问及解方法 组合学是算法竞赛的核心内容之一,涉及计、排列、组合等学问,常用于解决路径计子集生成、概率计算等场景。掌握其基础概念和解技巧能显著提升竞赛表现。下面我将逐步介绍常见问类型、解方法,并附上示例。 #### 一、组合学常见问类型 在算法竞赛中,组合学问通常分为以下几类,这些类型在目中频繁出现,要求选手灵活应用学原理: 1. **排列组合问**:计算对象排列或组合的量,如计算$n$个元素的排列$P(n,k)$或组合$C(n,k)$。常见于子集选择、序列生成等目[^2]。 2. **鸽巢原理(抽屉原理)问**:证明或计算在分配对象时,至少有一个“抽屉”包含多个对象。例如,证明在$n+1$个物品放入$n$个盒子时,至少有一个盒子包含两个物品[^2]。 3. **容斥原理问**:处理重叠集合的计,如计算多个集合的并集大小。典型应用包括求解满足多个条件的对象量(如“至少满足一个条件”的计)[^2]。 4. **生成函**:使用形式幂级表示序列,用于求解序列和或递推关系。例如,用生成函计算斐波那契列或组合优化问。 5. **图论中的组合问**:结合图论,如计算图中路径量、树的子树计或网络流中的组合优化。常见于最短路径变体或网络设计目[^3]。 6. **概率与组合问**:涉及期望值、概率分布的计算,如随机抽样或游戏策略中的胜率分析。这类问常结合组合计和概率公式[^3]。 #### 二、解方法与技巧 解决组合学问需结合学推导和算法实现。以下是关键解方法,参考竞赛策略和常见型[^2][^3]: 1. **基础公式应用**:直接使用学公式快速计算。例如: - 排列公式:$P(n,k) = \frac{n!}{(n-k)!}$ - 组合公式:$$ C(n,k) = \binom{n}{k} = \frac{n!}{k!(n-k)!} $$ 在代码中,可通过预计算阶乘优化(注意大处理)。 - 示例:计算从5人中选择3人的组合,$C(5,3) = 10$。 2. **动态规划(DP)**:用于高效计算组合或解决优化问,尤其当$n$较大时。DP可避免重复计算: - **状态定义**:设$dp[i][j]$表示前$i$个元素中选择$j$个的组合。 - **状态转移方程**:$dp[i][j] = dp[i-1][j] + dp[i-1][j-1]$(基于选或不选当前元素)。 - 代码示例(Python): ```python def nCr(n, k): dp = [[0] * (k+1) for _ in range(n+1)] for i in range(n+1): for j in range(min(i, k)+1): if j == 0 or j == i: dp[i][j] = 1 else: dp[i][j] = dp[i-1][j] + dp[i-1][j-1] return dp[n][k] ``` 此方法时间复杂度$O(nk)$,适合中等规模问[^2]。 3. **递归与分治**:处理子问分解,如Catalan计算(用于二叉树计或括号匹配): - Catalan公式:$$ C_n = \frac{1}{n+1} \binom{2n}{n} $$ - 递归实现:$C_n = \sum_{i=0}^{n-1} C_i C_{n-1-i}$,但需备忘录优化避免指级复杂度。 4. **学工具与定理**: - **容斥原理**:计算并集大小,公式为: $$ \left| \bigcup_{i=1}^n A_i \right| = \sum |A_i| - \sum |A_i \cap A_j| + \sum |A_i \cap A_j \cap A_k| - \cdots + (-1)^{n+1} |A_1 \cap \cdots \cap A_n| $$ 应用示例:求解1到100中不被2或3整除。 - **Lucas定理**:用于模素$p$下的大组合计算,$ \binom{n}{k} \mod p $可分解为小问。 - **鸽巢原理证明**:常用于构造性目,直接推导结论。 5. **高精度与大处理**:组合常涉及阶乘,易导致溢出。使用高精度库(如Python的`math.comb`)或模运算(如取模$10^9+7$)处理[^2]。 6. **优化策略**: - 预计算阶乘和逆元,实现$O(1)$查询组合。 - 结合贪心或二分搜索,减少搜索空间。 - 对于生成函,使用多项式乘法加速(如FFT算法)。 #### 三、示例问解析 **问**:计算$n \times m$网格中从左上角到右下角的路径(每次只能向右或向下移动)。 **解**: - 这本质是组合问:总路径为$C(n+m-2, n-1)$,因为需要$n-1$次向下和$m-1$次向右移动。 - 公式:$$ \binom{n+m-2}{n-1} = \frac{(n+m-2)!}{(n-1)!(m-1)!} $$ - 代码(使用动态规划优化): ```python def grid_paths(n, m): dp = [[0] * m for _ in range(n)] for i in range(n): for j in range(m): if i == 0 or j == 0: dp[i][j] = 1 else: dp[i][j] = dp[i-1][j] + dp[i][j-1] return dp[n-1][m-1] ``` 时间复杂度$O(nm)$,适用于大网格[^2]。 #### 四、备赛建议 - **重点**:多练习真如Codeforces或LeetCode上的组合学标签目(难度2200+),强化手玩能力和DP应用[^3]。 - **错误总结**:记录经典错(如边界条件处理),分析优化点。 - **资源参考**:使用在线库(如AtCoder)和书籍(如《组合学》),结合引用[2]中的框架[^2][^3]。 组合学在竞赛中无处不在,掌握这些方法能高效解决计和优化问。如果您有具体目,我可以进一步分析!
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值