多重集合组合数问题 (计数dp)

题目大意:
n n n 种物品,第 i i i 种物品有 a i a_i ai 个,不同种类的物品可以相互区分但是同一种的物品无法相互区分
求从这些物品种取出 m m m 个物品有多少种取法

题目分析:
为了避免重复计数,我们对一种物品尽量一次性拿完,故定义状态: d p [ i ] [ j ] dp[i][j] dp[i][j] 为前 i i i 种物品取出了 j j j 个物品的方案数

考虑转移,前 i i i 种物品取出了 j j j 个物品这个状态可以由前 i − 1 i-1 i1 种物品取出了 j − k j-k jk 个物品再从第 i i i 种物品种选取 k k k 个物品转移过来,故有转移方程:
d p [ i + 1 ] [ j ] = ∑ k = 0 m i n ( j , a i ) d p [ i ] [ j − k ] dp[i+1][j]=\sum_{k=0} ^{min(j,a_i)} dp[i][j-k] dp[i+1][j]=k=0min(j,ai)dp[i][jk]
此转移方程的时间复杂度显然为 O ( n ∗ m 2 ) O(n*m^2) O(nm2) ,复杂度不太优秀

那么继续考虑优化,我们发现求和上界 m i n ( j , a i ) min(j,a_i) min(j,ai) 可以分为两种情况:

  1. j ≤ a i j \le a_i jai ,将求和公式部分展开取出 d p [ i ] [ j ] dp[i][j] dp[i][j] 再化成求和公式的形式
    ∑ k = 0 m i n ( j , a i ) d p [ i ] [ j − k ] \sum_{k=0} ^{min(j,a_i)} dp[i][j-k] k=0min(j,ai)dp[i][jk]
    = ∑ k = 0 j d p [ i ] [ j − k ] = \sum_{k=0} ^j dp[i][j-k] =k=0jdp[i][jk]
    = d p [ i ] [ j ] + d p [ i ] [ j − 1 ] + . . . + d p [ i ] [ 0 ] = dp[i][j]+dp[i][j-1]+...+dp[i][0] =dp[i][j]+dp[i][j1]+...+dp[i][0]
    = d p [ i ] [ j ] + ∑ k = 0 j − 1 d p [ i ] [ j − 1 − k ] = dp[i][j]+\sum_{k=0} ^{j-1} dp[i][j-1-k] =dp[i][j]+k=0j1dp[i][j1k]
    ∑ k = 0 j − 1 d p [ i ] [ j − 1 − k ] \sum_{k=0} ^{j-1} dp[i][j-1-k] k=0j1dp[i][j1k] 带回状态转移方程有
    ∑ k = 0 j − 1 d p [ i ] [ j − 1 − k ] \sum_{k=0} ^{j-1} dp[i][j-1-k] k=0j1dp[i][j1k]
    = ∑ k = 0 j − 1 , a I d p [ i ] [ ( j − 1 ) − k ] = \sum_{k=0} ^{j-1,a_I} dp[i][(j-1)-k] =k=0j1,aIdp[i][(j1)k]
    = d p [ i + 1 ] [ j − 1 ] = dp[i+1][j-1] =dp[i+1][j1]
    故在此时有 d p [ i + 1 ] [ j ] = d p [ i ] [ j ] + d p [ i + 1 ] [ j − 1 ] dp[i+1][j] = dp[i][j]+dp[i+1][j-1] dp[i+1][j]=dp[i][j]+dp[i+1][j1]
  2. j > a i j \gt a_i j>ai ,将求和公式部分展开取出 d p [ i ] [ j ] dp[i][j] dp[i][j] d p [ i ] [ j − 1 − a i ] dp[i][j-1-a_i] dp[i][j1ai] 再化成求和公式的形式(为了方便推导增加式子 d p [ i ] [ j − 1 − a [ i ] ] − d p [ i ] [ j − 1 − a [ i ] ] dp[i][j-1-a[i]] - dp[i][j-1-a[i]] dp[i][j1a[i]]dp[i][j1a[i]],因为 j > a i j \gt a_i j>ai 故在 j − 1 − a i j-1-a_i j1ai 时才会为0所以数组不会越界)
    ∑ k = 0 m i n ( j , a i ) d p [ i ] [ j − k ] \sum_{k=0} ^{min(j,a_i)} dp[i][j-k] k=0min(j,ai)dp[i][jk]
    = ∑ k = 0 a i d p [ i ] [ j − k ] = \sum_{k=0} ^{a_i} dp[i][j-k] =k=0aidp[i][jk]
    = d p [ i ] [ j ] + d p [ i ] [ j − 1 ] + . . . + d p [ i ] [ j − a i ] = dp[i][j]+dp[i][j-1]+...+dp[i][j-a_i] =dp[i][j]+dp[i][j1]+...+dp[i][jai]
    = d p [ i ] [ j ] + d p [ i ] [ j − 1 ] + . . . + d p [ i ] [ j − a i ] + d p [ i ] [ j − 1 − a i ] − d p [ i ] [ j − a i − 1 ] = dp[i][j]+dp[i][j-1]+...+dp[i][j-a_i]+dp[i][j-1-a_i]-dp[i][j-a_i-1] =dp[i][j]+dp[i][j1]+...+dp[i][jai]+dp[i][j1ai]dp[i][jai1]
    取出 d p [ i ] [ j ] dp[i][j] dp[i][j] − d p [ i ] [ j − 1 − a i ] -dp[i][j-1-a_i] dp[i][j1ai] 对剩下的求和带入转移方程有:
    d p [ i ] [ j − 1 ] + d p [ i ] [ j − 2 ] + . . . + d p [ i ] [ j − 1 − a i ] dp[i][j-1]+dp[i][j-2]+...+dp[i][j-1-a_i] dp[i][j1]+dp[i][j2]+...+dp[i][j1ai]
    = ∑ k = 0 a i d p [ i ] [ ( j − 1 ) + k ] =\sum_{k=0}^{a_i}dp[i][(j-1)+k] =k=0aidp[i][(j1)+k]
    = d p [ i + 1 ] [ j − 1 ] =dp[i+1][j-1] =dp[i+1][j1]
    故在此时有 d p [ i + 1 ] [ j ] = d p [ i ] [ j ] + d p [ i + 1 ] [ j − 1 ] − d p [ i ] [ j − 1 − a i ] dp[i+1][j] = dp[i][j] + dp[i+1][j-1] - dp[i][j-1-a_i] dp[i+1][j]=dp[i][j]+dp[i+1][j1]dp[i][j1ai]

