货币系统 【教你一边推思路一边敲代码 + 三种解法】

文章介绍了如何使用递归和动态规划方法解决完全背包问题,即给定面值不同的货币,求组成特定面值的组合数。通过递归树的思想,文章详细解释了递归函数的参数设计和递归过程,并提供了完整的C++代码示例。此外,还给出了两种动态规划的解决方案,包括二维和一维DP数组的状态转移方程。
摘要由CSDN通过智能技术生成

大家也跟着我一起每次思考完一个小思路,就去自己敲代码,久而久之,你就能独立组合编码了!(我也是小白!亲测该法有效!有不好的地方大家多评论,我一定回复纠正!)

题目:

给你一个n种面值的货币系统,求组成面值为m的货币有多少种方案。

输入格式
第一行,包含两个整数n和m。

接下来n行,每行包含一个整数,表示一种货币的面值。

输出格式
共一行,包含一个整数,表示方案数。
数据范围
n≤15,m≤3000

思路:暴搜!

由于每种物品会有无穷多个,要想凑出来 价值 m, 那么我们需要从n种物品中去选择,既然是选择了,那么就意味着有许多的选法,所以我们要枚举所有的选法,比如我可以选:12,22,36,ncnt;(前后分别表示 第i种物品,选择多少件)。所以说选法很多。我们要想的是,枚举每一种选法,不重不漏(可以重复,但是时间复杂度高!),这里我们想到递归树的方法自然可以枚举所有的选法!
递归:

  1. 首先我们应该想到的是递归的出口,上面分析中,所选物品的总价值为m时,是一种合法的方案,若所选物品的总价值是大于m的时候,则说明该选法不可执行!
if (sum > m) return 0;  //表示凑不出方案
    if (sum == m) return 1; //表示凑得出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-k
v] = 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;
}
非线性方程数值解法: 1.二分法:这是一最简单的数值解法,但它收敛速度较慢,只有在函数单调性和连续性的条件下才能使用。 2.牛顿法:利用曲线的切线来逼近方程的根,需要一阶导数存在且连续。 3.割线法:牛顿法的改进版,使用两个点来逼近方程的根。 4.弦截法:割线法的改进版,使用一个点和斜率来逼近方程的根。 5.迭代法:通过不断逼近根来解方程,需要选择合适的迭代函数。 非线性方程组的几数值解法: 1.牛顿法:将非线性方程组转化为一个线性方程组,需要计算雅可比矩阵。 2.拟牛顿法:与牛顿法类似,但是不需要计算雅可比矩阵。 3.高斯-赛德尔迭代法:将方程组的解逐个逼近。 4.雅可比迭代法:将方程组的每个未知数的解逐个逼近。 这里提供一个使用牛顿法解非线性方程组的 Matlab 源代码: ```matlab function [x,iter]=newton(f,df,x0,tol,maxiter) % f:目标函数 % df:目标函数的一阶导数 % x0:初值 % tol:误差容限 % maxiter:最大迭代次数 % x:方程的解 % iter:迭代次数 x=x0; iter=0; while norm(f(x))>tol && iter<maxiter x=x-inv(df(x))*f(x); iter=iter+1; end if iter==maxiter fprintf('迭代次数达到上限,可能无解或解收敛缓慢!\n'); else fprintf('迭代成功,迭代次数:%d\n',iter); end end ``` 使用方法: 1.定义目标函数和一阶导数 例如,解方程组 $\begin{cases}x^2+y^2=1\\x-y=0\end{cases}$,定义目标函数和一阶导数: ```matlab f=@(x)[x(1)^2+x(2)^2-1;x(1)-x(2)]; df=@(x)[2*x(1),2*x(2);1,-1]; ``` 2.设置初值、误差容限和最大迭代次数 例如,设置初值 $x_0=[1;1]$,误差容限 $10^{-6}$,最大迭代次数 $100$: ```matlab x0=[1;1]; tol=1e-6; maxiter=100; ``` 3.调用函数解 ```matlab [x,iter]=newton(f,df,x0,tol,maxiter); ``` 完整代码: ```matlab clear;clc; f=@(x)[x(1)^2+x(2)^2-1;x(1)-x(2)]; df=@(x)[2*x(1),2*x(2);1,-1]; x0=[1;1]; tol=1e-6; maxiter=100; [x,iter]=newton(f,df,x0,tol,maxiter); fprintf('方程的解为:\n'); disp(x); ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值