题解 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
",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
",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就过了此题

原文地址:https://www.cnblogs.com/ThinkofBlank/p/10297150.html