【题解】[POI2004]PRZ

[POI2004]PRZ 解题报告;状态压缩DP


题意

\(n\)名队员分成若干组,每名队员有重量\(w_i\)与过桥时间\(t_i\)。每组的总重量不超过\(W\)且该组对答案的贡献为组内队员中最大的\(t_i\),求全部\(n\)名队员分组能得到的最小贡献。
其中 \(100≤W≤400,1≤n≤16,1≤t_i≤50,10≤w_i≤100\)

思路

看到\(1≤n≤16\),于是八九不离十用状态压缩DP。定义状态i为选择编号是\(i\)的二进制表示中为\(1\)的所有队员。
“这不是状压DP模板吗” 刚做完实打实模板题[USACO12MAR]Cows in a Skyscraper G的某人如是想。
但是奶牛题问的是最少分组数,而这一题问的是时间最短的安排。那么将每组对答案的贡献从\(1\)改为组里最大的\(t_i\)不就可以了?某人狂喜,欲套模板。但是如果能够成功把当前队员塞到现有的某组里,不能保证这一组的时间最优。

怎么办?

在奶牛题中,我们用一个一维DP数组记录答案,再用一个一维数组r[i],记录i状态下的最大剩余空间以对“是否能把奶牛加入现有组里”进行约束。在这题中,我们用两个一维数组twt[i]w[i]分别记录选择i状态的人为一组时的时间和总重量。最后以w为约束,t为贡献进行DP。

问题转化为“从若干组里挑若干组,每组交集为空。使所有人都被选,求最小贡献”。好耶ヽ(✿゚▽゚)ノ

枚举状态i和它的子集j,那么\(i=j+(i \oplus j)\),也即将状态i视作 若干组合并后的结果\(j\) 和 单组\(i \oplus j\) 合并后得到的状态。那么f[i]=min(f[i],f[j]+w[i^j]),当w[i^j]<=W(是合法的一组).

代码实现

DP过程的内循环如果从1~i枚举j,那么时间复杂度为\(O(2^{2n})\),会TLE两个点拿90分。
但是改变枚举方式就可以了!

实际操作时……
已知

  1. \(j=i&(j-1)\) 作为\(j\)状态的枚举方式的合理性证明。

  • 关于能够简化时间复杂度:由于只用枚举当前状态使置0的那一位的后面的情况,因此大大优化了状态枚举的常数
  • 关于“-1”:只用使当前状态使置0的那一位的后面发生改变!♪(^∀^●)ノ
  • 关于不会重复枚举:当不应该选的人被选时,会产生状态的重复。但是由于在转移状态时有\(&i\),因此不会选上不应该选的人。所以不可能枚举出不合法的状态。
  1. \((i \oplus j)\oplus j=i\).

因此可以用ji^j表示i的“子集”。

AC code

#include<bits/stdc++.h>
using namespace std;
const int INF=0x3f3f3f3f;
const int N=1<<16;
int W,n,T[18],c[18];
int f[N],t[N],w[N];

int main(){
	scanf("%d%d",&W,&n);
	for(int i=1;i<=n;i++) scanf("%d%d",&T[i],&c[i]);
	
	int x;
	for(int i=0;i<(1<<n);i++){
		for(int j=1;j<=n;j++){
			x=i+(1<<(j-1));
			if(i&(x-i)) continue;
			w[x]=w[i]+c[j];
			t[x]=max(t[x],T[j]);
		}
		f[i]=INF;
	}
	
	f[0]=0;
	for(int i=0;i<(1<<n);i++){
		if(w[i]<=W) f[i]=t[i];//因为j=0时会退出下面的循环
		for(int j=i;j;j=i&(j-1)){
			if(w[i^j]<=W) f[i]=min(f[i],f[j]+t[i^j]);
		}
	}
	printf("%d",f[(1<<n)-1]);
	return 0;
}

有想法了会更新

安利一下我在博客园第一篇放错位置的题解

posted @ 2021-12-28 21:45  Searshkiu  阅读(84)  评论(0编辑  收藏  举报