poj 1276 多重背包+二进制优化+单调队列优化

Cash Machine
Time Limit: 1000MS   Memory Limit: 10000K
Total Submissions: 27570   Accepted: 9806

Description

A Bank plans to install a machine for cash withdrawal. The machine is able to deliver appropriate @ bills for a requested cash amount. The machine uses exactly N distinct bill denominations, say Dk, k=1,N, and for each denomination Dk the machine has a supply of nk bills. For example, 

N=3, n1=10, D1=100, n2=4, D2=50, n3=5, D3=10 

means the machine has a supply of 10 bills of @100 each, 4 bills of @50 each, and 5 bills of @10 each. 

Call cash the requested amount of cash the machine should deliver and write a program that computes the maximum amount of cash less than or equal to cash that can be effectively delivered according to the available bill supply of the machine. 

Notes: 
@ is the symbol of the currency delivered by the machine. For instance, @ may stand for dollar, euro, pound etc. 

Input

The program input is from standard input. Each data set in the input stands for a particular transaction and has the format: 

cash N n1 D1 n2 D2 ... nN DN 

where 0 <= cash <= 100000 is the amount of cash requested, 0 <=N <= 10 is the number of bill denominations and 0 <= nk <= 1000 is the number of available bills for the Dk denomination, 1 <= Dk <= 1000, k=1,N. White spaces can occur freely between the numbers in the input. The input data are correct. 

Output

For each set of data the program prints the result to the standard output on a separate line as shown in the examples below. 

Sample Input

735 3  4 125  6 5  3 350
633 4  500 30  6 100  1 5  0 1
735 0
0 3  10 100  10 50  10 10

Sample Output

735
630
0
0

Hint

The first data set designates a transaction where the amount of cash requested is @735. The machine contains 3 bill denominations: 4 bills of @125, 6 bills of @5, and 3 bills of @350. The machine can deliver the exact amount of requested cash. 

In the second case the bill supply of the machine does not fit the exact amount of cash requested. The maximum cash that can be delivered is @630. Notice that there can be several possibilities to combine the bills in the machine for matching the delivered cash. 

In the third case the machine is empty and no cash is delivered. In the fourth case the amount of cash requested is @0 and, therefore, the machine delivers no cash.

Source

Southeastern Europe 2002



题目大意:

取款机有n种面额的钱币,其面额为d[i],每种面额有最大可取用数num[i]

给一个所需要取用的总钱数needed,要求通过选择钱币的种类和数目,输出不大于这个数的最大的可构造钱数。


思路:

看完上面的题意描述很明显应该联想到多重背包,然后就用最朴素的多重背包写了,壕无疑问,T了。

所以后来开始考虑优化


二进制优化

首先采用了二进制优化,将原本的num[i]个(需要做num[i]次选择)同一种物品,

重新变成重量(面额)分别为1d[i],2d[i],4d[i],8d[i]...的log2n个物品

照着背包九讲的模板敲的,竟然WA了,然后在网上找了一个ac的来对拍,找出了问题,

不乖背包九讲,错就错在自己没有好好理解好他的这一步二进制优化。

具体的错误,和测错数据,还有一些想法,都在下面代码的注释中,那一步错误改了就ac了。


#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define maxv 100015

using namespace std;
int num[15],d[15];
int dp[maxv];
int n;
int needed;

int main()
{
    //freopen("in.txt","r",stdin);
    //freopen("out.txt","w",stdin);
    while(scanf("%d",&needed)!=EOF){
        scanf("%d",&n);
        for(int i=1;i<=n;i+=1){
            scanf("%d %d",&num[i],&d[i]);
        }
        memset(dp,0,sizeof(dp));

        for(int i=1;i<=n;i+=1){
            if(d[i]*num[i]>=needed){      //达不到最高数量上限时可以直接当做完全背包来解
                for(int j=d[i];j<=needed;j+=1){
                    dp[j]=max(dp[j],dp[j-d[i]]+d[i]);
                }
            }
            else{
                //达到了最高数量上限,当做01背包求解
                //将n件物品转为log2n件物品,其重量依次为d[i],2d[i],4d[i]...num[i]-sum
                //sum就是前面所有物品的重量总和,因为不一定可以让每一个数都是2n
                //所以最后一个直接取剩下的就好了
                //这么来严格说复杂度其实不应该是log2n
                //应该是log2(n-sum)
                int k=1;
                int amount=num[i];
                //amount于while中无用,在for中构成最后一件商品的重量amount*d[i]
                while(k=k*d[i];j-=1){
                        dp[j]=max(dp[j],dp[j-k*d[i]]+k*d[i]);
                    }
                    amount-=k;
                    k*=2;
                }
                for(int j=needed;j>=d[i]*amount;j-=1){           //_______错在这个地方,这里的amount错的时候是num[i]
                    dp[j]=max(dp[j],dp[j-d[i]*amount]+d[i]*amount);
                }
            }
        }
        printf("%d\n",dp[needed]);
    }

    return 0;
}

/*
出错数据
答案应该是25xxx
错解为29xxx

29580
1
48
533
*/


