邮票面值设置(dfs + 动态规划减枝)

1. 问题描述:

给定一个信封,最多只允许粘贴N张邮票,计算在给定K(N+K≤15)种邮票的情况下(假定所有的邮票数量都足够),如何设计邮票的面值,能得到最大值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分。

输入描述:
1行。2个整数,代表N,K。
输出描述:
2行。第一行若干个数字,表示选择的面值,从小到大排序。
第二行,输出“MAX=S”,S表示最大的面值。
示例1
输入
3 2
输出
1 3
MAX=7

链接:https://ac.nowcoder.com/acm/problem/16813
来源:牛客网

2. 思路分析:

① 因为我们需要在所有可能的面值方案中选择最优的方案使得能够得到最大的MAX,所以我们可以使用递归搜索找到最优的方案。其实相当于在N个格子中放入K个数字使得这些数字的若干倍能够构成连续的最大值MAX,所以最容易想到的是搜索每一个位置,每一个位置尝试不同的可能性,对于每一个位置都是同样的操作所以到递归出口的时候计算出当前这些数字的若干倍能够构成的最大值(每一个位置尝试若干个可能性所以需要使用for循环中递归尝试各个可能的数字),如果得到的连续的最大值MAX更大那么更新历史上的最大值。思路其实还是比较容易想出来的,但是因为搜索是很耗时的,如何确定每一个位置中可以尝试的最大范围呢,也就是搜索的上界,这其实也是这道题目的一个难点。在牛客的题解中发现了可以这样处理:声明一个数组rec来记录递归过程中的各个位置的面值,每一次递归的时候当前位置的面值等于上一个位置的面值加1(最小上界),这样可以避免重复性的问题,在递归当前的位置k的时候那么计算当前位置能够尝试的最大面值,也即搜索上界,策略是:通过计算当前的数组rec中前k - 1个位置构成的最大连续值t,那么当前递归的k位置的能够尝试的最大上界为t + 1,其实举一个例子就很好理解了,比如当n = 3, k = 2时,递归到当前的位置k = 2那么计算k - 1,也即k = 1位置中rec数组能够构成1~MAX的最大数字MAX,k = 1的时候计算的rec[1] = 1(面值为1)所以n = 3所以能够构成的1~MAX的最大数字MAX = 3,所以第二个位置的最大范围t为3 + 1 = 4,超过了4比如最大范围是t + 1 = 5那么前面两个位置的面值1,5的倍数是无法构成面值为t的,也即无法构成面值为4的邮票,所以对于当前位置搜索的上界为记录递归过程中的数组中索引1~k-1能够构成最大的MAX(1到上一个位置k - 1数组能够构成的的最大连续值),感觉这种确定当前位置搜索上界的思路还是值得好好学习的。

② 由①可以知道我们需要计算当前的1~k-1位置中数组rec中能够构成的最大连续MAX,这里可以使用动态规划来解决,其中dp[i]表示当前的rec数组能够构成数字i所需要的的最少数目,使用两层循环解决,第一层循环表示当前的1-k-1的位置,这样可以取出当前位置的值,第二层循环表示能够构成的最大面值,对于当前的k - 1构成的最大面值为rec[k - 1] * n,表示全部位置放的都是最大面值,使用两层循环递归找出当前位置i需要的最少数目,当前位置的数字减去当前rec[i]之后的dp值小于n的时候才进行更新。

③ 这道题目中使用动态规划的方式进行减枝的方法还是值得学习学习的,能够确定当前位置的搜索上界。

3. 代码如下:

大佬的代码c++:

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<iostream>
using namespace std;
int n,m;
int a[21];//暂时的储存
int maxx=0,ans[21];//ans和maxx记录最终结果
int dp[51000];//dp数组
int solve(int k){
    memset(dp,63,sizeof(dp));dp[0]=0;
    for(int i=1;i<=k;i++)//前k个数
        for(int j=a[i];j<=a[k]*n;j++)//最多能组成到a[k]*n,表示全部都选最大的数
            if(dp[j-a[i]]<n)//只能继承的<n
                dp[j]=min(dp[j],dp[j-a[i]]+1);//当然是求最小值,以后才可以用
    int x=0;
    while(dp[x+1]<=100)x++;//得到最长的连续前缀
    return x;
}
void dfs(int k){
    if(k==m+1){//如果找到m个
        int t=solve(k-1);
        if(t>maxx){
            maxx=t;
            memcpy(ans,a,sizeof(ans));
        }
        return;
    }
    int end=solve(k-1);
    for(int j=a[k-1]+1;j<=end+1;j++){//往下搜索
        a[k]=j;dfs(k+1);a[k]=0;
    }
}
int main(){
    cin>>n>>m;//把n,k改变为n,m
    a[1]=1;//剪枝2
    dfs(2);//从第2个开始问
    for(int i=1;i<=m;i++)printf("%d ",ans[i]);//输出
    printf("\nMAX=%d\n",maxx);
    return 0;
}

我自己改写的python代码(超时,得了75分):

from typing import List

maxx = 0
res = list()


def solve(n: int, k: int, rec: List[int]):
    # dp[i]的含义为列表rec构成数字i所需要的最少的数字个数
    # 注意dp列表的值需要初始化为1000的长度这样才不会发生越界的情况, 提交上去得了75分
    dp = [100] * 1000
    dp[0] = 0
    for i in range(1, k + 1):
        for j in range(rec[i], rec[k] * n + 1):
            # 注意取的是应该是最小的那个
            if dp[j - rec[i]] < n:
                dp[j] = min(dp[j], dp[j - rec[i]] + 1)
    x = 0
    # 计算最大的连续
    while dp[x + 1] < 100:
        x += 1
    return x


def dfs(k: int, n: int, m: int, rec: List[int]):
    global maxx, res
    # 递归出口
    if k == m + 1:
        t = solve(n, k - 1, rec)
        if t > maxx:
            maxx = t
            res = rec[:]
        return
    end = solve(n, k - 1, rec)
    # 对于当前的位置需要尝试若干个可能的值所以需要for循环尝试各个可能的值,这里的最大上界为1~k-1也即上一个位置的rec数组能够构成的最大连续MAX,最小上界为上一个位置的面值加1
    for j in range(rec[k - 1] + 1, end + 2):
        rec[k] = j
        dfs(k + 1, n, m, rec)
        rec[k] = 0


if __name__ == '__main__':
    # dfs + dp
    n, m = map(int, input().split())
    rec = [0] * 21
    # 1这个面值是一定要的
    rec[1] = 1
    dfs(2, n, m, rec)
    for i in range(1, m + 1):
        print(res[i], end=" ")
    print("\nMAX={}".format(maxx))

 

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值