B4093 [CSP-X2021 山东] 发送快递
二者互为双倍经验。
首先对于一个强制捆绑组里的物品,我们直接使用并查集,将一个并查集里的物品合成为一个大物品就好了。那它就变成了 \(s=0\) 时的情况。
接下来均考虑 \(s=0\) 的情况。
先说说暴力做法吧,考场上打一打还是很有用的。
暴力做法 \(O(2^nn^2)\)
\(s=0\) 的情况下,这道题就是数据加强后的这俩题。详解可以看这里。
转述一下那个题的思路。设 \(dp_{i,S}\) 表示当前已经开到了第 \(i\) 班快递,并且已经邮出去的书的集合为 \(S\) 的情况下,最后一班快递的总重量最小是多少。
刷表法转移这个似乎更快一点。我们就考虑当前这个状态能转移到哪去。
我们枚举当前还没进快递的书包 \(k\),如果下一个进快递的是它,先给出结论:只需要两种情况,一种是新开一个快递让它装进去,一种是让它进最后一班快递。
为什么不考虑它进入先前快递的情况呢?因为我们发现,它进的如果是靠前一点的快递的话,那当这班快递还是最后一班快递的时候应该考虑过这个情况了。
我们细想一下,它在这班快递时可能有多本书和它一班,它和其他书的所有可能组合,都应该在这个靠前一点的快递还是最后一班快递时考虑过了。
剩下的就好懂多了。直接上暴力代码。
B4093暴力
#include<bits/stdc++.h>
using namespace std;
const int N=24;
const int inf=0x3f3f3f3f;
int n,m,a[N],fa[N],dp[2][(1<<(N-1))+3],b[N],cnt,to[N];
string s;
inline int FIND(int x){
return (x==fa[x]?x:fa[x]=FIND(fa[x]));
}
inline void MERGE(int x,int y){
int fx=FIND(x),fy=FIND(y);
if(fx==fy) return ;
fa[fy]=fx;
}
signed main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);fa[i]=i;
}
int ss;
scanf("%d",&ss);
for(int i=0;i<=ss;i++){
int x,lst=-1,num=0;
//用了一个很猎奇的方法处理不定个数字:读字符串。。。
getline(cin,s);
s=s+' ';
int len=s.size();
for(int j=0;j<len;j++){
if(s[j]==' '){
x=num;
if(lst!=-1){
//将上一个与它合并,这样一整个该行输入都会被合并起来
MERGE(x,lst);
}
lst=x;
num=0;
}
else{
num=num*10+s[j]-'0';
}
}
}
//b数组为合并后的数组,这里是在处理b
for(int i=1;i<=n;i++){
int fi=FIND(i);
if(!to[fa[i]]){
to[fa[i]]=++cnt;
}
b[to[fa[i]]]+=a[i];
}
n=cnt;
for(int i=0;i<n;i++){
a[i]=b[i+1];
}
for(int i=0;i<=n;i++){
for(int S=0;S<(1<<n);S++){
dp[(i&1)][S]=inf;
}
}
//由于空间不够,所以dp数组用滚动数组优化空间
//初始化,第一班快递里什么书都不装的话,那总重显然是0
dp[1][0]=0;
for(int i=0;i<=n;i++){
for(int S=0;S<(1<<n);S++){
if(dp[(i&1)][S]!=inf){//只有当前状态合法才能转移
for(int k=0;k<n;k++){//枚举当前没进快递的书包
if(S&(1<<k)) continue;
//新开一班快递
dp[((i+1)&1)][(S|(1<<k))]=min(dp[((i+1)&1)][(S|(1<<k))],a[k]);
//进最后一班快递
if(dp[(i&1)][S]+a[k]<=m){
dp[(i&1)][(S|(1<<k))]=min(dp[(i&1)][(S|(1<<k))],dp[(i&1)][S]+a[k]);
}
}
}
}
//如果当前用i班快递就能送完所有书的话,那答案就是i了
if(dp[(i&1)][(1<<n)-1]!=inf){
printf("%d\n",i);
break;
}
}
return 0;
}
正解
100 分做法在此基础上,就是一句话的事情:你要把当前某个即将加入快递序列的书,放进当前最空的快递序列里。如果实在装不下,就新开一班快递。
那有的人就要问了:假设当前有一个 \(x\),\(x\) 可以装进 \(u,v\) 两班快递里(\(v\) 的剩余容量比 \(u\) 大),并且有一个 \(y\) 只能装进 \(v\) 里(并且 \(x,y\) 由于容量不够不能同时装进 \(v\) 里),那万一让 \(y\) 装进 \(v\),\(x\) 装进 \(u\) 的情况更优怎么办?
可是我们接着思考一下这个情况:当我们先考虑 \(y\) 而非 \(x\) 时,\(y\) 装进去的就是最空的那班快递序列 \(v\);然后再考虑 \(x\) 的话,就会考虑到 \(x\) 装进 \(u\) 的情况了(如果 \(u\) 是当前最空的话)。
或者我这么说,按我们这个逻辑考虑的话,对于有 \(x\) 无 \(y\) 的情况 \(S\),让 \(x\) 进 \(v\) 更优;有 \(y\) 无 \(x\) 的情况 \(T\),也是让 \(y\) 进 \(v\) 最优。
而当我们转移有 \(x\) 有 \(y\) 的状态时,我们会考虑 \(S\) 状态加入 \(y\) 的转移,此时明显只能新开一班快递;但是如果是从 \(T\) 状态里加入 \(x\) 的话,\(x\) 可以进入 \(v\) 快递,就不需要再开一班快递了。所以显然是从 \(T\) 状态转移优。
所以我们这样相当于是考虑了这种情况、也考虑了一个不是很优的情况,但是最终转移肯定是从最优状态转移而来,所以它是正确的。
所以我们对于每个 \(S\) 维护它要开多少班快递,以及最空的那班快递的最小容量。
我们可以填表法转移,每次枚举最后一个进快递的书 \(i\) 并转移状态 \(S\) 即可。
然后我们就能以 \(O(n2^n)\) 的时间复杂度通过本题。考虑到极限情况下要跑 \(2 \times 10^8\),所以千万不要把常数写大。
代码:
B4093正解
#include<iostream>
#include<cstdio>
using namespace std;
const int N=25;
const int inf=2147483647;
int n,m,a[N],fa[N],to[N],b[N],cnt;
string s;
struct sw{
int cnt,val;
}dp[(1<<(N-1))+5];
//dp[S]:当前送出去的快递集合是S,最小要送多少班快递,并且每班快递里最空的一班快递的容量
bool operator <(const sw SW,const sw WS){
//对于一个状态来说,首先要让开的快递班数尽可能小,其次要让最小容量最小
return (SW.cnt==WS.cnt?SW.val<WS.val:SW.cnt<WS.cnt);
}
inline int FIND(int x){
return (x==fa[x]?x:fa[x]=FIND(fa[x]));
}
inline void MERGE(int x,int y){
int fx=FIND(x),fy=FIND(y);
if(fx==fy) return ;
fa[fy]=fx;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];fa[i]=i;
}
int ss;cin>>ss;
for(int i=0;i<=ss;i++){
int x,lst=-1;
cin>>lst;
if(cin.get()=='\n') continue;
while(cin>>x){
MERGE(x,lst);
if(cin.get()=='\n') break;
}
}
//b数组为合并后的数组,这里是在处理b
for(int i=1;i<=n;i++){
int fi=FIND(i);
if(!to[fa[i]]){
to[fa[i]]=++cnt;
}
b[to[fa[i]]]+=a[i];
}
n=cnt;
for(int i=0;i<n;i++){
a[i]=b[i+1];
}
//预处理部分同上
for(int S=0;S<(1<<n);S++){
dp[S]={N,inf};
}
for(int i=0;i<n;i++){
dp[(1<<i)]={1,a[i]};
}
for(int S=1;S<(1<<n);S++){
if((S&(S-1))==0) continue;//1后面很多0的情况,这样的情况是初始化了的,所以不参与转移
//刚才为什么要提常数写大了的事呢。。。因为这里的mn只需要在第一层循环更新S状态,在第二层更新的话很可能T
sw mn={N,inf};
for(int i=0;i<n;i++){
if((S&(1<<i))){
sw s;
if(a[i]+dp[S^(1<<i)].val>m){
//新开一班快递
s={dp[S^(1<<i)].cnt+1,a[i]};
}
else{
//挤进当前的快递
s={dp[S^(1<<i)].cnt,dp[S^(1<<i)].val+a[i]};
}
if(s<mn){
mn=s;
}
}
}
dp[S]=mn;
}
int ans=dp[(1<<n)-1].cnt;
cout<<ans;
return 0;
}

浙公网安备 33010602011771号