单调队列优化

随后旋即用了单调队列优化,一开始自己实在是没有想到优化的切入点,第一次使用优化是在hdu 3401上的

那道题也是背包的变形,但是略微不同,他的第二下标的转移时连续的,而这里的确不是连续的,

首先看看递推式

hdu 3401: dp[i][j] = max (dp[i][j]    ,    dp[i-1][j-k]  +  k*value[i])

本题:  dp[i][j] = max (dp[i][j]    ,    dp[i-1][j-k*d[i]]  +  k*d[i])

在那道题中,主需要不断在 forj 的循环中把最新求得的放入队列即可,但是这道题,如果按照那道题,

没求一个数就放一个进去,很显然是不行的,因为到时候取出来的第二下标不一定符合递推关系(上面标红处就是为难我的地方)


后来找了别人的代码,真的是学习了,先贴上来

#include 
#include 

using namespace std;

#define MAX 35

const int MAX_V = 100004;

//v、n、w:当前所处理的这类物品的体积、个数、价值
//V:背包体积, MAX_V:背包的体积上限值
//f[i]:体积为i的背包装前几种物品,能达到的价值上限。

int total;
inline void pack(int f[], int V, int v, int n, int w){
    if (n == 0 || v == 0) return;
    if (n == 1) {               //01背包
      for (int i = V; i >= v; --i)
        if (f[i] < f[i - v] + w) {
          f[i] = f[i - v] + w;
        }
        return;
    }
    if (n * v >= V - v + 1) {   //完全背包(n >= V / v)
       for (int i = v; i <= V; ++i)
          if (f[i] < f[i - v] + w) {
             f[i] = f[i - v] + w;
          }
          return;
     }

    int va[MAX_V], vb[MAX_V];   //va/vb: 主/辅助队列
    for (int j = 0; j < v; ++j){     //多重背包
        int *pb = va, *pe = va - 1;     //pb/pe分别指向队列首/末元素
        int *qb = vb, *qe = vb - 1;     //qb/qe分别指向辅助队列首/末元素
        for (int k = j, i = 0; k <= V; k += v, ++i) {
            if (pe  == pb + n){       //若队列大小达到指定值,第一个元素X出队。
                if (*pb == *qb) ++qb;   //若辅助队列第一个元素等于X,该元素也出队。
                    ++pb;
            }
            int tt = f[k] - i * w;
            *++pe = tt;                  //元素X进队
            //删除辅助队列所有小于X的元素,qb到qe单调递减,也可以用二分法
            while (qe >= qb && *qe < tt) --qe;
            *++qe = tt;              //元素X也存放入辅助队列
            f[k] = *qb + i * w;      //辅助队列首元素恒为指定队列所有元素的最大值
            //total = f[k];
         }
     }
}

int main()
{
    int V;
    while(~scanf("%d",&V)){
        int v[100010] = {0};
        int w[100010] = {0};
        int f[100010] = {0};
        int k;
        scanf("%d",&k);
        if(k == 0){
            cout<<"0"<=0 ; --j){//最大值f[V]开始搜索等于或小于限度的最大值
            if(f[j] <= V){
                cout<

这个代码的第二层for循环,竟然很巧妙的使用了 (0<=j来使得对于第二下标的每一个连续值都分别枚举了拿商品的数目k

这这这实在是没想到啊............................姿势不够,还需努力

简单讲一下,再看一下递推式:

dp[i][j] = max (dp[i][j]    ,    dp[i-1][j-k*d[i]]  +  k*d[i])

经过这题我发现一个做单调队列优化的小技巧,不要有太多的执念在数字的形式上,

最主要的是看等号左边和右边关键值的


原来看着这个式子什么思路都没有,但是后来发现,

(1)左边的第二下标和右边的第二下标之差为k*d[i]。

(2)又总是可以找到某些a,b,c,使得左边的j=b+a*d[i](b

(3)将两式相减那么第二下标之差,同时右边dp多加的那个数,就有 k*d[i]=(a-c)*d[i]

那么在我的代码里面呢,那个a和c就分别是不同时刻的k,这样就能将向前求取最大值的过程成功构造出来了,也就是说,化归到只与一个变量有关

 

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define maxv 100015

using namespace std;
int num[15],d[15];
int dp[maxv];
int q[maxv];
int n;
int needed;

int main()
{
    while(scanf("%d",&needed)!=EOF){
        scanf("%d",&n);
        for(int i=1;i<=n;i+=1){
            scanf("%d %d",&num[i],&d[i]);
        }
        memset(dp,0,sizeof(dp));

        for(int i=1;i<=n;i+=1){
            for(int j=0;j=num[i]) head+=1;
                    int tmp=dp[k]-countt*d[i];
                    while(head<=tail&&tmp>q[tail]) tail-=1;
                    tail+=1;
                    q[tail]=tmp;
                    dp[k]=q[head]+countt*d[i];
                }
            }
        }
        printf("%d\n",dp[needed]);
    }

    return 0;
}






你可能感兴趣的:(单调队列,dp,背包)