【LeetCode第115场双周赛】100029. 和带限制的子多重集合的数目 | 前缀和背包 | 中等

题目内容

原题链接

给定一个长度为 n n n 的数组 n u m s nums nums 和一个区间左右端点 l , r l,r l,r

返回 n u m s nums nums 中子多重集合的和在闭区间 l , r l, r l,r 之间的 子多重集合的数目 。

子多重集合 指的是从数组中选出一些元素构成的 无序 集合,每个元素 x x x 出现的次数可以是 0 , 1 , . . . , o c c x 0, 1, ..., occx 0,1,...,occx 次,其中 o c c x occx occx 是元素 x x x 在数组中的出现次数。

注意:

  • 如果两个子多重集合中的元素排序后一模一样,那么它们两个是相同的子多重集合 。
  • 空集合的和是 0 。

数据范围

  • 1 ≤ n ≤ 2 ⋅ 1 0 4 1\leq n\leq 2\cdot 10^4 1≤n≤2⋅104
  • 0 ≤ n u m s i ≤ 2 ⋅ 1 0 4 , s u m ( n u m s ) ≤ 2 ⋅ 1 0 4 0\leq numsi\leq 2\cdot 10^4, sum(nums)\leq 2\cdot 10^4 0≤numsi≤2⋅104,sum(nums)≤2⋅104
  • 0 ≤ l ≤ r ≤ 2 ⋅ 1 0 4 0\leq l\leq r\leq 2\cdot 10^4 0≤l≤r≤2⋅104

题解

本题从数据范围出发。

考虑 n u m s nums nums 总和不超过 20000 20000 20000 ,那么 n u m s nums nums 中不同的数有多少个呢?

考虑最小的情况下有 x x x 种数,每种数一个,那么总和为: x × ( x + 1 ) 2 \frac{x\times (x+1)}{2} 2x×(x+1)

当 x = 200 x=200 x=200 时, x × ( x + 1 ) 2 = 20100 > 20000 \frac{x\times (x+1)}{2}=20100>20000 2x×(x+1)=20100>20000 ,故至多有 199 199 199 个不同的数。

那么问题转换为一个分组背包问题,值为 v v v 的数的个数有 c c c 个,那么可以选择这个数 0 , v 0,v 0,v 次,这样可以转换成 01 01 01 背包,最多有 O ( n ) O(n) O(n) 个物品。

这样时间复杂度为: O ( n ∑ n u m s ) O(n\sum nums) O(n∑nums) , 4 e 8 4e8 4e8 不能通过。

考虑从定义出发:
d p i j dpij dpij 表示前 i i i 种物品,容量使用为 j j j 的方案数。

那么 d p i j dpij dpij 的状态转移方程是什么呢?

d p i j = ∑ k d p i − 1 k dpij=\sum\limits_{k} dpi-1k dpij=k∑dpi−1k ,满足 0 ≤ j − k ≤ c n t i × v a l i 0\leq j-k\leq cnti\times vali 0≤j−k≤cnti×vali

其中 c n t i cnti cnti 表示第 i i i 个数的数量, v a l i vali vali 表示第 i i i 个数的值。

那么就是要求一个区间和了。

麻烦在于如果二维转移,时间复杂度还是 O ( n ∑ n u m s ) O(n\sum nums) O(n∑nums) 。

这里具体的实现是,先用完全背包计算前缀和,然后最多考虑每个数的次数 c n t i cnti cnti 次。

cpp 复制代码
def func():
	dp = [0] * (r + 1)
	dp[0] = 1
	nums_cnt
	// 枚举每个数及其次数
	for v, c in nums_cnt;
		for i in range(x, r + 1):
			dp[i] += dp[i - 1]
		// 这样 dp[i] 就是考虑有 0 个,1 个,... i/v 个数 v 的集合
		// 做完了前缀和
		// 但是需要注意的是,数的数量只有 c 个
		// 所以我们还需要多的部分
		for i in range(r + 1, (c + 1) * v - 1, -1):
			// dp[i] 只能由 dp[i], dp[i-v], dp[i-2v], ..., dp[i-cv] 
			// 转移而来,所以对于 dp[i-(c+1)*v]存储的是 i-(c+1)*v 的前缀和,
			// 其并不能转移到 dp[i] ,删去即可
			dp[i] -= dp[i-(c + 1) * v]
	// 最后考虑 0 选择即可,有 zero + 1 种选法
	return (nums_cnt[0] + 1) * sum(dp[l:r+1])

时间复杂度: O ( 200 ∑ n u m s ) O(200\sum nums) O(200∑nums)

代码

cpp 复制代码
class Solution {
public:
    int countSubMultisets(vector<int>& nums, int l, int r) {
        const int MAX = 20010;
        const int MOD = 1e9 + 7;
        vector<int> dp(r + 1);
        dp[0] = 1;
        
        vector<int> cnt(MAX);
        vector<int> vec;
        int zero = 0;
        for (int u: nums) {
            if (u == 0) {
                zero += 1;
                continue;
            }
            cnt[u] += 1;
            if (cnt[u] == 1) vec.push_back(u);
        }
        
        for (int u: vec) {
            for (int i = u; i <= r; ++i) {
                dp[i] += dp[i - u];
                if (dp[i] >= MOD) dp[i] -= MOD;
            }
            int left = (cnt[u] + 1) * u;
            for (int i = r; i >= left; --i) {
                dp[i] -= dp[i - left];
                if (dp[i] < 0) dp[i] += MOD;
            }
        }
        
        int ans = 0;
        for (int i = l; i <= r; ++i) {
            ans += dp[i];
            if (ans >= MOD) ans -= MOD;
        }
        
        ans = 1ll * ans * (zero + 1) % MOD;
        
        return ans;
    }
};
相关推荐
折哥的程序人生 · 物流技术专研1 天前
Java面试85题图解版 · 特别篇:2026后端高频面试题复盘(算法底层逻辑+高并发架构设计全解析,附Java实战代码)
java·网络·数据库·算法·面试
想吃火锅10051 天前
【leetcode】14.最长公共前缀js
算法·leetcode·职场和发展
云絮.1 天前
数据库操作
数据库·mysql·算法·oracle
小林ixn1 天前
LeetCode 206. 反转链表(迭代 + 递归详解)
算法·leetcode·链表
凡人叶枫1 天前
Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
java·linux·开发语言·c++·算法
我爱cope1 天前
【Agent智能体26 | 多智能体-多智能体工作流】
人工智能·设计模式·语言模型·职场和发展
菜鸟‍1 天前
LeetCode 1 27 和 704 || 两数之和 移除元素 二分查找
算法·leetcode·职场和发展
退休倒计时1 天前
【每日一题】LeetCode 142. 环形链表 II TypeScript
算法·leetcode·链表·typescript
popcorn_min1 天前
Digits 手写数字识别:随机森林多分类 + 像素级特征热力图
算法·随机森林·分类
liulilittle1 天前
拥塞控制:排水终止的两种决策:OR 与 AND
网络·tcp/ip·计算机网络·算法·信息与通信·tcp·通信