【动态规划】Poj1742 Coins【解法汇集】

【 题目描述 】
金银岛上的人使用金币,每种金币面值分别是 A1; A2; A3; : : : ; An 元。一天 Tony 决定在
附近商店买一个非常好的表,他想在付钱的时候不要找零,但是他发现他的钱包里每种金
币的数量分别只有 C1; C2; C3; : : : ; Cn 个。不过,Tony 知道这块表的价格不会超过 M 元金币
(他不知道表的精确价格)。不知他的付钱方式能否实现。
你的任务是帮助 Tony 算一下,在 1::M 元范围内(包括边界),他钱包中的金币可以精
确支付多少种价格。
【 输入格式 】
输入包括多组测试数据。每组测试数据的格式如下:
第一行包括 2 个整数 n; M。
第二行包括 2n 个整数 A1; A2; A3; : : : ; An 和 C1; C2; C3; : : : ; Cn。
测试数据的最后一行有 2 个 0,这一行无需处理。
【 输出格式 】
每组测试数据输出一行,为一个整数,即能精确支付的价格种数。
【输入输出样例】
coin.in coin.out
3 10
1 2 4 2 1 1
2 5
1 4 2 1
0 0
8
4
【 数据规模与约定 】
对于 30% 的数据,1<=N<=30; 1<= M<=1000
对于 60% 的数据,1<=N<=60
对于 100% 的数据,1<=n<=100; 1<=M<=10^5;1<=Ai<=10^5;1<=Ci<=1000

————————————————分割の线——————————————————
楼天成男人八题入门级,如下提供四种解法:

Way1

限制数量的完全背包

本质:多重背包转完全背包
因为是有多个,所以可以先看作是完全背包,但在状态转移的过程中,使达f[i]状态时使用的当前种类的物品尽可能的少(即如果之前有达到过面值i,则不进行更行)
详细问题看代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int f[100100],cnt;//f[i]表示面值为i能否达到
int g[100100];
int a[110],c[110];//记录单个面值和数量
int main()
{
    freopen("coin.in","r",stdin);
    freopen("coin.out","w",stdout);
    int n,m;
    scanf("%d%d",&n,&m);
    while(n!=0||m!=0)//如果n,m都等于0则跳出循环
        {
        cnt=0;
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        for(int i=1;i<=n;i++)
            scanf("%d",&c[i]);
        memset(f,0,sizeof(f));//先都赋值为达不到
        memset(g,0,sizeof(g));//先默认不需要使用
        f[0]=1;
        for(int i=1;i<=n;i++)
            for(int j=0;j<=m;j++)
                if(f[j]&&f[j+a[i]]==0&&j+a[i]<=m&&g[j]<c[i])//f[j]->只有当前状态成立,转移的状态才会成立
                {//f[j+a[i]]==0->减少更新,以使g[i]尽可能小,能到达更多的状态;
                    f[j+a[i]]=1;
                    g[j+a[i]]=g[j]+1;//此处可以用g[j][i]表示到状态j使用i物品最少个数,因为只与当前第i种物品有关,所以可以用滚动数组保存,以省一维空间。
                }
        for(int i=1;i<=m;i++)
            if(f[i])cnt++;//如果面值可以达到则可能数++
        cout<<cnt<<endl;
        scanf("%d%d",&n,&m);
    }
    return 0;
}

方法一结束


Way2

滚动数组的可行性

本质:有可行数统计的完全背包
即是通过判断是否可行,并以滚动数组进行维护,与Way1有几分神似
详见代码:

#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
int n,m,f[100100],c[110],v[110],ans=0;
//v[i]表示第i种面值,c[i]表示第i种数量,n、m不解释 
void mpAble()
{
    memset(f,-1,sizeof(f));//全部赋值为不可行 
    f[0]=0;//无论何时,面值为0一定可行 
    for(int i=1;i<=n;i++)
    {
        for(int j=0;j<=m;j++)
        {
            if(f[j]>=0) f[j]=c[i];//如果原来是可行的则还可以再放c[i]个硬币 
            else f[j]=-1;//如果原来是不可行的,那第i种面值也不可行 
        }
        for(int j=0;j<=m-v[i];j++)
            if(f[j]>0)
        f[j+v[i]]=max(f[j+v[i]],f[j]-1);//如果可以放置,比较放置后的最大可行性。 
    }
}
int main()
{
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        if(n==0&&m==0) break;
        for(int i=1;i<=n;i++)
            scanf("%d",&v[i]);
        for(int i=1;i<=n;i++)
            scanf("%d",&c[i]);//如上完成读入 
        mpAble();//ans记录所有可行的面值数量 
        for(int i=1;i<=m;i++)
            if(f[i]>=0) ans++;
        cout<<ans<<endl;//输出 
        ans=0;
    }
    return 0;
}

