连续邮资问题

本文通过回溯法解决连续邮资问题,详细介绍了如何利用动态规划更新最大连续邮资区间,给出了具体算法实现及代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

此文章来自:http://blog.youkuaiyun.com/jcwkyl/article/details/4137398

王晓东老师编著的《计算机算法设计与分析》5.12节以“连续邮资问题”为例展示了回溯法的应用。讲解比较简略,对于搜索出一张新的邮票面值后如何更新最大连续邮资区间这一点没有过多的说明。以下是自己对于这一节学习的一点笔记。
实际上,关于刚才所说的更新最大连续邮资区间的方法,可以归结到一种“等价类”的思想。与此相似的还有《编程之美》中“数组分割问题”的解法三,《编程之美》中“找符合条件的整数”的最后一种算法,JOJ 1903JOJ 1278这些题目等等。以下先从头到尾把连续邮资问题复习一遍,然后小结一下这种“等价类”的方法。
连续邮资问题:某国家发行了n种不同面值的邮票,并且规定每张信封上最多只允许贴m张邮票。连续邮资问题要求对于给定的nm的值,给出邮票面值的最佳设计,在1张信封上贴出从邮资1开始,增量为1的最大连续邮资区间。例如,当n=5m=4时,面值为{1,3,11,15,32}5种邮票可以贴出的最大连续邮资区间是170.
当然是用回溯法。搜索结点的状态应该是已经确定的邮票面值(各不相同并且总数不超过n)和它们能够贴出的最大连续邮资区间,以此来枚举下一个可能的邮票面值。因此,很自然地,使用原书中的标识符,数组x记录当前已经确定的邮票面值,整数r表示当前使用不超过m张邮票能贴出的最大连续邮资区间。对于第i层的结点,x[1…i]表示当前已经有i面值确定,r表示由x[1…i]能贴出的最大连续区间,现在,要想把第i层的结点往下扩展,有两个问题需要解决:,哪些数有可能成为下一个的邮票面值,即x[i+1]的取值范围是什么;二,对于一个确定的x[i+1],如何更新r的值让它表示x[1…i+1]能表示的最大连续邮资区间。
第一个问题很简单,x[i+1]的取值要和前面i个数各不相同,最小应该是x[i] + 1,最大就是r+1,否则r+1没有办法表示。我们现在专注第二个问题。
第二个问题自己有两种思路:,计算出所有使用不超过mx[1…i+1]中的面值能够贴出的邮资,然后从r+1开始逐个检查是否被计算出来。二,从r+1开始,逐个询问它是不是可以用不超过mx[1…i+1]中的面值贴出来。
两种思路直接计算其计算量都是巨大的,需要借助动态规划的方法。模仿0-1背包问题,假设S(i)表示x[1…i]中不超过m张邮票的贴法的集合,这个集合中的元素数目是巨大的,例如,只使用1张邮票的贴法有C(i+1-1,1)C(i,1)=i种,使用2张邮票的贴法有C(i+2-1,2)=C(i+1,2)=i*(i+1)/2种,……,使用m张邮票的贴法有C(i+m-1, m)种,其中C(n,r)表示n元素中取r元素的组合数。于是,S(i)中的元素的数目总共有C(i+1-1, 1) + C(i+2-1,2)+ … + C(i+m-1,m)S(i)中的每个元素就是一种合法的贴法,对应一个邮资。当前最大连续邮资区间为1r,那么S(i)中每个元素的邮资是不是也在1r之间呢?不一定,比如{1,2,4},当m=2时,它能贴出来8,但不能贴出来7,这一点自己在写代码时犯了错误。总之,在搜索时,一定要保持状态的一致性,即当深度搜索到第i层时,一定要确保用来保存结点状态的变量中保存的一定是第i层的这个结点的状态。言归正传,定义S(i)中元素的值就是它所表示的贴法贴出来的邮资,于是,可以把S(i)中的元素按照它们的值的相等关系分成k类。第j类表示贴出邮资为j的所有的贴法集合,用T(j)表示,T(j)有可能是空集,例如对于{1,2,4},T(7)为空集,T(8)={{4,4}}。此时有:S(i) = T(1) U T(2) U T(3) U … U T(k)U表示两个集合的并。
现在考虑x[i+1]加入后对当前状态S(i)的影响。假设sS(i)中的一个元素,即s表示一种合法的贴法x[i+1]s能贴出的邮资的影响就是x[i+1]的多次重复增加了s能贴出的邮资。这样说是因为有两种情况不需要考虑:, 从s中去掉几张邮票,把x[i+1]加进去,这没有意义,因为从s中去掉几张邮票后s就变成了S(i)中的另一个元素t,我们迟早会对t考虑x[i+1]的影响的。二,将x[i+1]加入s,同时再把x[1]也加入s(如果s中还能再贴两张邮票的话),这也没有意义,原因同一。所以,x[i+1]s的影响就是,如果s中贴的邮票不满m张,那就一直贴x[i+1],直到s中有m张邮票,这个过程会产生出很多不同的邮资,它们都应该被加入S(i+1)中。因为s属于S(i),它也必定在某个T(k)中,而T(k)中能产生出最多不同邮资的是T(k)中用的邮票最少的那个元素。至此,原书中的解法就完全出来了:用数组x记录当前已经确定的邮票面值,用r表示当前最大的连续邮资区间,用数组y表示用当前的面值贴出某个邮资所需要的最少的邮票数。状态结点的转换过程已经在上面说的非常清楚了。现在只差写代码了。代码如下:

#include <stdio.h>
 
#define MAX_NM 10
#define MAX_POSTAGE 1024
#define INF 2147483647
 
int n, m;
int x[MAX_NM], ans[MAX_NM], y[MAX_POSTAGE],  maxStamp, r;
 
/*
 * backtrack(i)表示x[0...i-1]这i张邮票已经完全确定,
 * 相应于x[0...i-1]的最大连续邮资区间r和每种邮资所需要的
 * 最少邮票张数y[0...r]也都确定,现在枚举x[i]
 * 的每个值,确定x[i]
 */
void backtrack(int i) {
    int *backup_y, backup_r;
    int next, postage, num, tmp;
 
    if(i >= n) {
        if(r > maxStamp) {
            maxStamp = r;
            for(tmp = 0; tmp < n; tmp++)
                ans[tmp] = x[tmp];
        }
        return;
    }
 
    backup_y = (int*)malloc(MAX_POSTAGE * sizeof(int));
    for(tmp = 0; tmp < MAX_POSTAGE; tmp++)backup_y[tmp] = y[tmp];
    backup_r = r;
 
    for(next = x[i - 1] + 1; next <= r + 1; next++) {
        /* update x[i] */
        x[i] = next;
        /* update y */
        for(postage = 0; postage < x[i-1] * m; postage++) {
            if(y[postage] >= m)continue;
            for(num = 1; num <= m - y[postage]; num++)
                if(y[postage] + num< y[postage + num * next]
                   && (postage + num * next< MAX_POSTAGE))
                    y[postage + num * next] = y[postage] + num;
        }
        /* update r */
        while(y[r + 1] < INF) r++;
 
        backtrack(i + 1);
 
        /* restore */
        r = backup_r;
        for(tmp = 0; tmp < MAX_POSTAGE; tmp++) y[tmp] = backup_y[tmp];
    }
    free(backup_y);
}
 
int main() {
    int i;
 
    scanf("%d%d", &n, &m);
 
    x[0] = 1;
    r = m;
    for(i = 0; i <= r; i++) y[i] = i;
    while(i < MAX_POSTAGE) y[i++] = INF;
    maxStamp= 0;
 
    backtrack(1);
 
    printf("max stamp is: %d/n", maxStamp);
    for(i = 0; i < n; i++) printf("%4d", ans[i]);
 
    return 0;
}



用算法教材上的蒙特卡罗方法估算该算法解空间树的结点数,得到以下结果:

nm平均结点数
54{1,3,11,15,32}:706190
55{1,4,9,31,51}:12626762
56{1,7,12,43,52}:21694690
63{1,3,7,9,19,24}:5212587
64{1,4,9,16,38,49}:108158364

