大家也跟着我一起每次思考完一个小思路,就去自己敲代码,久而久之,你就能独立组合编码了!(我也是小白!亲测该法有效!有不好的地方大家多评论,我一定回复纠正!)
题目:
给你一个n种面值的货币系统,求组成面值为m的货币有多少种方案。
输入格式
第一行,包含两个整数n和m。
接下来n行,每行包含一个整数,表示一种货币的面值。
输出格式
共一行,包含一个整数,表示方案数。
数据范围
n≤15,m≤3000
思路:暴搜!
由于每种物品会有无穷多个,要想凑出来 价值 m, 那么我们需要从n种物品中去选择,既然是选择了,那么就意味着有许多的选法,所以我们要枚举所有的选法,比如我可以选:12,22,36,ncnt;(前后分别表示 第i种物品,选择多少件)。所以说选法很多。我们要想的是,枚举每一种选法,不重不漏(可以重复,但是时间复杂度高!),这里我们想到递归树的方法自然可以枚举所有的选法!
递归:
- 首先我们应该想到的是递归的出口,上面分析中,所选物品的总价值为m时,是一种合法的方案,若所选物品的总价值是大于m的时候,则说明该选法不可执行!
if (sum > m) return 0; //表示凑不出方案
if (sum == m) return 1; //表示凑得出1个方案!
- 思考递归的参数:
1.由递归的出口是:总的价值sum,所以说我们每次递归一个物品的时候,都要记录当前物品的一个价值,所以说参数之一是sum!
2.本题是完全背包问题哦!即意味着一种物品你可以选多次,那么我们如何使得它在递归的代码里,实现一种物品选多次呢?先来思考一种极端情况:如下图的递归树:(假设四种物品!)
如图,完全背包的枚举就需要达到这样的效果,对于一种物品而言,虽然它可以选择多次,但是它具有唯一的索引下标,比如第1种物品的下标就是1啦!所以说我们要想使第 i 种物品选多件,我们必须要记录上一次选的是哪种物品!即下标last,这也是递归的参数!
void dfs (int sum, int last)
{
if (sum > m) return 0; //表示凑不出方案
if (sum == m) return 1; //表示凑得出1个方案!
}
3.接下来就是我们的递归体了!上图所示, 直到某种脑溢血
选法全部都填满了为止,如上图的左下角,此时我们要枚举第二种选法了,既然是递归,必然需要回溯,回溯的时候,就加入第2种物品。同理,还会有第 i 种物品的掺杂,所以说我们需要循环枚举每种物品!必然有一个循环。即当脑溢血(1,1,1,1) 回溯的时候,第四个位置上放 下一个物品,所以需要循环放置!又由于第 i 种物品可能选取多次,所以说我们每次都要从第 i 种物品开始循环 – 》递归!
每次递归都是从last开始递归!实现了第i种物品被选择多次!
i ++ 都是回溯的时候才执行!实现了循环每举每种物品!
注意看代码就明白了!
void dfs (int sum, int last)
{
if (sum > m) return ; //表示凑不出方案
if (sum == m) {
res ++; //记录合法的方案数量!
return ; //表示凑得出1个方案!
}
//第i种物品可能选取多次,所以要从last开始,然后递归!
//每次递归都是从last开始递归!实现了第i种物品被选择多次!
//i ++ 都是回溯的时候才执行!实现了循环每举每种物品!
for (int i=last; i < n; i ++)
dfs (sum + a[i], i);
}
贴完整代码:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20;
int a[N]; //每种货币的面额!
int n, m, res;
void dfs (int sum, int last)
{
if (sum > m) return ; //表示凑不出方案
if (sum == m) {
res ++; //记录合法的方案数量!
return ; //表示凑得出1个方案!
}
//第i种物品可能选取多次,所以要从last开始,然后递归!
//每次递归都是从last开始递归!实现了第i种物品被选择多次!
//i ++ 都是回溯的时候才执行!实现了循环每举每种物品!
for (int i=last; i < n; i ++)
dfs (sum + a[i], i);
}
int main()
{
cin >> n >> m;
for (int i=0; i < n; i ++)
cin >> a[i];
dfs (0, 0);
cout << res << endl;
return 0;
}
DP:
完全背包问题;从前 i 种物品里面选,所选物品总属性恰好为 j 的所有方案数;
而对于一种物品我们选几件的集合划分!每种划分都是一类集合!
如:f[i][j] += f[i-1][j - kv] ;表示第 i 种物品选,选择k件,然后从前 i - 1 种物品里,求满足总属性恰好为 j - kv 的方案数!
二维dp:
注意状态转移:
f[i][j] += f[i-1][j-kv];
该转移方程的本质是:
f[i][j] = f[i][j] + f[i-1][j-kv] =f[i-1][j] + f[i-1][j-k*v]
;
标红部分:因为 f[i][j] = f[i-1][j],即第 i 种物品不选,从前 i - 1种物品里面凑出价值为j的所有方案,f[i][j] 可以继承 f[i-1][j]的情况,所以说本质是不选第 i 种的方案数 + 选第 i 种的方案数!
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20, M = 3e3 + 10;
long long f[N][M];
int n, m;
int main()
{
cin >> n >> m;
f[0][0] = 1; //选 0 种物品,总价值为 0 的方案只要一种(一种都不选!)
for (int i=1; i <= n; i ++) //枚举前i种物品!
{
int v;
cin >> v;
for (int j=0; j <= m; j ++) //记录所凑体积为1,2,3..m时的方案数
{
for (int k=0; k*v <= j; k ++) //j体积下,第i种物品可以选k件!枚举记录不同k的情况
f[i][j] += f[i-1][j-k*v];
}
}
cout << f[n][m] << endl;
return 0;
}
一维dp:
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20, M = 3e3 + 10;
long long f[M];
int n, m;
int main()
{
cin >> n >> m;
f[0] = 1; //选 0 种物品,总价值为 0 的方案只要一种(一种都不选!)
for (int i=1; i <= n; i ++) //枚举前i种物品!
{
int v;
cin >> v;
for (int j=v; j <= m; j ++) //记录所凑体积为1,2,3..m时的方案数
f[j] += f[j-v];
}
cout << f[m] << endl;
return 0;
}