题目背景
uim 神犇拿到了 uoi 的 ra(镭牌)后,立刻拉着基友小 A 到了一家……餐馆,很低端的那种。
uim 指着墙上的价目表(太低级了没有菜单),说:“随便点”。
题目描述
不过 uim 由于买了一些书,口袋里只剩 M 元 (M≤10000)。
餐馆虽低端,但是菜品种类不少,有 N 种 (N≤100),第 ii 种卖 ai 元 (ai≤1000)。由于是很低端的餐馆,所以每种菜只有一份。
小 A 奉行“不把钱吃光不罢休”的原则,所以他点单一定刚好把 uim 身上所有钱花完。他想知道有多少种点菜方法。
由于小 A 肚子太饿,所以最多只能等待 1 秒。
输入格式
第一行是两个数字,表示 N 和 M。
第二行起 N 个正数 ai(可以有相同的数字,每个数字均在 1000 以内)。
输出格式
一个正整数,表示点菜方案数,保证答案的范围在 int 之内。
输入输出样例
输入 #1
4 4
1 1 2 2
输出 #1
3
思路
动态规划:
这道题目的考点是动态规划。具体来说,它是一个典型的0-1背包问题的变种。什么是背包问题呢?背包问题描述了一种情况:你有一个背包,它有一个有限的容量Volume,同时你有一些物品,每个物品都有一个重量Weight和价值Value。目标是在不超过背包容量的前提下,选择一些物品放入背包,使得背包中的总价值最大化。而动态规划是解决背包问题最常用的方法之一。我们通过创建一个二维数组来存储每个物品组合下的最大价值,然后逐步计算直到找到最优解。
这道题就是将我们的口袋钱数作为背包,在背包完全被占满,即钱数完全花完的情况下,要得到最大价值,也就是总点菜方案是多少种。
我们创建一个二维数组cnt[i][j],i代表我们有前i个菜品可以考虑,j代表我们现在有j元的钱。cnt[i][j]代表在面对前i个菜品,我们有j元的情况下,我们最多有几种选择方案。
对于每一次,我们有三种情况
(1)当钱数不够时,我们可以选择的方案数只能等于拥有相同钱数 j 在面对i-1个菜品时的方案数cnt[i-1][j]。
(2)当钱数正好相等时,我们可以选择的方案就多了1,也就是拥有相同钱数 j 在面对i-1个菜品时的方案数cnt[i-1][j]+1。
(3)当钱数大于时,我们有两种选择,买或者不买。买的话,那么我们花的钱数就得加上money[i] , 所以我们要满足刚好把 j 元花完的情况的话,我们考虑的方案数就得是拥有钱数 j - money[i] 在面对i-1个菜品时的方案数cnt[i-1][j-money[i]](注意这与刚好钱数相等的情况不同,不需要加1);不买的话,那么我们的方案数就只能等于拥有相同钱数 j 面对i-1个菜品时的方案数cnt[i-1][j]。总次数就是两种情况相加起来。
cnt[i][j] = cnt[i-1][j] (j<money[i])
cnt[i][j] = cnt[i-1][j] + 1 (j==money[i])
cnt[i][j] = cnt[i-1][j] + cnt[i-1][j-money[i]](j>money[i])
我们再以图片的形式来直观的理解一下。
循环从i = 1,开始遍历,内层循环从j = 1开始遍历。
i = 1,j = 1,满足j == money[1],cnt[1][1] 加+1,cnt[1][1] = 1。j>1时,因为题目要求我们手里的前得全部花完,所以在只有一种菜品的情况下,我们后面的情况方案数都是0。
i = 2,j = 1,满足j == money[2],cnt[2][1] 加+1,cnt[i][j] = 2。j = 2,j>money[2],cnt[2][2] = cnt[1][2] + cnt[1][1] = 1 + 0 = 1。
i = 3,j = 1,j<money[3] ,cnt[3][1] = cnt[2][1] =2,j = 2,j = money[3],cnt[3][2] = cnt[2][2] + 1 = 2, j = 3,j>money[3],cnt[3][3] = cnt[2][1] + cnt[2][3] = 2 + 0 = 2, j = 4,j>money[3],cnt[3][4] = cnt[2][2] + cnt[2][4] = 1 + 0 = 1。
i = 4,j = 1,j<money[4] ,cnt[4][1] = cnt[3][1] =2,j = 2,j = money[4],cnt[4][2] = cnt[3][2] + 1 = 3, j = 3,j>money[4],cnt[4][3] = cnt[3][1] + cnt[3][3] = 2 + 2 = 4, j = 4,j>money[4],cnt[4][4] = cnt[3][2] + cnt[3][4] = 2 + 1 = 3。
最后我们的答案也出来了,就是cnt[4][4] = 3。
dfs:
对于深搜来说,思路也是差不多的,钱数小于就没法买,大于等于的话就可以选择买或者不买,深搜就是将所有情况都搜索一般,那么在大于等于的时候我们买和不买都需要遍历。函数的参数有两个,一个是搜索到第几个菜品,另一个是手里还剩的钱数。在结束条件有两种,一种是刚好钱花完了,即m = 0,此时我们返回1,表示这是一种方案;另一种是已经到了最后一个菜品了,但是钱还没花完,即x = n,此时我们返回0,表示这种选择不符合。
同时我们还要再加上一个“记忆”的二维数组cnt[i][j],当为第i个菜品,还剩j块钱时,方案数为cnt[i][j]。定义一个int类的ans,记录这层递归的总方案数,在返回时,将ans记录到cnt中。所以在两个结束条件之前我们还要再加上一个if语句,判断这个cnt是否已经记录,如果已经记录,那么直接返回记录的数据即可。
递归的话,会比较绕,大家可以在草稿纸上进行模拟,从(0,m)开始,如果买得起就分为两支,一支是要买(1,m - money),一支是不买(1,m),买不起就只有一支:不买(1,m)。然后依次类推,逻辑应该也是很清晰的。
接下来就是代码环节啦!
AC代码
动态规划
#include<iostream>
#include<vector>
using namespace std;
int main()
{
int n, m;
cin >> n >> m;
vector<int> money(n+1);
for (int i = 1; i <= n; i++) {
cin >> money[i];
}
vector< vector<int> > cnt(n+1, vector<int>(m+1,0));//注意vector二维数组的初始化和创建
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (j == money[i]) {
cnt[i][j] = cnt[i - 1][j] + 1;
}
else if (j < money[i]) {
cnt[i][j] = cnt[i - 1][j];
}
else if (j > money[i]) {
cnt[i][j] = cnt[i - 1][j] + cnt[i - 1][j - money[i]];
}
}
}
cout << cnt[n][m];
return 0;
}
dfs
#include<iostream>
using namespace std;
const int N = 110;
int money[N];
int n, m;
int cnt[N][10010];
//x:第几个位置
//m:钱数上限
int dfs(int x, int M) {
if (cnt[x][M]) return cnt[x][M];
int ans = 0;
if (M == 0) return 1;//如果钱花完了,就直接返回1
if (x == n) return 0;//如果到了第n个,钱还没花完就返回0
if (money[x + 1] <= M) {
ans += dfs(x + 1, M - money[x + 1]);//选择当前位置的菜
}
ans += dfs(x + 1, M); //不选择当前位置的菜
cnt[x][M] = ans;
return ans;
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
cin >> money[i];
}
cout << dfs(0, m) << endl;
return 0;
}