连续邮资问题

/*   

连续邮资问题   

算法设计:   

该问题是设计最佳的邮票面值,用来表示最大的区间   

对于连续邮姿问题,用n元组x[1:n]表示n种不同的邮票面值并约定它们从小到大排列。   

整数r表示当前使用不超过m张邮票能贴出的最大连续邮资区间。 

x[1] = 1是唯一的选择。此时最大连续邮资区间是[1:m]   

接下来x[2]的可能取值范围是[2:m+1],这个取值范围就决定了x[2]应该怎么选,   

只所以确定这样的范围是因为,至少要保证增加一个面值后,可以将区间增   

大1一般情况下,已选定x[1:i-1],此时最大连续邮资区间是[1:r],则接下来   

x[i]的可取值范围是[x[i-1]+1:r+1]。由此可以看出,在用回溯法时,可以用一棵树表示其   

解空间。该解空间树中各结点的度随x的不同取值而变化。   

   

设x[1~n]是从小到大排列的n种邮票面值。    

首先x[1]=1 是毫无疑问的,否则最小邮资 1 无法支付。    

面值为1的邮票构成的最大连续区间只能是[1,m] (只贴1张就是1……m张邮票全贴满是m)。   

如果这时候再给我们第二种面额的邮票。因为它需要比x[1]大,所以最少只能是2;   

又因为单纯由x[1]支付的邮资区间最大只到m,再往上就出现了断档,所以x[2]最大也只能取m+1   

(如果再大的话,m+1这个值就无法正常表示)    

同样的道理,假设前面k种邮票面值都已经有了,并且能构成[1, r]的连续邮资区间,   

那么第k+1种邮票的面值必须满足:    

x[k]+1 <= x[k+1] <= r+1    

算法就是要找到这么多种可能情况下的最优方案   

   

对于连续邮资问题,用n元组x[1:n]表示n种不同的邮票面值,并约定它们从小到大排列。   

x[1] = 1是惟一的选择。在未确定剩余其它n-1种邮票面值的情况下,只用x[1]一种邮票面值,   

所能得到的最大连续邮资区间是[1:m]。接下来,确定x[2]的取值范围,很明显,   

x[2]的值最小可以取到2,因为x[1] 这时已经为1了。那么最大可以取的值呢?应当是m+1,   

为什么?因为如果x[2]取m+2,则在这个方案中,邮资m+1不可能取得,   

(这个时候,x[2:n]的任何一个面值是不能取的,因为就算只取一张,总和也会至少为m+2,   

超过m+1了,反过来,如果只取x[1]面值,则就算把m张全取了,也只能凑到m)。所以,   

在搜索第2张邮票的时候,搜索范围是2 ~ m+2(m+2不剪取)。在一般的情况下,已选定x[1:i-1],   

最大连续邮资区间是[1:r]时,接下来x[i]的可取值范围是[x[i-1]+1 :r+1]。由此可以看出,   

[x[i-1]+1 :r+1](为限界函数)在用回溯法解连续邮资问题时,可用树表示其解空间,   

该解空间树中各结点的度随x的不同取值而变化。   

   

现在可以大致画出该解空间树的结构。   

现在的关键问题是,如何确定x3的取值范围?转换一下就是如何求得在x[1],x[2]面值确定的情况下,   

用不超过m张邮票,所得到的最大连续邮资区间是多少?如果每一个这样的问题能够确定,   

则解空间树已经构造好了(每一个的儿子结点为r-x[i-1]),我们就可以通过深度优先遍历的方式得到最佳面值设计   

(每到达叶结点见比较更新)。   

   

    现在来看在x[1],x[2]面值确定的情况下,用不超过m张邮票,所得到的最大连续邮资区间是多少?   

最大连续邮资区间总是以1作为起点的,所以我们用max来表示这个最大值,显然,max至少可以取到m,   

因为即使不用x[2]面值,只用x[1]面值的情况下,所能得到的最大值就已经是m了。现在来看在x[1],   

x[2]面值确定的情况下,用不超过m张邮票,max+1能不能取到?    

假设在拼凑的过程中,x[2]面值的邮票取t张,则t>=0,t<=(max+1) /x[2] && t<=m   

现在x[2]面值的邮票张数已经确定,原问题转化为另一个子问题,即,在x[1]面值确定的情况下,   

用不超过m-t张邮票,max+1 – t*x[2] 能不能取到? 。。。。。。   

到这一步就很容易理解了,因为x[1] 面值为1,所以如果要拼凑的值max+1- t*x[2] <= m-t的话,   

则只用取max+1- t*x[2]张就可以拉。相反,如果max+1- t*x[2] > m-t的话,则是不可以满足的。   

   

   

  在下面的回溯法描述中,递归函数Backtrack实现对整个解空间的回溯搜索。   

maxvalue记录当前已经找到的最大连续邮资区间,bestx是相应的当前最优解。   

数组y用来记录当前已经选定的邮票面值x[1:i]能贴出各种邮资所需的最少邮票数。   

   

  也就是说,y[k]是用不超过m张面值为x[1:i]的邮票,贴出邮资k所需的最少邮票张数。   

在函数Backtrack中,   

当i>n时,表示算法已经搜索到一个叶结点,得到一个新的邮票面值设计方案x[1:n]。如果该方案能贴出的   

最大连续邮资区间大于当前已经找到的最大连续邮资区间maxvalue,则更新当前最优值maxvalue和相应的最优解。   

当i <= n时,当前扩展结点z是解空间中的一个内部结点,在该结点处x[1:i-1]   

能贴出的最大最大邮资区间为r-1.因此在结点z处x[i]的可取范围是x[i-1]+1:r,   

从而,结点z有r-x[i-1]个儿子结点。算法对当前扩展结点z的每一个儿子结点,   

以深度优先的方式递归地对相应子树进行搜索   

   

  解空间是多叉树,孩子接点个数是每层都在变化的   

*/   

