atcoder 题目传送门,经典的 dp + 前缀和优化。
题意
有 n n n 个不同的小朋友和 k k k 个相同的糖,给每个小朋友分配若干个糖且糖要分配完,每个小朋友分配糖的上限个数为 a i a_i ai,求分配方案总数。
-
1 ≤ n ≤ 100 1\le n\le 100 1≤n≤100
-
1 ≤ k ≤ 1 0 5 1\le k\le 10^5 1≤k≤105
-
1 ≤ a i ≤ k 1\le a_i\le k 1≤ai≤k
思路
-
朴素的 dp 状态: d p ( i , j ) dp(i,j) dp(i,j) 表示考虑到第 i i i 个小朋友,当前已经用掉 j j j 个糖的方案数。
-
转移方程:
d p ( i , j ) = ∑ k = max ( 0 , j − a i − 1 ) j d p ( i − 1 , k ) dp(i,j)=\sum\limits_{k=\max(0,j-a_{i-1})}^jdp(i-1,k) dp(i,j)=k=max(0,j−ai−1)∑jdp(i−1,k)
这样一来转移的复杂度是 O ( k ) O(k) O(k),总复杂度为 O ( n k 2 ) O(nk^2) O(nk2),会超时。这时候我们进行优化,就必须要观察转移方程的特点了。
- 优化:发现公式里每个 dp 值都由上一行中一段连续的 dp 值之和转移而来,可以前缀和优化转移,每次用
s
u
m
sum
sum 数组预处理上一行
d
p
dp
dp 的前缀和,可以
O
(
1
)
O(1)
O(1) 地求出连续一段 dp 值之和并转移。具体处理如下:
s u m ( i ) = ∑ j = 0 i d p ( i − 1 , j ) sum(i)=\sum\limits_{j=0}^idp(i-1,j) sum(i)=j=0∑idp(i−1,j)
时间复杂度: O ( n k ) O(nk) O(nk)
空间复杂度: O ( n k ) O(nk) O(nk)
代码
#include <bits/stdc++.h>
#define ll long long
#define pb push_back
#define pii pair<int,int>
#define mp make_pair
#define F first
#define S second
using namespace std;
const ll MOD=1e9+7;
int n,m,a[105];
ll dp[105][100005],sum[100005];
ll getsum(int l,int r)//前缀和
{
return (l==0?sum[r]:(sum[r]+MOD-sum[l-1])%MOD);
}
int main()
{
cin>>n>>m;
for(int i=0;i<n;i++)
cin>>a[i];
dp[0][0]=1ll;
for(int i=1;i<=n;i++)
{
sum[0]=dp[i-1][0];//处理上一行dp的前缀和
for(int k=1;k<=m;k++)
sum[k]=(sum[k-1]+dp[i-1][k])%MOD;
for(int j=0;j<=m;j++) //按公式转移
dp[i][j]=getsum(max(0,j-a[i-1]),j);
}
cout<<dp[n][m]<<endl;
return 0;
}
其实我的 dp 公式是从前面的值转移到当前值的。也有其他的选手喜欢用当前值更新后面值的转移方式。这样的话优化就会运用到一个差分数组,其实和前缀和是异曲同工的。
希望题解对你有帮助!