连续邮资问题

 

王晓东老师编著的《计算机算法设计与分析》 5.12 节以“连续邮资问题”为例展示了回溯法的应用。讲解比较简略,对于搜索出一张新的邮票面值后如何更新最大连续邮资区间这一点没有过多的说明。以下是自己对于这一节学习的一点笔记。
实际上,关于刚才所说的更新最大连续邮资区间的方法,可以归结到一种“等价类”的思想。与此相似的还有《编程之美》中“数组分割问题”的解法三,《编程之美》中“找符合条件的整数”的最后一种算法, JOJ 1903 JOJ 1278 这些题目等等。以下先从头到尾把连续邮资问题复习一遍,然后小结一下这种“等价类”的方法。
连续邮资问题:某国家发行了 n 种不同面值的邮票,并且规定每张信封上最多只允许贴 m 张邮票。连续邮资问题要求对于给定的 n m 的值,给出邮票面值的最佳设计,在 1 张信封上贴出从邮资 1 开始,增量为 1 的最大连续邮资区间。例如,当 n=5 m=4 时,面值为 {1,3,11,15,32} 5 种邮票可以贴出的最大连续邮资区间是 1 70.
当然是用回溯法。搜索结点的状态应该是已经确定的邮票面值 ( 各不相同并且总数不超过 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 没有办法表示。我们现在专注第二个问题。
第二个问题自己有两种思路:,计算出所有使用不超过 m x[1…i+1] 中的面值能够贴出的邮资,然后从 r+1 开始逐个检查是否被计算出来。二,从 r+1 开始,逐个询问它是不是可以用不超过 m x[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) 中的每个元素就是一种合法的贴法,对应一个邮资。当前最大连续邮资区间为 1 r ,那么 S(i) 中每个元素的邮资是不是也在 1 r 之间呢?不一定,比如 {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) 的影响。假设 s S(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;
}
用算法教材上的蒙特卡罗方法估算该算法解空间树的结点数,得到以下结果:
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,算法,扩展)