综合上述两种情况转移方程可以化简为:

if(j <= a[i]) dp[i][j] = dp[i-1][j]+dp[i][j-1]
else dp[i][j] = dp[i-1][j]+dp[i][j-1]+dp[i-1][j-1-a[i]]

此方程复杂度为 O ( n m ) O(nm) O(nm)
再思考状态 d p [ i ] [ j ] dp[i][j] dp[i][j] 其空间复杂度为 O ( n m ) O(nm) O(nm) ,我们发现在转移时我们只关心这一行和上一行的元素而不关心其他行的元素故可以对第一维 i   m o d   2 i \ mod \ 2 i mod 2 ,空间复杂度就能优化到 O ( m ) O(m) O(m)

例题:poj-3046Ant Counting
题目链接:点击这里
具体细节见代码

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>

#define ll long long
#define inf 0x3f3f3f3f
using namespace std;

int read()
{
	int res = 0,flag = 1;
	char ch = getchar();
	while(ch<'0' || ch>'9')
	{
		if(ch == '-') flag = -1;
		ch = getchar();
	}
	while(ch>='0' && ch<='9')
	{
		res = (res<<3)+(res<<1)+(ch^48);//res*10+ch-'0';
		ch = getchar();
	}
	return res*flag;
}
const int mod = 1e6;
int id,ans,dp[2][100005],x[1005];
int main()
{
	int t = read(),a = read(),s = read(),b = read();
	for(int i = 1;i <= a;i++)
	{
		id = read();
		x[id]++;
	}
	dp[0][0] = dp[1][0] = 1;
	for(int i = 1;i <= t;i++) 
	for(int j = 1;j <= b;j++)
	{
		if(j <= x[i])
		dp[i%2][j] = (dp[(i-1)%2][j]+dp[i%2][j-1])%mod;
		else 
		dp[i%2][j] = (dp[(i-1)%2][j]+dp[i%2][j-1]-dp[(i-1)%2][j-1-x[i]]+mod)%mod;
	}
	for(int i = s;i <= b;i++) 
		ans = (ans+dp[t%2][i])%mod;
	printf("%d\n",ans);
	return 0;
}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值