连续邮资问题


题目大意:

某国家发行k种不同面值的邮票,并且规定每张信封上最多只能贴h张邮票。 公式n(h,k)表示用从k中面值的邮票中选择h张邮票,可以组成面额为连续的1,2,3,……n, n是能达到的最大面值之和。例如当h=3,k=2时, 假设两种面值取值为1,4, 那么它们能组成连续的1……6,  虽然也可以组成8,9,12,但是它们是不连续的了。


分析与总结:

连续邮资问题,这个算是很经典的一个问题了。王晓东的《计算机算法设计与分析第三版》 p173有介绍, 但是讲的不够详细, 这里转载了一篇博客对王晓东讲的有更深入详细分析:

连续邮资问题 http://blog.csdn.net/shuangde800/article/details/7755254


以下是按照我自己的话来总结:


首先开一个数组stampVal【0...i】来保存各个面值,再开一个maxVal[0...i]来保存当前所有面值能组成的最大连续面值。

那么,我们可以确定stampVal[0] 一定是等于1的。因为如果没有1的话,很多数字都不能凑成。

然后相应的,maxVal[0] = 1*h.   h为允许贴邮票的数量

接下去就是确定第二个,第三个......第k个邮票的面值了,这个该怎么确定呢?

对于stampVal[i+1],  它的取值范围是stampVal[i]+1 ~maxVal[i]+1. 

stampVal[i]+1好理解, 这一次取的面值肯定要比上一次的面值大,  而这次取的面值的上限是上次能达到的最大连续面值+1, 是因为如果比这个更大的话, 那么

就会出现断层, 即无法组成上次最大面值+1这个数了。 举个例子, 假设可以贴3张邮票,有3种面值,前面2种面值已经确定为1,2, 能达到的最大连续面值为6, 那么接下去第3种面值的取值范围为3~7。如果取得比7更大的话会怎样呢? 动手算下就知道了,假设取8的话, 那么面值为1,2,8,将无法组合出7 !!


知道了这个以后,就可以知道回溯的大概思路了, 但是还不够, 怎样取得给定的几个面值能够达到的最大连续面值呢?

最直观容易想到的就是直接递归回溯枚举所有情况, 便可知道最大连续值了。

下面是我写的递归函数:

[cpp] view plain copy
  1. // cur当前用了几张票, n面额数, sum面值之和  
  2. void dfs(int cur, int n, int sum){  
  3.     if(cur >= h){   
  4.         occur[sum] = true;      
  5.         return;  
  6.     }  
  7.     occur[sum] = true;  
  8.     for(int i=0; i<=n; ++i){  
  9.         dfs(cur+1, n, sum+stampVal[i]);  
  10.     }  
  11. }  

occur是一个全局数组, 调用递归时先初始化为0。 然后用它来记录出现过的面值之和。

最后只需要从occur数组的下标1开始枚举,直到不是true值时就是能达到的最大连续面值。



由于这道题的数据量很小,所以这样做完全不会超时,速度也不错:

 165 - Stamps  Accepted C++ 0.068

 下面是用这种方法的代码:

[cpp] view plain copy
  1. #include<cstring>   
  2. #include<cstdio>  
  3. #include<cstdlib>  
  4. #include<iostream>  
  5. #define MAXN 200  
  6. using namespace std;  
  7. int h, k;  
  8. int ans[MAXN], maxStampVal, stampVal[MAXN], maxVal[MAXN];  
  9. bool occur[MAXN];  
  10.   
  11.   
  12.   
  13. // 计算给定面额和数量能够达到的最大连续面值  
  14. // cur当前用了几张票, n当前面额数, sum面额之和  
  15. void dfs(int cur, int n, int sum){  
  16.     if(cur >= h){   
  17.         occur[sum] = true;      
  18.         return;  
  19.     }  
  20.     occur[sum] = true;  
  21.     for(int i=0; i<=n; ++i){  
  22.         dfs(cur+1, n, sum+stampVal[i]);  
  23.     }  
  24. }  
  25.   
  26. void search(int cur){  
  27.     if(cur >= k){  
  28.         if(maxVal[cur-1] > maxStampVal){  
  29.             maxStampVal = maxVal[cur-1];  
  30.             memcpy(ans, stampVal, sizeof(stampVal));  
  31.         }  
  32.         return ;  
  33.     }  
  34.     for(int i=stampVal[cur-1]+1; i<=maxVal[cur-1]+1; ++i){  
  35.   
  36.         memset(occur, 0, sizeof(occur));  
  37.         stampVal[cur] = i;  
  38.         dfs(0, cur, 0);  
  39.         int num=0, j=1;  
  40.      
  41.         while(occur[j++]) ++num;  
  42.         maxVal[cur] = num;  
  43.   
  44.         search(cur+1);  
  45.     }  
  46. }  
  47.   
  48. int main(){  
  49. #ifdef LOCAL  
  50.     freopen("input.txt""r", stdin);  
  51. #endif  
  52.     // h数量, k面额数  
  53.     while(scanf("%d %d", &h, &k), h+k){  
  54.         stampVal[0] = 1;  
  55.         maxVal[0] = h;  
  56.         maxStampVal = -2147483645;  
  57.         search(1);  
  58.   
  59.         for(int i=0; i<k; ++i)  
  60.             printf("%3d", ans[i]);  
  61.         printf(" ->%3d\n", maxStampVal);  
  62.     }  
  63.     return 0;  
  64. }  


