当前位置: 首页 > news >正文

动态规划DP:从硬币问题到挤牛奶问题的算法实战

1.一般性的最少硬币组成问题【动态规划 DP专题训练】

题目描述

从n种币值为a[1..n]的硬币中,任选几个硬币组成价值为V的一堆货币,问最少需要几个硬币?其中每种硬币的数量没有限制。1<=n<=100,1<=v<=100000,1<=a[i]<=100000

输入格式

输入中有两行:第一行有两个数v和n;第二行有n个以空格分隔的数,表示n个币值.

输出格式

输出只有一行,该行只有一个数,表示所需的最少硬币数, 如果无论如何选取硬币,均不能得到币值v,则输出0.

输入样例 
10 2 
3 5
输出样例 
2
#include<bits/stdc++.h>
using namespace std;
#define MAX 100010 
int k,n;
int dp[MAX],a[MAX];
int mi=MAX;
int main()
{memset(dp,0,sizeof(dp));dp[0]=0;scanf("%d %d",&k,&n);for(int i=1;i<=n;i++)scanf("%d",&a[i]);for(int i=1;i<=n;i++){for(int j=a[i];j<=k;j++){if(dp[j]==0||dp[j]>dp[j-a[i]]+1){if(dp[j-a[i]]!=0||j-a[i]==0){dp[j]=dp[j-a[i]]+1;}}}}if(dp[k]==0){cout<<"0";return 0;}printf("%d",dp[k]);return 0;
}

一道简单的入门题

1. 代码整体功能

  • 问题描述:这是一个经典的优化问题。假设有$n$种硬币,每种硬币面值为$a_i$($i$从1到$n$),硬币数量无限。需要计算组成目标金额$k$所需的最小硬币数。如果无法组成,则输出0。
  • 算法核心:使用动态规划,通过构建一个dp数组来存储子问题的解。dp[j]表示组成金额$j$所需的最小硬币数量。
  • 输出结果:代码输出dp[k],即组成$k$的最小硬币数;如果$k$不可达,输出0。

2. 代码结构和关键变量

  • 头文件和常量
    • #include<bits/stdc++.h>:包含所有标准库头文件,简化代码。
    • using namespace std;:使用标准命名空间,避免前缀std::
    • #define MAX 100010:定义常量MAX为100010,表示数组最大大小(足够大以处理金额$k$)。
  • 变量定义
    • int k, n;k是目标金额,n是硬币种类数。
    • int dp[MAX];:动态规划数组,dp[j]存储金额$j$的最小硬币数。
    • int a[MAX];:硬币面值数组,a[i]表示第$i$种硬币的面值($i$从1到$n$)。
    • int mi = MAX;:初始化为MAX,但在代码中未使用,可能是冗余变量(不影响功能)。
  • 输入输出:使用scanf读取输入,coutprintf输出结果。

3. 动态规划算法逻辑

算法基于状态转移方程。核心思想是:对于每个金额$j$,尝试使用每种硬币$a_i$来更新dp[j],取最小值。状态转移方程可表示为: $$dp[j] = \min(dp[j], dp[j - a_i] + 1) \quad \text{for all} \quad i \in [1, n]$$ 但代码中通过条件判断实现此最小化过程。以下是分步解释:

(1) 初始化
  • memset(dp, 0, sizeof(dp));:将dp数组所有元素初始化为0(表示初始时所有金额均不可达)。
  • dp[0] = 0;:基础情况,金额0不需要任何硬币。
  • 输入kn,然后读取硬币面值到数组a
(2) 状态转移(双重循环)
  • 外层循环for(int i=1; i<=n; i++),遍历每种硬币(索引$i$从1到$n$)。
  • 内层循环for(int j=a[i]; j<=k; j++),遍历金额$j$(从当前硬币面值$a_i$开始到目标$k$)。
    • 在循环内部,检查是否需要更新dp[j]
      • 条件1:if(dp[j]==0 || dp[j]>dp[j-a[i]]+1):如果dp[j]未被设置(0)或当前方案(使用硬币$a_i$)的硬币数更小。
      • 条件2:if(dp[j-a[i]]!=0 || j-a[i]==0):确保子问题$j - a_i$可达(dp[j-a[i]]不为0)或子问题为金额0(基础情况)。
    • 如果两个条件都满足,则更新dp[j] = dp[j-a[i]] + 1:表示使用一个$a_i$硬币,加上组成$j - a_i$的最小硬币数。
    • 逻辑含义:这相当于在状态转移中取最小值,但避免了直接使用$\min$函数,而是通过条件判断实现。
(3) 结果输出
  • if(dp[k]==0):检查目标金额$k$是否可达(dp[k]为0表示不可达)。
    • 如果不可达,输出"0"
    • 否则,输出dp[k],即最小硬币数。

