[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}=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 100R31579801

原文地址:https://www.cnblogs.com/crab-in-the-northeast/p/luogu-p1164.html