题解 P3423 【[POI2005]BAN-Bank Notes】
本题有两个问,第一个是求最少硬币数,第二个则是求方案(翻译竟然没写。。。)。
首先,我们来解决第一问。
我们可以很容易想出,这是一个dp,我们设dp[i]表示凑出面值i最少需要多少个硬币,然后打个多重背包就好了。。。于是你就T了。。。
对于多重背包,我们通常使用一种手段:二进制拆分(听说还可以使用单调队列优化,但是本弱鸡并不会。。。QwQ)
二进制拆分,它的原理是:任何一个数都可以用20,21,22...2k表示(每个最多取1个)。
于是,对于一个物品的个数C,我们把它成:C=20+21+22...+2k+X(X<2^(k+1)),这我们就能表示0-C的所有数,然后,我们跑一遍01背包就好了,对于一个物品,它拆分出的物品中,被选的物品所代表的个数之和即是这个物品被选了几个。
复杂度O(nklog(n)),于是,我们成功切掉了第一问。。。
那么,重要的是第二问了。。。
某dalao曰:最优美的算法是用的暴力还能A题。
于是,我们就考虑使用dfs来解决此题
首先,我们把问题转化成了01背包,那么我们我们就需要知道:我们选了哪些数,我们怎么解决呢?
首先我们设每个物品的参数:B[i]->面值,C[i]->对答案的影响值(其实就是二进制拆分时拆出来的物品个数),D[i]->由哪个物品拆出来的。
我们假设,现在我们要知道面值x可以由哪些物品组合出来,那么,我们知道,要选择物品i的话,其必要条件是:x>=B[i]并且dp[x-B[i]]+C[i]==dp[x]。于是我们就可以用dfs来枚举选哪些物品了。
代码:
//#pragma GCC optimize()//手动Ox优化
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+1,M=2600001;
int dp[N],b[N],c[N];
int B[M],C[M],D[M];
int ti[N];
bool vis[N];
int e;int n;
inline void dfs(int x){
if(!x){
for(int i=1;i<=n;++i){
printf("%d ",ti[i]);
}
exit(0);//找到一种方案后,输出,然后直接强制结束程序
}
for(int i=1;i<=e;++i){
if(vis[i]||B[i]>x){//如果被选过了,或者超出了,则不选
continue;
}
if(dp[x]==dp[x-B[i]]+C[i]){//如果可以选
vis[i]=1;
ti[D[i]]+=C[i];//第D[i]个选上C[i]个
dfs(x-B[i]);
ti[D[i]]-=C[i];
vis[i]=0;
}
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d",&b[i]);
}
for(int i=1;i<=n;++i){
scanf("%d",&c[i]);
for(int k=0;k<=13;++k){//二进制拆分
if(c[i]>=(1<<k)){
B[++e]=(1<<k)*b[i];
C[e]=(1<<k);
D[e]=i;
c[i]-=(1<<k);
}else{
break;
}
}
if(c[i]){//处理剩余的
B[++e]=c[i]*b[i];
C[e]=c[i];
D[e]=i;
}
}
memset(dp,0x3f3f,sizeof(dp));//初始化
dp[0]=0;
int v;
scanf("%d",&v);
for(int i=1;i<=e;++i){
for(int j=v;j>=B[i];--j){
dp[j]=min(dp[j],dp[j-B[i]]+C[i]);
}
}
printf("%d\n",dp[v]);
dfs(v);
return 0;
}
这个代码吸氧可过,不吸氧的话,不清楚,大家可以试验下。。。
不过,这明显不是我们希望的效果。
那么,我们可以考虑优化,怎么优化呢?
我提出一种思路:
我们把所有满足:dp[v-B[i]]+C[i]==dp[v]的物品叫做v的可行物品。
我们想下,为什么不是所有可行物品可以被选择。
理由是:对于物品i,他会对dp[v],与dp[v-B[i]]的结果同时造成影响。也即是说,有可能v-B[i]的可行物品中有i,v中又有i,那么i就被选了两次!
那么,我们就可以推出:必选物品的条件:
一个物品必定被选,就相当于,该物品是可行物品且选两个该物品后就会错误或者无法选两个,即:
v>=B[i]并且dp[v]==dp[v-B[i]]+C[i],且:
v>=2B[i],dp[v-2B[i]]+2C[i]!=dp[v]或者v<2B[i]
那么,我们把所有必选的物品选了,再去dfs,就会快上许多,当然,我们可以重复这个过程直到没有关于现在体积的必选物品为止。
代码:
//pragma GCC optimize()//手动Ox优化
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+1,M=2600001;
struct node{
int B,C,D;
}t[N];
int dp[N],b[N],c[N];
int B[M],C[M],D[M];
int ti[N];
bool vis[N],vit[N];
int e,n,cnt;
inline void dfs(int x){
if(!x){
for(int i=1;i<=n;++i){
printf("%d ",ti[i]);
}
exit(0);
}
for(int i=1;i<=cnt;++i){
if(!vit[i]&&dp[x-t[i].B]+t[i].C==dp[x]){
vit[i]=1;
ti[t[i].D]+=t[i].C;
dfs(x-t[i].B);
ti[t[i].D]-=t[i].C;
vit[i]=0;
}
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;++i){
scanf("%d",&b[i]);
}
for(int i=1;i<=n;++i){
scanf("%d",&c[i]);
for(int k=0;k<=13;++k){
if(c[i]>=(1<<k)){
B[++e]=(1<<k)*b[i];
C[e]=(1<<k);
D[e]=i;
c[i]-=(1<<k);
}else{
break;
}
}
if(c[i]){
B[++e]=c[i]*b[i];
C[e]=c[i];
D[e]=i;
}
}
memset(dp,0x3f3f,sizeof(dp));
dp[0]=0;
int v;
scanf("%d",&v);
for(int i=1;i<=e;++i){
for(int j=v;j>=B[i];--j){
if(dp[j-B[i]]+C[i]<dp[j]){
dp[j]=dp[j-B[i]]+C[i];
}
}
}
printf("%d\n",dp[v]);
while(true){
bool flag=0;
for(int i=1;i<=e;++i){
if(vis[i]){
continue;
}
if((v>=2*B[i]&&dp[v-B[i]-B[i]]+C[i]+C[i]!=dp[v]&&dp[v-B[i]]+C[i]==dp[v])||(v<2*B[i]&&v>=B[i]&&dp[v-B[i]]+C[i]==dp[v])){//必选
v-=B[i];
ti[D[i]]+=C[i];
flag=1;
vis[i]=1;
}
}
if(!flag){//如果没有必选物品了,就退出
break;
}
}
for(int i=1;i<=e;++i){
if(vis[i]){
continue;
}
if(v>=B[i]&&dp[v-B[i]]+C[i]==dp[v]){//统计所有的可行物品
t[++cnt]={B[i],C[i],D[i]};
}
}
dfs(v);//dfs
return 0;
}
加了此优化后,速度飞起,仅用257ms就过了此题