4. 算法实现细节

  • 时间复杂度:$O(n \times k)$。因为双重循环:外层$n$次,内层最多$k$次(平均情况)。对于$k \leq 10^5$,代码在合理时间内运行。
  • 空间复杂度:$O(k)$。主要空间由dp数组占用,大小为MAX
  • 正确性保证
    • 动态规划从金额0开始逐步构建解。
    • 状态转移确保每个dp[j]都是最优解(通过比较所有可能硬币选择)。
  • 边界处理
    • 金额0直接设为0。
    • 如果$k$不可达,输出0(符合问题要求)。

总结

这段代码高效地解决了硬币找零问题的最小硬币数计算,使用动态规划方法。核心在于状态转移的双重循环,确保每个金额的最优解

2.

    挤牛奶【动态规划 DP专题训练】

题目描述

小卡卡终于帮农夫John找到了走丢的那一头奶牛,John为了感谢小卡卡,不仅告诉了他在 Pascal山脉上可能存在Pascal圣地最大的宝藏,还说要请小卡卡喝牛奶。可是农夫John发现他家里所储藏的牛奶已经喝光了,所以要临时给奶牛挤奶。小卡卡实在是太好心了,说要帮农夫John一起挤牛奶。John答应了,他把他的n头奶牛中的n/2头(n是个偶数)分给小卡卡挤,他自己挤剩下的n/2头奶牛。但是每一头奶牛都有不同的产奶量,农夫John为了让小卡卡减轻负担,他希望他们两个所挤的牛奶的总量之差最小。小卡卡得知了每头奶牛的产奶情况之后很快就解决了这个问题。

输入格式

测试数据第一行一个整数n,n为偶数且小于100。表示有n头奶牛。第二行n个整数分别给出每头奶牛的产奶量(产奶量的总和不超过2000)。

输出格式

输出小卡卡和农夫所挤的牛奶量的最小差值。

输入样例
6
7 9 2 6 4 2
输出样例
0
#include<bits/stdc++.h>
using namespace std;
#define MAX 2010 
int n;
int dp[MAX][MAX];
int a[MAX],v[MAX];
long long sum;
int main()
{memset(dp,0,sizeof(dp));dp[0][0]=1;scanf("%d",&n);for(int i=1;i<=n;i++){scanf("%d",&a[i]);sum+=a[i];}   for(int i=1;i<=n;i++){for(int k=n;k>=0;k--){for(int j=sum-a[i];j>=0;j--){if(dp[k][j])dp[k+1][j+a[i]]=1;}}   } for(int i=sum/2;i>=0;i--){if(dp[n/2][i]){printf("%d",sum-i-i);return 0;}}return 0;
}

1. 问题定义与目标

  • 输入:$n$个整数,存储在数组a中。
  • 目标:将数组划分成两个子集$A$和$B$,每个子集大小恰好为$n/2$(假设$n$为偶数),最小化两个子集和的差的绝对值,即最小化$| \text{sum}(A) - \text{sum}(B) |$。
  • 由于总和$\text{sum}$固定,如果子集$A$的和为$i$,则子集$B$的和为$\text{sum} - i$,差的绝对值为$|\text{sum} - 2i|$。代码的目标是找到$i$,使得$|\text{sum} - 2i|$最小。

2. 代码变量解释

  • n:输入整数的个数。
  • a[MAX]:存储输入整数的数组,索引从1到$n$。
  • sum:所有整数的总和,类型为long long以防大数。
  • dp[MAX][MAX]:二维DP数组,dp[k][j]表示是否可能选取$k$个元素,使得它们的和恰好为$j$。其中:
    • $k$ 表示选取的元素个数(范围 $0$ 到 $n$)。
    • $j$ 表示当前和(范围 $0$ 到 $\text{sum}$)。
    • dp[k][j] = 1 表示状态可达,0 表示不可达。
  • v[MAX]:在代码中未使用,可能是冗余变量。
  • MAX:定义为2010,限制数组大小,假设$n \leq 2000$。

3. 代码逻辑分步讲解

(1) 初始化和输入
memset(dp,0,sizeof(dp));  // 初始化DP数组所有元素为0
dp[0][0] = 1;             // 基础状态:选取0个元素,和为0是可能的
scanf("%d",&n);            // 输入n
for(int i=1;i<=n;i++) {scanf("%d",&a[i]);     // 输入每个整数sum += a[i];           // 累加总和
}
  • dp[0][0] = 1 是动态规划的起点,表示“不选任何元素时和为0”是可行的。
  • 输入循环计算总和$\text{sum}$,为后续DP提供上限。
(2) 动态规划状态转移
for(int i=1; i<=n; i++) {              // 遍历每个元素a[i]for(int k=n; k>=0; k--) {           // 逆序遍历k(选取元素个数),从大到小避免覆盖for(int j=sum - a[i]; j>=0; j--) { // 逆序遍历j(当前和),从大到小if(dp[k][j]) {              // 如果状态(k, j)可达dp[k+1][j + a[i]] = 1;  // 则新状态(k+1, j + a[i])也可达}}}
}

4. 算法复杂度与注意事项

5. 示例说明

假设输入$n=4$,数组$a = [1, 2, 3, 4]$,总和$\text{sum}=10$。

