连续邮资问题

转载自: http://blog.csdn.net/jcwkyl/article/details/4137398   By  jcwKyl


王晓东老师编著的《计算机算法设计与分析》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_ybackup_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;
}
用算法教材上的蒙特卡罗方法估算该算法解空间树的结点数,得到以下结果:

n m 平均结点数
5 4 {1,3,11,15,32}:70 6190
5 5 {1,4,9,31,51}:126 26762
5 6 {1,7,12,43,52}:216 94690
6 3 {1,3,7,9,19,24}:52 12587
6 4 {1,4,9,16,38,49}:108 158364

你可能感兴趣的:(编程,c,算法,扩展)