题意:说白了还是个01背包,给出每种物品的两个属性w和t,对于给定的m和s,求是否存在方案使得取到的物品的w之和为m,t之和为s。如果存在方案还要输出任意一组解。
比赛的时候非常痛快地用尽人家服务器给的10S----TLE不知道几次。最后无果只能放弃。
对于方案的判断,我们可以简单的用01表示是否存在,但是题目数据量非常大,常规的做法的计算量是400 * 200000 * 50,10S肯定不够。比赛那会儿有想过位运算,最终还是没有写出来。
由于w只有50,可以用一个64位整型表示对应的值是否存在。dp[j]表示t的和为j时,可以对应到的m的和的集合。
那么对于新的物品w=x, t=y,dp[j+y] = dp[j+y] | (dp[j]<<w),状态转移还是好做的。关键是路径记录。
比赛的时候想到的是遍历当前状态集合,可是遍历还是相当于写个for循环,复杂度还是不对啊。
后来才想到,不要遍历当前集合,只是找到新状态与旧状态之间的差异,针对那几个新增的位做记录。这样的话,每个位置最多被更新一次。
然后对于每个位的判断,可以树状数组求最低位的做法,x&(-x),找到最低位的1,然后事先搞个map映射下就ok了,算完就把这个位去掉。
这里记录好了后面输出路径也就简单了。
#include<cstdio>
#include<cstring>
#include<map>
using namespace std;
typedef long long LL;
LL dp[200001];
int sel[200001][51];
int n, q, w[410], t[410], m[410], s[410];
map<LL, int> MP;
int main(){
for(int i=0; i<=50; i++){
MP[1LL<<i] = i;
}
int T;
scanf("%d", &T);
LL mask = (1LL<<51)-1;
while(T--){
scanf("%d %d", &n, &q);
for(int i=1; i<=n; i++) scanf("%d %d", w+i, t+i);
int S = 0;
for(int i=1; i<=q; i++){
scanf("%d %d", m+i, s+i);
if(s[i]>S) S = s[i];
}
memset(dp, 0, sizeof(dp));
memset(sel, 0, sizeof(sel));
dp[0] = 1;
for(int i=1; i<=n; i++){
for(int j=S; j>=t[i]; j--){
LL st = dp[j]|((dp[j-t[i]]<<w[i])&mask);
LL c = dp[j]^st;
dp[j] = st;
while(c){
LL low = c&(-c);
int p = MP[low];
if(p>50) break;
c ^= low;
sel[j][p] = i;
}
}
}
for(int i=1; i<=q; i++){
if(sel[s[i]][m[i]]){
int x=s[i], y=m[i];
int z;
while(x){
if(x!=s[i]) putchar(' ');
z = sel[x][y];
printf("%d", z);
x -= t[z];
y -= w[z];
}
puts("");
}
else{
puts("No solution!");
}
}
}
return 0;
}