连续邮资问题详解

3 篇文章 1 订阅
2 篇文章 0 订阅

假设国家发行了k种不同面值的邮票,并且规定每张信封上最多只允许贴h张邮票。连续邮资问题要求对于给定的k和h的值,给出邮票面值的最佳设计,在1张信封上可贴出从邮资1开始,增量为1的最大连续邮资区间。例如,当k=5和h=4时,面值为(1,3,11,15,32)的5种邮票可以贴出邮资的最大连续邮资区间是1到70。(UVA165)

image-20200326170059426

思路概括:

  • 用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。直接递归回溯所有情况, 便可知道最大连续值了。

image-20200326170152534

image-20200326170523901

回溯法

变量说明:

  • 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)时间内解决:

image-20200326145529471

类似于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.屈婉玲老师的算法设计与分析课程

  • 25
    点赞
  • 113
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
连续邮资问题是一个经典的组合优化问题,可以使用回溯算法来解决。在C++中,可以这样实现: ```cpp #include <iostream> #include <vector> using namespace std; // 回溯函数 void backtrack(vector<int>& stamps, vector<int>& combinations, int target, int current, int& minCombLen) { if (target == 0) { // 当目标值为0时,找到了一种组合方案 minCombLen = min(minCombLen, (int)combinations.size()); return; } if (target < 0 || current >= stamps.size()) { // 当目标值小于0或者已经遍历完所有邮资面额时,无法组合出目标值 return; } // 不选择当前面额 backtrack(stamps, combinations, target, current + 1, minCombLen); // 选择当前面额 if (target - stamps[current] >= 0) { combinations.push_back(stamps[current]); backtrack(stamps, combinations, target - stamps[current], current, minCombLen); combinations.pop_back(); } } int main() { vector<int> stamps = {1, 3, 4}; // 假设有1分、3分和4分的邮票 int target = 7; // 目标邮资为7分 vector<int> combinations; int minCombLen = INT_MAX; backtrack(stamps, combinations, target, 0, minCombLen); if (minCombLen == INT_MAX) { cout << "无法组合出目标邮资" << endl; } else { cout << "最少需要" << minCombLen << "张邮票" << endl; } return 0; } ``` 以上代码使用了回溯算法来求解连续邮资问题。首先定义了一个回溯函数`backtrack`,其中`stamps`表示邮票的面额,`combinations`表示当前的邮票组合,`target`表示目标邮资,`current`表示当前考虑的邮票面额的索引,`minCombLen`表示当前的最小组合长度。 在回溯函数中,首先判断目标值是否为0,如果是,则找到了一种组合方案,更新最小组合长度。然后判断目标值是否小于0或者已经遍历完所有邮资面额,如果是,则无法组合出目标值,直接返回。接下来,分别尝试不选择当前面额和选择当前面额两种情况,并进行递归调用。如果选择当前面额时,需要将当前面额加入到组合中,并将目标值减去当前面额。递归调用结束后,需要将当前面额从组合中移除,以便尝试其他组合。 最后,在`main`函数中定义了初始的邮票面额和目标邮资,并调用回溯函数求解最少需要的邮票张数。如果最小组合长度为INT_MAX,则说明无法组合出目标邮资;否则,输出最少需要的邮票张数。 希望这个回溯算法的实现对你有帮助!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值