B4093 [CSP-X2021 山东] 发送快递

题目传送门

我的博客 - 欢迎光临!

本题弱化版(?):P3052P10483

二者互为双倍经验。


首先对于一个强制捆绑组里的物品,我们直接使用并查集,将一个并查集里的物品合成为一个大物品就好了。那它就变成了 \(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;
}

posted @ 2025-11-12 20:18  qwqSW  阅读(8)  评论(0)    收藏  举报