方法二结束


Way3

物品数量的二进制优化

本质:多重背包转01背包
针对至多为一千的物品数,可以划分为1,2,4,8……c[i]-1-2-4-8,通过二进制的进位划分成不同数量的集合
如此可以在不影响01背包正确性的情况下,把c[i]化为log2级,使得复杂度(n*m*log(c[i]))

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,m;
int a[105],c[105],wl[105],w[105][20];//w[i][j]表示第i种物品的第j种集合的物品数量
int f[100050];
void Init()
{//按照二进制划分背包数量
    int j;
    for(int i=1;i<=n;i++)
    {
        wl[i]=0;//w[i]表示划分个数
        j=1;//j表示划分出的理想集合中i物品有j个
        while(1)
        {
            if(j<c[i])//如果剩余的物品数量可以被再次划分
            {
                c[i]-=j;//减去理想集合集合的数量
                w[i][++wl[i]]=j;//记录理想集合中的物品数量数量(化理想为现实)
                j=j<<1;//理想集合中物品数量进一位(乘2)
            }
            else//如果已经不能再被划分
            {
                w[i][++wl[i]]=c[i];//记录剩余的所有物品个数
                break;//跳出循环,无需再做(理想破灭)
            }
        }
    }
    return ;
}
int main()
{
    freopen("coin.in","r",stdin);
    freopen("coin.out","w",stdout);
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        if(n==0&&m==0)return 0;//如果计算至文末,结束程序
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        for(int i=1;i<=n;i++)
            scanf("%d",&c[i]);//如上为读入部分
        Init();//划分背包数量的预处理
        memset(f,0,sizeof(f));//默认所有的值均为不可能
        f[0]=1;//面值为0,一定可行
        for(int i=1;i<=n;i++)//枚举物品种类
            for(int k=1;k<=wl[i];k++)//枚举数量的集合数量
                for(int j=m-a[i]*w[i][k];j>=0;j--)//枚举面值
                    f[j+a[i]*w[i][k]]=f[j+a[i]*w[i][k]]|f[j];//如果f[i]==1,则f[j+a[i]*w[i][k]],用按位或进行无脑运算
        int ans=0;
        for(int i=1;i<=m;i++)
            ans+=f[i];//统计可行的面值数
        cout<<ans<<endl;
    }
    return 0;
}

方法三结束


Way4

多重背包+单调队列的优化

本质:多重背包
在不改变多重背包的性质下,用单调队列优化决策和运行速度。
有趣的是这道题的单调队列不需要删除队尾,所以可以定义FIFO队列,想一想这是为什么?
代码如下:

#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int n,m;
int a[110],c[110];
queue<int>q;//建立一个FIFO的队列q(此题的单调队列只进不出) 
bool f[100100];
int main()
{
    freopen("coin.in","r",stdin);
    freopen("coin.out","w",stdout);
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        if(n==0&&m==0)return 0;
        for(int i=1;i<=n;i++)
            scanf("%d",&a[i]);
        for(int i=1;i<=n;i++)
            scanf("%d",&c[i]);
        memset(f,0,sizeof(f));
        f[0]=1;
        for (int i=1;i<=n;i++)//如下进行01背包,完全背包,多重背包的分解 
        {
            int maxl=a[i]*c[i];
            if (c[i]==1)//如果只有一件物品, 则进行01背包 
            {
                for(int j=m;j>=a[i];j--)
                if(f[j-a[i]])
                f[j]=1; 
            }
            else if(m<=maxl)//如果物品总面值大于最大面值,则进行完全背包 
            {
                for(int j=a[i];j<=m;j++)
                if (f[j-a[i]])
                f[j]=1; 
            }//如下才是重头戏,完全背包的单调队列优化 
            else for(int j=0;j<a[i];j++)//避免重复 
            {
                while(q.size()>0) q.pop();//清空队列 
                for(int k=j;k<=m;k+=a[i])//充分利用大小 
                {
                    while(q.size()>0&&k-q.front()>maxl) q.pop();//始末两点之间的距离应小于第i种物品的总价值 
                    if(!f[k])//由此对于枚举c[i]和m有了较大的提升 
                    {
                        if(q.size()>0)//只有队列不为空,即可以从某点到达 
                        f[k]=1;
                    }
                    else q.push(k);//如果等于0,则入队尾 
                } 
            }
        }
        int ans=0;
        for(int i=1;i<=m;i++)
            if(f[i])ans++;//统计可行的面值数 
        printf("%d\n",ans);
    }
    return 0;
}

方法四结束


至此coins的四种方法全部整理完毕,如果有所遗漏请在下方评论区提出

猜你喜欢

转载自blog.csdn.net/xyc1719/article/details/80042739
今日推荐