小A点菜
题目背景
uim
神犇拿到了uoi
的ra
(镭牌)后,立刻拉着基友小A
到了一家......餐馆,很低端的那种。
uim
指着墙上的价目表(太低级了没有菜单),说:"随便点"。
题目描述
不过uim
由于买了一些辅(e)辅(ro)书
,口袋里只剩(M)元((M le 10000))。
餐馆虽低端,但是菜品种类不少,有(N)种((N le 100)),第(i)种卖(a_i)元((a_i le 1000))。由于是很低端的餐馆,所以每种菜只有一份。
小A
奉行"不把钱吃光不罢休",所以他点单一定刚好吧uim
身上所有钱花完。他想知道有多少种点菜方法。
由于小A
肚子太饿,所以最多只能等待(1)秒。
输入输出格式
输入格式
第一行是两个数字,表示(N)和(M)。
第二行起(N)个正数(a_i)(可以有相同的数字,每个数字均在(1000)以内)。
输出格式
一个正整数,表示点菜方案数,保证答案的范围在(int)之内。
输入输出样例
输入样例 #1
4 4
1 1 2 2
输出样例 #1
3
分析
裸01
背包。(好吧没有采药裸,这题实际上有点变化)
首先我们设(dp_{i,j})为点前(i)个菜花费(j)元钱的方案数,那么状态转移方程有(3)种情况:(j > a_i),(j = a_i),(j < a_i)。分别考虑:
如果(j < a_i),说明当前的钱买不了这个菜了。那还能怎么办?方案数和不买这个菜的数一样,也就是(dp_{i,j} = dp_{i - 1, j})。
如果(j = a_i),说明当前的钱恰好可以买这个菜,也就是多了一种买菜方案。(dp_{i,j} = dp_{i - 1,j} +1)。
如果(j > a_i),那么我们可以吃,可以不吃。吃的方案数是(dp_{i - 1,j - a_i}),不吃的还是(dp_{i-1,j})。答案就是两者之和:(dp_{i - 1,j - a_i} + dp_{i-1,j})。
细心的同学可能会发现,第二个条件和第三个条件可以合并考虑。(j = a_i)的时候,如果把他用(j > a_i)的方法计算,结果是(dp_{i - 1,0} + dp_{i-1,j}),和原来的(dp_{i,j} = dp_{i - 1,j} +1)对比一下,我们只要初始化所有的(dp_{i,0})都为1,第二条和第三条就能合并考虑。
二维版本的递推方程:
(dp_{i,j}=egin{cases}dp_{i - 1, j}&j < a_i\dp_{i - 1,j - a_i} + dp_{i-1,j}&j ge a_iend{cases})
接下来考虑降维。采用滚动数组的思想,(dp_{i-1})降维成(pre),(dp_{i})降维成(now)。
(now_j=egin{cases}pre_j&j < a_i\pre_{j - a_i} + pre_j&j ge a_iend{cases})
但是实际过程中,我们优化成了一个一维数组。咋肥四呢?首先我们先把(now)和(pre)都无脑换成(dp)。
(dp_j = egin{cases}dp_j&j < a_i\dp_{j - a_i} + dp_j&j ge a_iend{cases})
也就是说,当(j ge a_i)的时候,(dp_j = dp_j + dp_{j-a_i})。
但是这个递推方程仍然可以画上等号吗?
首先我们令(now_j
ightarrow dp_j),首先当(now_j)没更新的时候,(now_j = pre_j),但是我们还得让(pre_{j - a_i} = dp_{j-a_i}),也就是说,我们需要让(dp_{j-a_i})保持在(i - 1)层状态,大白话,我们不能让(dp_{j-a_i})更新。但是(j-a_i le j),如果我们内部循环是正这遍历的,求(dp_j)的时候,(dp_{j - a_i})必然会更新,这会导致结果的错误。所以我们必须让内部循环倒着遍历,这样(dp_{j - a_i})还是旧的(i - 1)层,结果就正确啦。
刚刚证明完了可以画等号,只是需要倒序遍历,也就是说(j ge a_i)的时候,(dp_j = dp_j + dp_{j-a_i})这个结论是正确的。这下整个问题就会变得异常简单,我们只需要让(j)从(m)倒序遍历到(a_i),然后dp[j] += dp[j - a[i]]
。
上代码。
代码
#include <iostream>
#include <cstdio>
const int maxn = 105;
const int maxm = 10005;
int n, m, a[maxn], dp[maxm] = {1};//别忘了,dp[0]必须是1哦!
int main() {
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
scanf("%d", &a[i]);
for(int i = 1; i <= n; i++)
for(int j = m; j >= a[i]; j--)
dp[j] += dp[j - a[i]];
printf("%d
", dp[m]);
return 0;
}
评测结果
AC 100
:R31579801