假设国家发行了k种不同面值的邮票,并且规定每张信封上最多只允许贴h张邮票。连续邮资问题要求对于给定的k和h的值,给出邮票面值的最佳设计,在1张信封上可贴出从邮资1开始,增量为1的最大连续邮资区间。例如,当k=5和h=4时,面值为(1,3,11,15,32)的5种邮票可以贴出邮资的最大连续邮资区间是1到70。(UVA165)
思路概括:
-
用stampval来保存各个面值,用maxval来保存当前所有面值能组成的最大连续面值。
-
stampval[0] 一定等于1,因为1是最小的正整数。相应的,maxval[0]=1*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。直接递归回溯所有情况, 便可知道最大连续值了。
回溯法
变量说明:
- ans为最终方案的面值组合,maxStampVal是最终方案的最大面值
- stampVal[1~n]来保存各个邮票面值,maxVal[1~n]来保存当前所有面值能组成的最大连续面值
- stampVal[1]一定是等于1的。因为如果没有1的话,很多数字都不能凑成
- maxVal[1] = 1*h ,h为允许贴邮票的数量
- 对于stampVal[i+1],它的取值范围是stampVal[i]+1 ~maxVal[i]+1.
- occur是一个全局数组,调用递归时先初始化为0,然后用它来记录出现过的面值之和,最后只需要从occur数组的下标1开始枚举,直到不是true值时就是能达到的最大连续面值。
回溯过程:(进行到第cur种面额邮票时)
- cur > n 时:算法搜索到叶结点,得到新的邮票面值设计方案x[1:n]。如果该方案能够给出的最大连续邮资区间大于当前已找到的最大连续邮资区间maxvalue,则更新当前最优值maxvalue和相应的最优解ans。
- cur <= n 时:当前扩展结点Z是解空间的内部节点。在该节点处,x[1:cur-1]能给出的最大连续邮资区间为r-1因此,在结点Z处,x[i]的可取值范围是[x[cur-1] + 1 : r + 1],从而,结点Z有r - x[cur-1]个儿子节点。算法对当前扩展结点Z的每个儿子结点,以深度优先的方式递归地对相应子树进行搜索
dfs(int mcur, int n, int sum)说明:
- 计算给定面额种类下,最大数量不超过m时,当前数量为mcur时能够达到的最大连续面值sum
- 当前用了mcur张邮票,在当前邮票种类数 n下,面额之和是sum
- mcur >= m 时:达到了单种邮票的最大张数,记录当前总的面额sum
- mcur < m 时:
- 记录记录当前总的面额sum
- 将其他种类的邮票各加一张进行深度优先搜索
#include<cstring>
#include<cstdio>
#include<iostream>
const int MAXN = 200;
using namespace std;
int m, n;
int ans[MAXN], maxStampVal, stampVal[MAXN], maxVal[MAXN];//从下标1开始赋值
bool occur[MAXN];
void dfs(int mcur, int n, int sum){
// mcur当前用了几张票, n当前面额种类数, sum面额之和
// 计算给定面额和数量能够达到的最大连续面值
if(mcur >= m){//达到单种邮票的最大张数
occur[sum] = true;
return;
}
occur[sum] = true;//sum面额可以达到
for(int i = 1; i <= n; ++i){//一张一张地增加
dfs(mcur + 1, n, sum + stampVal[i]);//面额种类加1,面额之和需加上当前邮票面额
}
}
void search(int cur){//在 第cur种 邮票下进行搜索
if(cur > n){//已经达到最大邮票种类数,cur = n + 1
if(maxVal[cur - 1] > maxStampVal){//如果第n张邮票的最大连续面额为当前最大
maxStampVal = maxVal[cur - 1];//更新最大连续面额
memcpy(ans, stampVal, sizeof(stampVal));//将邮票面额组合赋值给ans
}
return ;
}
for(int i = stampVal[cur - 1] + 1; i <= maxVal[cur - 1] + 1; ++i){
//对第cur种面额的可取值范围进行最大连续面额的搜索
memset(occur, 0, sizeof(occur));//每一次新的面额下都要初始化出现数组
stampVal[cur] = i;//第cur个邮票面值为 i
dfs(0, cur, 0);//进行张数的搜索
int num=0, j=1;
while(occur[j++]) ++num;//寻找第一个没有连续访问的面额num
maxVal[cur] = num;//第cur种邮票的最大面额为num
search(cur+1);//对第cur+1种邮票进行搜索
}
}
int main(){
//n面额数,m每种邮票可以使用的数量
while(scanf("%d %d", &n, &m)!=EOF){
stampVal[1] = 1;
maxVal[1] = m;
maxStampVal = -1;
search(2);
for(int i = 1; i <= n; ++i)
printf("%3d", ans[i]);
printf(" ->%3d\n", maxStampVal);
}
return 0;
}
回溯法+动态规划法
设不超过m张面值为x[1:i]的邮票贴出邮资j所需的最少邮票数为y[j]。通过y[j]可以很快推出r的值。事实上,y[j]可以通过递推在O(n)时间内解决:
类似于0-1背包问题:
- y[j]表示贴上去的邮资j时的最小耗费邮票数,类似背包问题中的w[j]表示价值为j时的最小重量
- y[j+x[i-1] * k] = min(y[j+x[i-1] * k],y[j] + k),类似背包问题中的w[j] = min(w[j-val[i-1]],w[j])
- 初始化y[j]时,意味着在只有m张面额为1的邮票下,贴上去的邮资为j时的最小耗费邮票数,即y[j] = j
- 最大连续邮资为r,maxVal[cur] = r
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++;
#include<cstring>
#include<cstdio>
#include<cstdlib>
#include<iostream>
#define INF 2147483647
const int MAXN = 2000;
using namespace std;
int m, n;
int ans[MAXN], maxStampVal, stampVal[MAXN], maxVal[MAXN], y[MAXN];
bool occur[MAXN];
void search(int cur){//搜索前cur种邮票的最大连续邮资
if(cur > n){
if(maxVal[cur-1] > maxStampVal){
maxStampVal = maxVal[cur-1];
memcpy(ans, stampVal, sizeof(stampVal));
}
return ;
}
int tmp[MAXN];//tmp存储当前y数组,为接下来回溯后返回用
memcpy(tmp, y, sizeof(y));
for(int i = stampVal[cur - 1] + 1; i <= maxVal[cur - 1] + 1; ++i){
//对第cur种面额的可取值范围进行最大连续面额的搜索
stampVal[cur] = i;//第cur个邮票面值为 i
// 关键步骤,利用了动态规划的思想
//设不超过m张面值为x[1:i]的邮票贴出邮资j所需的最少邮票数为y[j]
//y贴出的邮资为0~(第cur-1种邮票)*m
for(int j = 0; j < stampVal[cur - 1] * m; ++j){
if(y[j] < m){//不超过m张的时候
for(int num = 1; num <= m - y[j]; ++num){
//num为可以贴上面额为i的邮票的数量
//如果最少邮票数y[j]添加num张邮票时的张数(y[j] + num)
//小于 y[j]再添加num张邮票时的邮资 的张数(y[j + i * num])时,
//说明邮资为j + i * num时耗费的张数应该是更小的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];//最大连续邮资为r
while(y[r + 1] < INF) r++;//当y[r+1]有进行动态规划得到的值时,说明最大连续邮资可以到r+1
maxVal[cur] = r;//当前的最大连续邮资为r
search(cur + 1);//搜索cur+1时的情况
memcpy(y, tmp, sizeof(tmp));//回溯y
}
}
int main(){
//n邮票面额种类数,m单种邮票最大张数
while(scanf("%d %d", &n, &m)!=EOF){
stampVal[1] = 1;//第一张邮票必须是1
maxVal[1] = m;//用m张面值为1的邮票组成的最大连续面额为m
int i=0;
for(i = 0; i <= m; ++i)
y[i] = i;//只有一张面值为1的邮票时,y[i]=i
while(i < MAXN) y[i++] = INF;//其他y[i+1~MAXN]必须最大,方便之后选更小的
maxStampVal = -1;
search(2);
for(i = 1; i <= n; ++i)
printf("%3d", ans[i]);
printf(" ->%3d\n", maxStampVal);
}
return 0;
}
参考:
1.王晓东《计算机算法设计与分析》p151~152
2.UVa 165 - Stamps, 连续邮资问题
3.屈婉玲老师的算法设计与分析课程