#include<iostream>  
#include<Windows.h>  
using namespace std;  
   
class Stamp  
{  
friend int MaxStamp(int  ,int  ,int []);  
  
private:  
     // int Bound(int i);  
      void Backtrack(int i,int r);  
      int n;//邮票面值数  
      int m;//每张信封最多允许贴的邮票数  
      int maxvalue;//当前最优值  
      int maxint;//大整数  
      int maxl;//邮资上界  
      int *x;//当前解  
      int *y;//贴出各种邮资所需最少邮票数  
      int *bestx;//当前最优解  
   
};  
  
void Stamp::Backtrack(int i,int r)  
{  
    /*计算X[1:i]的最大连续邮资区间,考虑到直接递归的求解复杂度太高, 
    我们不妨尝试计算用不超过m张面值为x[1:i]的邮票贴出邮资k所需的最少邮票数y[k]。 
    通过y[k]可以很快推出r的值。事实上,y[k]可以通过递推在O(n)时间内解决*/  
	int j,k;
    for(j=0;j<=x[i-2]*(m-1);j++) //x[i-2]*(m-1)是第i-2层循环的一个上限,目的是找到r-1的值   
            if(y[j]<m)  
            {  
                for(k=1;k<=m-y[j];k++) //k是对表示j剩余的票数进行检查     
                {  
                    if(y[j]+k<y[j+x[i-1]*k])  
                //x[i-1]*k是k张邮票能表示的最大邮资     
                //+j表示增加了i邮资后能     
                //判断新增加的能表示的邮资需要多少   
                    {  
                        y[j+x[i-1]*k]=y[j]+k;  
                        //对第i-2层扩展一个x[i-1]后的邮资分布  
                    }  
                    
                }  
            }  
                //查看邮资范围扩大多少,然后查询y数组从而找到r   
                while(y[r]<maxint) //计算X[1:i]的最大连续邮资区间  
                {  
                    r++;  
                }  
                //搜索求出r-1的值,对应x[1:i-1]的在m张限制内的最大区间   
                if(i>n) // 如果到达发行邮票的张数,则更新最终结果值,并返回结果  
                {  
                    if(r-1>maxvalue)  // 用r计算可贴出的连续邮资最大值,而maxStamp存放最终结果  
                    {  
                        maxvalue=r-1;  
                        for(int j=1;j<=n;j++)  
                            bestx[j]=x[j];  // 用x[i]表示当前以确定的第i+1张邮票的面值,bestx保存最终结果  
                    }  
                        return;  
                }  
                int *z=new int[maxl+1];  
                for(k=1;k<=maxl;k++)  
                    z[k]=y[k];  
                //保留数据副本,以便返回上层时候能够恢复数据     
                //以上都是处理第i-1层及其之上的问题     
                for(j=x[i-1]+1;j<=r;j++) //在第i层有这么多的孩子结点供选择     
                {  
                    x[i]=j;  
                    Backtrack(i+1,r);//返回上层恢复信息  
                    for(int k=1;k<=maxl;k++)  
                        y[k]=z[k];  
                }  
                delete[] z;  
}  
  
  
  
int MaxStamp(int n,int m,int bestx[]){  
    Stamp X;  
    int maxint=32767;  
    int i,maxl=1500;  
      
    X.n=n;  
    X.m=m;  
    X.maxvalue=0;  
    X.maxint=maxint;  
    X.maxl=maxl;  
    X.bestx=bestx;  
    X.x=new int [n+1];  
    X.y=new int [maxl+1];  
      
    for(i=0;i<=n;i++)  
        X.x[i]=0;  
    for(i=1;i<=maxl;i++)  
        X.y[i]=maxint;  
    X.x[1]=1;  
    X.y[0]=0;  
    X.Backtrack(2,1);  
    cout<<"当前最优解:";  
    for(i=1;i<=n;i++)  
        cout<<bestx[i]<<"  ";  
    cout<<endl;  
    delete[] X.x;  
    delete [] X.y;  
    return X.maxvalue;  
}  
  
  
int main(){  
  
      
    int *bestx;  
    int n;  
    int m;  
    cout<<"请输入邮票面值数:";  
    cin>>n;  
    cout<<"请输入每张信封最多允许贴的邮票数:";  
    cin>>m;  
  
    bestx=new int[n+1];  
    for(int i=1;i<=n;i++)  
    bestx[i]=0;  
      
    cout<<"最大邮资:"<<MaxStamp(n,m,bestx)<<endl;  
    Sleep(5000);  
      
      return 0;
  
} 

点赞