目录
题目详情:
问题描述:
给定一个信封,最多只允许粘贴N张邮票,计算在给定K(N+K≤13)种邮票的情况下(假定所有的邮票数量都足够),如何设计邮票的面值,能得到最大值MAX,使在1~MAX之间的每一个邮资值都能得到。
例如,N=3,K=2,如果面值分别为1分、4分,则在1分~6分之间的每一个邮资值都能得到(当然还有8分、9分和12分);如果面值分别为1分、3分,则在1分~7分之间的每一个邮资值都能得到。可以验证当N=3,K=2时,7分就是可以得到的连续的邮资最大值,所以MAX=7,面值分别为1分、3分。
输入说明:
一行,两个数N、K
输出说明:
两行,第一行升序输出设计的邮票面值,第二行输出“MAX=xx”(不含引号),其中xx为所求的能得到的连续邮资最大值。
测试用例:
输入:
3 2
输出:
1 3
MAX=7
题解:
问题分析:
这道题用到DFS+DP 并且DP是完全背包的变形 很巧妙 有助于加强对DFS和完全背包DP的理解
DFS分析:
由于邮票的种类未知 可以用DFS来深搜邮票的面额组合。搜索时,第一张只能从1开始,不然凑不出1这个面额,不满足题意。第二张时,就有很多可能,但是选用的邮票必须比上一张大,所以最少大1,就是2,最大应该是上一张及之前能凑出的最大面额+1,再大就没有意义了,因为如果选用最大面额+2,上一张及之前能凑出的最大面额假设为num,这一张选用num+2,但是num+1肯定凑不出,所以选用更大的就没有意义了。所以上界最大为num+1。
所以DFS函数应该有两个参数,一个是已经选了几张邮票的数量 一个是前几张邮票能凑出的最大面额。每层要用数组记录现在已经选的邮票种类。
DP分析:
为什么要使用DP呢,因为深度搜索的每一层,都要计算此时能凑出的最大面额。在每一层中,没有区别的是可以选用邮票的最大张数都是n,但是能凑出来的面额不一定。类似于限制个数的完全背包问题,我们可以设dp[i][j],i代表前i种邮票,j代表能凑出的面额,然后dp[i][j]代表最少用的邮票张数。也就是完全背包 限制每种物品的个数最多为n的变形。但总使用个数再dp时,不限制。最后只要筛查dp数组种大于n的全部凑不到即可。
我们可以用滚动数组形式,一维数组即可。设DP[i] i为面额 DP[i]为所用张数。类似于完全背包的滚动数组一维形式。
目标确定:
在每一次DP时,最后只要顺序遍历DP[i],若第一次遇到dp[i]>n,那么就说明凑不出来i这个面额,最多只能凑出i-1这个面额。所以dp我们需要的关键是下标,dp[i]中的值只是为了判断用了几张。
DP数组初始化:
每次初始化dp数组为INF 默认凑不出来就初始化为最大值。要初始化的下限应该是1,上限应该是,此时有可能凑出的最大面额,应该是n*DFS存储的邮票种类中的最后一张的面额,也就是都用最后一张的情况。为什么是最后一张,因为根据DFS的逻辑,每次DFS选的邮票种类的面额都会大于上层的,所以此时的最后一张邮票应该面额最大。
状态转移方程:
用一个两重循环,外层循环遍历此时每一种邮票,假设有x种。内层循环遍历邮票面额,从小遍历到大。也就是用这种邮票的面额从小到大去更新dp数组。因为一张邮票最多能用n次,假设这种邮票的面额为s[i] 则内层遍历的面额应该是从s[i]到n*s[i] 。
for(int i=1;i<=x;++i){//用每一种邮票
for(int j=s[i];j<=n*s[x];++j){//遍历每一个金额{
f[j]=min(f[j-s[i]]+1,f[j]);//看用这个邮票会不会更少
}
}
顺序遍历有一个问题,就是一个金额可能会用超过n种邮票组成。但是我们最后会有一个循环顺序遍历DP[]数组,超过n的默认认为凑不到,我们只能用n张。
注意:
为了便于理解,同时写出非滚动数组,也就是常规二维的形式的状态转移。
如果是常规二维的形式,我们可以设dp[i][j],i代表前i种邮票,j代表能凑出的面额,然后dp[i][j]代表最少用的邮票张数,则状态转移方程为 dp[i][j]=min(dp[i-1][j],dp[i][j-s[i]])
由 前i-1种邮票能凑出来j面额的使用的张数 或者
i种邮票,并且已经凑出了j-s[i]面额 再用这张邮票的面额
两种状态的最小值转移而来。
最后只要筛查dp数组种大于n的全部凑不到即可。
题解代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
#define INF 0x3f3f3f3f
const int maxn=1e5+5;
int n,k;
int f[maxn];//f[i]代表凑出i金额所需要的最少邮票数
int ans[20];//是记录最佳邮票组合
int maxs=-INF;//初始化最大面额为最小 方便每次更新
int s[20]={0};//是记录可能的邮票组合
int dp(int x){//return 前x张邮票能凑出的最大面值
for(int i=1;i<=s[x]*n;++i){//初始化dp数组为最大值 为什么上限是这个 因为s[x]是最大的邮票面额 最大为n张都用这个邮票
f[i]=INF;
}
for(int i=1;i<=x;++i){//用每一种邮票
for(int j=s[i];j<=n*s[x];++j){//遍历每一个金额{
f[j]=min(f[j-s[i]]+1,f[j]);//看用这个邮票会不会更少
}
}
for(int i=1;i<=n*s[x];++i){
if(f[i]>n)//最多只能用n张 如果比n多了 说明没办法凑到
return i-1;//说明只能凑i-1这么面额
}
return n*s[x];
}
void dfs(int floor,int num){//floor 代表现在用了几种邮票了 num代表此时能凑出来的最大面额
if(floor==k+1){
if(num>maxs){
//如果这次的num更大 更新maxs以及ans数组
maxs=max(num,maxs);
for(int i=1;i<floor;++i){
ans[i]=s[i];//存储每一种邮票大小为答案
}
}
return;
}
for(int i=s[floor-1]+1;i<=num+1;++i){//遍历搜索其他邮票面额 起点为上一张邮票面额+1 最大为前几种邮票能凑出的金额+1 再大就没有意义了 上下限难!
s[floor]=i;
dfs(floor+1,dp(floor));
}
}
int main() {
cin>>n>>k;
dfs(1,0);
for(int i=1;i<=k;++i){
cout<<ans[i];
if(i!=k)
cout<<" ";
}
cout<<endl<<"MAX="<<maxs;
return 0;
}
代码详解:
1. 全局变量与数据结构
f
数组:动态规划的核心,记录每个金额所需的最小邮票数。ans
数组:保存当前最优的邮票面值组合。s
数组:当前DFS递归中尝试的邮票面值组合。
2. 深度优先搜索 dfs(floor, num)
- 邮票面值递增:
i
从prev + 1
开始,确保面值递增(如1,3,4等)。 - 剪枝:新面值不超过前一步的最大连续值+1,避免无效搜索。
3.dfs初始化细节
dfs(1,0)开始 由于s[]初始化为都为0 所以第一个数的选用巧妙地只能选1.
是因为在下面这个选面额的循环中 开始时floor=1 则s[floor-1]=s[0]=0 所以i初始化为1 而num开始为0 所以i最多也只能为1 所以第一个邮票只能选择1
for(int i=s[floor-1]+1;i<=num+1;++i){}