计算X[1:i]的最大连续邮资区间在本算法中被频繁使用到,如果数据量大的话,很可能就会超时了。

《计算机算法设计与分析第三版》给了一个更高效的方法:

考虑到直接递归的求解复杂度太高,我们不妨尝试计算用不超过m张面值为x[1:i]的邮票贴出邮资k所需的最少邮票数y[k]。通过y[k]可以很快推出r的值。事实上,y[k]可以通过递推在O(n)时间内解决:

for (int j=0; j<= x[i-2]*(m-1);j++)
        if (y[j]<m)
          for (int k=1;k<=m-y[j];k++)
            if (y[j]+k<y[j+x[i-1]*k]) y[j+x[i-1]*k]=y[j]+k;
      while (y[r]<maxint) r++;

转载的那篇博文对这个有更深入的分析。


 165 - Stamps  Accepted C++ 0.016
 速度有明显的提高。


下面是按照这种方法做的代码:

[cpp] view plain copy
  1. #include<cstring>   
  2. #include<cstdio>  
  3. #include<cstdlib>  
  4. #include<iostream>  
  5. #define INF 2147483647  
  6. #define MAXN 2000  
  7. using namespace std;  
  8. int h, k;  
  9. int ans[MAXN], maxStampVal, stampVal[MAXN], maxVal[MAXN], y[MAXN];  
  10. bool occur[MAXN];  
  11.   
  12.   
  13. void search(int cur){  
  14.     if(cur >= k){  
  15.         if(maxVal[cur-1] > maxStampVal){  
  16.             maxStampVal = maxVal[cur-1];  
  17.             memcpy(ans, stampVal, sizeof(stampVal));  
  18.         }  
  19.         return ;  
  20.     }  
  21.     int tmp[MAXN];   
  22.     memcpy(tmp, y, sizeof(y));  
  23.     for(int i=stampVal[cur-1]+1; i<=maxVal[cur-1]+1; ++i){  
  24.   
  25.         stampVal[cur] = i;  
  26.         // 关键步骤,利用了动态规划的思想  
  27.         for(int j=0; j<stampVal[cur-1]*h; ++j)if(y[j]<h){  
  28.             for(int num=1; num<=h-y[j]; ++num){  
  29.                 if(y[j]+num < y[j + i*num] && (j+i*num<MAXN))  
  30.                     y[j+i*num] = y[j]+num;  
  31.             }  
  32.         }  
  33.         int r = maxVal[cur-1];  
  34.         while(y[r+1]<INF) r++;  
  35.         maxVal[cur] = r;  
  36.   
  37.         search(cur+1);  
  38.   
  39.         memcpy(y, tmp, sizeof(tmp));  
  40.     }  
  41. }  
  42.   
  43. int main(){  
  44. #ifdef LOCAL  
  45.     freopen("input.txt""r", stdin);  
  46. #endif  
  47.     // h数量, k面额数  
  48.     while(scanf("%d %d", &h, &k), h+k){  
  49.         stampVal[0] = 1;  
  50.         maxVal[0] = h;  
  51.         int i;  
  52.         for(i=0; i<=h; ++i)  
  53.             y[i] = i;  
  54.         while(i<MAXN) y[i++] = INF;        
  55.         maxStampVal = -2147483645;  
  56.         search(1);  
  57.   
  58.         for(i=0; i<k; ++i)  
  59.             printf("%3d", ans[i]);  
  60.         printf(" ->%3d\n", maxStampVal);  
  61.     }  
  62.     return 0;  

你可能感兴趣的:(连续邮资问题)