[luogu p1164] 小A点菜

传送门

小A点菜

题目背景

uim神犇拿到了uoira(镭牌)后,立刻拉着基友小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}=\begin{cases}dp_{i - 1, j}&j < a_i\\dp_{i - 1,j - a_i} + dp_{i-1,j}&j \ge a_i\end{cases}\)

接下来考虑降维。采用滚动数组的思想,\(dp_{i-1}\)降维成\(pre\)\(dp_{i}\)降维成\(now\)

\(now_j=\begin{cases}pre_j&j < a_i\\pre_{j - a_i} + pre_j&j \ge a_i\end{cases}\)

但是实际过程中,我们优化成了一个一维数组。咋肥四呢?首先我们先把\(now\)\(pre\)都无脑换成\(dp\)

\(dp_j = \begin{cases}dp_j&j < a_i\\dp_{j - a_i} + dp_j&j \ge a_i\end{cases}\)

也就是说,当\(j \ge a_i\)的时候,\(dp_j = dp_j + dp_{j-a_i}\)

但是这个递推方程仍然可以画上等号吗?
首先我们令\(now_j \rightarrow 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\n", dp[m]);
    return 0;
}

评测结果

AC 100R31579801

posted @ 2020-03-10 11:55  dbxxx  阅读(266)  评论(0编辑  收藏  举报