这段代码高效地解决了子集划分问题,适合中等规模输入。如果$n$很大或$\text{sum}$过大,可能需要优化(如位运算压缩状态),但当前实现清晰易读。

  • 外层循环:遍历每个元素$a_i$($i$从1到$n$),表示当前考虑添加的元素。
  • 中层和内层循环:逆序更新$k$和$j$(从大到小),这是背包问题的标准优化,防止同一元素被重复使用。
  • 状态转移
    • 如果dp[k][j] == 1(即选取$k$个元素和$j$是可行的),则选择当前元素$a_i$后,新状态为选取$k+1$个元素,和变为$j + a_i$,所以设置dp[k+1][j + a[i]] = 1
  • 数学表达:状态转移方程为: $$\text{dp}[k+1][j + a_i] = \text{dp}[k+1][j + a_i] \lor \text{dp}[k][j]$$ 其中$\lor$表示逻辑或(这里用赋值实现)。
  • 这个过程填充DP表,记录了所有可能的$(k, j)$组合。
  • (3) 结果提取
  • for(int i = sum/2; i >= 0; i--) {  // 从sum/2向下遍历iif(dp[n/2][i]) {               // 如果存在大小为n/2的子集和为iprintf("%d", sum - i - i); // 输出差的绝对值(即sum - 2*i)return 0;                  // 找到解,立即结束}
    }

  • 循环逻辑:从$i = \text{sum}/2$向下遍历到$0$,目的是找到最大的$i$(接近$\text{sum}/2$),使得dp[n/2][i] == 1(即存在大小为$n/2$的子集和为$i$)。
  • 输出sum - 2*i 表示两个子集和的差$|\text{sum} - 2i|$。由于$i$从大到小遍历,第一个满足条件的$i$会使$|\text{sum} - 2i|$最小(即差最小)。
  • 为什么有效:假设子集$A$和为$i$,子集$B$和为$\text{sum} - i$,则差为$|(\text{sum} - i) - i| = |\text{sum} - 2i|$。遍历从$\text{sum}/2$开始,确保找到的$i$使$|\text{sum} - 2i|$最小化。
  • 时间复杂度:$O(n \times n \times \text{sum})$。外层循环$n$次,中层循环$n$次,内层循环$\text{sum}$次($\text{sum}$是总和)。由于$\text{sum}$可能较大,但代码中MAX=2010限制了$n$和$\text{sum}$的范围。
  • 空间复杂度:$O(n \times \text{sum})$,由DP数组大小决定。
  • 关键假设
    • $n$必须是偶数,否则n/2是整数除法(如$n=5$时n/2=2),可能不是最优划分。代码未处理$n$为奇数的 case。
    • 输入整数非负,否则DP状态转移可能出错(但代码未显式检查)。
  • DP过程会记录所有$(k, j)$状态,例如:
    • 选取2个元素和为5(如子集${1,4}$),则dp[2][5] = 1
  • 结果提取:遍历$i$从$5$($\text{sum}/2=5$)向下,当$i=5$时dp[2][5] == 1,输出10 - 2*5 = 0(即差为0,最优解)。
    • 优化点:逆序更新$k$和$j$避免了重复计算,是经典背包问题技巧。
http://www.dtcms.com/a/423699.html

相关文章:

  • 二十八、API之《System 类》——与系统交互的“桥梁”
  • GitHub 热榜项目 - 日榜(2025-09-29)
  • 3分钟,官方讲讲STM32CubeProgrammer 2.20更新哪些新特性?
  • 想学做网站要去哪里学济南网站制作哪家最好
  • Go基础(⑥Cors)
  • 九龙坡区网站建设南宁网站建设速成培训
  • ⸢ 柒 ⸥ ⤳ 可信纵深防御建设方案:a.基线 基础设施可信
  • 山东营销网站建设设计装修网平台
  • android 增强版 RecyclerView
  • HTML 开发工具有哪些?常用 HTML 开发工具推荐、学习路线与实战经验分享
  • 做申诉资料网站外包app开发价格表
  • ChatGPT被降智怎么办?自查方法+恢复指南
  • Linux系统管理文件锁的工具之flock
  • 用DuckDB官方的步骤生成 1.4版插件的问题和解决
  • 如何快速找到与课题相关的高质量文献?
  • 第三十八天:回文数组
  • 字体排版网站做动漫图片的网站
  • springboot个人博客系统的设计与实现(代码+数据库+LW)
  • 软件工程实验三-原型设计
  • Android开发-存储框架技术总结
  • 备案审核网站显示500爱做网站免费
  • 深圳做网站哪家公司比较好而且不贵弄一个网站要多少钱
  • 借助SFTTrainer进行微调 (109)
  • BeanFactory
  • Linux CentOS 7 安装 zip-3.0-11.el7.x86_64.rpm 详细步骤(命令行教程)​(附安装包)
  • VLM Prompt优化之 DynaPrompt(ICLR 2025)论文总结
  • 网站开发和网站制作的区别wordpress如何改成cms
  • 网站首页布局河北手机网站制作价格
  • 负载均衡式的在线OJ项目编写(六)
  • 中止 Web 请求新方式 - AbortController API