题目大意:
某国家发行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 !!
知道了这个以后,就可以知道回溯的大概思路了, 但是还不够, 怎样取得给定的几个面值能够达到的最大连续面值呢?
最直观容易想到的就是直接递归回溯枚举所有情况, 便可知道最大连续值了。
下面是我写的递归函数:
-
- void dfs(int cur, int n, int sum){
- if(cur >= h){
- occur[sum] = true;
- return;
- }
- occur[sum] = true;
- for(int i=0; i<=n; ++i){
- dfs(cur+1, n, sum+stampVal[i]);
- }
- }
occur是一个全局数组, 调用递归时先初始化为0。 然后用它来记录出现过的面值之和。
最后只需要从occur数组的下标1开始枚举,直到不是true值时就是能达到的最大连续面值。
由于这道题的数据量很小,所以这样做完全不会超时,速度也不错:
165 - Stamps |
Accepted |
C++ |
0.068 |
下面是用这种方法的代码:
- #include<cstring>
- #include<cstdio>
- #include<cstdlib>
- #include<iostream>
- #define MAXN 200
- using namespace std;
- int h, k;
- int ans[MAXN], maxStampVal, stampVal[MAXN], maxVal[MAXN];
- bool occur[MAXN];
-
-
-
-
-
- void dfs(int cur, int n, int sum){
- if(cur >= h){
- occur[sum] = true;
- return;
- }
- occur[sum] = true;
- for(int i=0; i<=n; ++i){
- dfs(cur+1, n, sum+stampVal[i]);
- }
- }
-
- void search(int cur){
- if(cur >= k){
- if(maxVal[cur-1] > maxStampVal){
- maxStampVal = maxVal[cur-1];
- memcpy(ans, stampVal, sizeof(stampVal));
- }
- return ;
- }
- for(int i=stampVal[cur-1]+1; i<=maxVal[cur-1]+1; ++i){
-
- memset(occur, 0, sizeof(occur));
- stampVal[cur] = i;
- dfs(0, cur, 0);
- int num=0, j=1;
-
- while(occur[j++]) ++num;
- maxVal[cur] = num;
-
- search(cur+1);
- }
- }
-
- int main(){
- #ifdef LOCAL
- freopen("input.txt", "r", stdin);
- #endif
-
- while(scanf("%d %d", &h, &k), h+k){
- stampVal[0] = 1;
- maxVal[0] = h;
- maxStampVal = -2147483645;
- search(1);
-
- for(int i=0; i<k; ++i)
- printf("%3d", ans[i]);
- printf(" ->%3d\n", maxStampVal);
- }
- return 0;
- }
计算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 |
速度有明显的提高。
下面是按照这种方法做的代码:
- #include<cstring>
- #include<cstdio>
- #include<cstdlib>
- #include<iostream>
- #define INF 2147483647
- #define MAXN 2000
- using namespace std;
- int h, k;
- int ans[MAXN], maxStampVal, stampVal[MAXN], maxVal[MAXN], y[MAXN];
- bool occur[MAXN];
-
-
- void search(int cur){
- if(cur >= k){
- if(maxVal[cur-1] > maxStampVal){
- maxStampVal = maxVal[cur-1];
- memcpy(ans, stampVal, sizeof(stampVal));
- }
- return ;
- }
- int tmp[MAXN];
- memcpy(tmp, y, sizeof(y));
- for(int i=stampVal[cur-1]+1; i<=maxVal[cur-1]+1; ++i){
-
- stampVal[cur] = i;
-
- for(int j=0; j<stampVal[cur-1]*h; ++j)if(y[j]<h){
- for(int num=1; num<=h-y[j]; ++num){
- if(y[j]+num < y[j + i*num] && (j+i*num<MAXN))
- y[j+i*num] = y[j]+num;
- }
- }
- int r = maxVal[cur-1];
- while(y[r+1]<INF) r++;
- maxVal[cur] = r;
-
- search(cur+1);
-
- memcpy(y, tmp, sizeof(tmp));
- }
- }
-
- int main(){
- #ifdef LOCAL
- freopen("input.txt", "r", stdin);
- #endif
-
- while(scanf("%d %d", &h, &k), h+k){
- stampVal[0] = 1;
- maxVal[0] = h;
- int i;
- for(i=0; i<=h; ++i)
- y[i] = i;
- while(i<MAXN) y[i++] = INF;
- maxStampVal = -2147483645;
- search(1);
-
- for(i=0; i<k; ++i)
- printf("%3d", ans[i]);
- printf(" ->%3d\n", maxStampVal);
- }
- return 0;
- }