<think>好的,我需要帮助用户理解连续邮资问题的定义和解决方案。首先,用户已经提供了四个引用内容,我需要仔细阅读并提取关键信息。 首先,问题定义:连续邮资问题要求在给定n种邮票和最多贴m张的情况下,设计邮票面值,使得从1开始的连续邮资最大。例如,当n=5,m=4时,面值设计为(1,3,11,15,32)可以达到1到70的连续区间。 接下来,解决方案部分提到了动态规划和回溯算法的结合。引用[2]指出,搜索节点的状态包括已确定的面值和当前的最大连续邮资,然后枚举下一个面值。下一个面值的范围是当前最大面值+1到当前最大连续邮资r+1,否则无法覆盖r+1。这里的关键是r的计算,可能使用递归和回溯,或者用动态规划优化。 引用[3]中的Python代码示例展示了如何通过回溯生成可能的邮票组合,并计算最大连续区间。代码中的calc_Sk函数用来确定下一个面值的候选范围,而回溯函数reback递归地探索每个可能的子节点。 用户需要算法解释和实现,因此我需要将问题拆解为步骤,并解释每个步骤的作用。例如,初始化第一个面值为1,然后逐步确定后续面值的可能范围,使用动态规划来计算当前组合能覆盖的最大邮资,并通过回溯遍历所有可能的面值组合,记录最大的连续区间。 需要注意代码中的细节,比如如何计算每个面值组合的最大连续邮资。可能使用动态规划表来记录最少需要的邮票数,然后检查从1开始的连续值,直到无法覆盖为止。 此外,还需要解释时间复杂度的挑战,因为这是一个组合优化问题,可能需要进行剪枝优化,比如限制下一个面值的范围,避免无效的搜索。 最后,用户可能希望了解如何实现该算法,所以需要提供一个结构清晰的代码示例,并解释关键部分,如动态规划表的构建和回溯过程。</think>### 连续邮资问题算法解释及实现 #### 一、问题定义 连续邮资问题要求在给定$n$种邮票面值、每张信封最多贴$m$张邮票的限制下,找到一种面值组合,使得能贴出从邮资$1$开始的最大连续区间。例如,当$n=5$、$m=4$时,面值为$(1,3,11,15,32)$的组合能覆盖$1$到$70$的连续区间[^2]。 #### 二、算法核心思想 采用**回溯算法+动态规划**的混合策略: 1. **回溯算法**:枚举可能的邮票面值组合,要求面值严格递增。 2. **动态规划**:快速计算当前面值组合能覆盖的最大连续邮资区间,避免重复计算。 #### 三、算法步骤分解 1. **初始化**:第一个面值必须为$1$,否则无法表示邮资$1$。 2. **递推规则**:假设已确定前$i$个面值$(x_1,x_2,...,x_i)$,其最大连续邮资为$r_i$,则下一个面值$x_{i+1}$的取值范围为$[x_i+1, r_i+1]$[^2]。 3. **动态规划表**:用$dp[j]$表示组成邮资$j$所需的最少邮票数。递推公式为: $$dp[j] = \min(dp[j - k \cdot x_{new}] + k) \quad (1 \leq k \leq m)$$ 4. **剪枝优化**:若某面值组合无法扩展更大的连续区间,则回溯。 #### 四、Python实现代码 ```python def max_continuous_postage(n, m): best = {'max_r': 0, 'stamps': []} current = [1] # 初始面值必须为1 def calculate_max_r(stamps): dp = [float('inf')] * (stamps[-1] * m + 2) dp[0] = 0 max_r = 0 for j in range(1, len(dp)): for stamp in stamps: if j >= stamp and dp[j - stamp] + 1 <= m: dp[j] = min(dp[j], dp[j - stamp] + 1) if dp[j] > m: # 无法组成邮资j break max_r = j return max_r def backtrack(step): if step == n: current_r = calculate_max_r(current) if current_r > best['max_r']: best['max_r'] = current_r best['stamps'] = current.copy() return # 确定下一个面值的取值范围 last_r = calculate_max_r(current) min_x = current[-1] + 1 max_x = last_r + 1 for x in range(min_x, max_x + 1): current.append(x) backtrack(step + 1) current.pop() backtrack(1) # 第一个面值已确定为1 return best['stamps'], best['max_r'] # 示例测试 n, m = 5, 4 stamps, max_r = max_continuous_postage(n, m) print(f"面值组合:{stamps},最大连续邮资区间:1-{max_r}") ``` #### 五、关键点说明 1. **动态规划表优化**:通过记录最小邮票使用数,快速判断邮资$j$是否可达。 2. **取值范围限制**:下一面值$x_{i+1}$的上界为当前最大连续邮资$r_i+1$,确保$r_i+1$能被覆盖[^2]。 3. **剪枝策略**:当检测到无法继续扩展连续区间时立即回溯,减少无效搜索。 #### 六、复杂度分析 - 时间复杂度:最坏情况为$O((r_{max})^n)$,实际通过剪枝大幅优化。 - 空间复杂度:主要消耗在递归栈和动态规划表,为$O(n + r_{max})$。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值