Zombie’s Treasure Chest
题目链接
https://cn.vjudge.net/problem/UVA-12325
题意
两种物品无穷多个,第一种物品重量 s 1 s_1 s1,价值 v 1 v_1 v1,第二种物品重量 s 2 s_2 s2,价值 v 2 v_2 v2,背包重 n n n,求能装的最大价值之和. 数据全都是 2 e 9 2e9 2e9.也就是两种物品的完全背包.
题解
不可思议吧,这题还能模拟退火?
但仔细一想,求解最优值,而且随机解的生成也很简单,当然可以吗,模拟退火搞啦.
模拟退火的板子可以到我以前的博客里找到,现在默认大家都知道模拟退火怎么写了.
这道题我虽然用模拟退火 A A A掉了,但也尝试了好多发,现在把我采坑的过程根大家分享一下.
尝试一
直接套板子,设 x x x为第一种物品取的个数,显然 x ∈ [ 0 , ⌊ n s 1 ⌋ ] x \in [0,\lfloor \frac{n}{s_1} \rfloor] x∈[0,⌊s1n⌋],那么第二种物品的个数就是 y = ⌊ n − s 1 ∗ v 1 s 2 ⌋ y =\lfloor \frac{n-s_1*v_1}{s_2} \rfloor y=⌊s2n−s1∗v1⌋.
因此模拟退火的时候我可以在区间 [ 0 , ⌊ n s 1 ⌋ ] [0,\lfloor \frac{n}{s_1} \rfloor] [0,⌊s1n⌋]中随机一个数作为 x x x,然后计算 y y y,并且计算能量值 E = x ∗ v 1 + y ∗ v 2 E = x*v_1+y*v_2 E=x∗v1+y∗v2.
最后调调参数,使得平衡一下答案精度和时间复杂度.
尝试结果
多次尝试以后,一直WA,自己造了组极端数据 2000000000 , 2 , 3 , 1 , 1 2000000000,2,3,1,1 2000000000,2,3,1,1,发现根本过不去,总结原因:当随机区间过大的时候,很难随机到正确解,所以算法就在某个半山腰停住了.
尝试二
要想能想要枚举到最优解,区间一定不能太大.我们可以分块进行模拟退火,这样可以保证每次随机的区间不会太大,区间上的某一个点被随机到的概率就更大了,这种做法我还没有试过,但是感觉应该可行.我们进一步发现,这个函数的峰不会太多(实际没几个)大致是具有单调性质的,因此我们采用二分区间的做法,即对于当前区间,用模拟退火算出一个最优解,然后用这个解与区间中点做比较从而确定下一个需要进行模拟退火的区间.
通过多次调参之后:
代码
#include <iostream>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <cstring>
int T,cas;
long long n,s1,v1,s2,v2;
double randfloat() {
return rand()/(RAND_MAX+0.0);
}
void solve() {
std::cin >> n >> s1 >> v1 >> s2 >> v2;
long long ansE = 0,ansx = 0;
long long nowE = 0,x = 0;
long long low = 0,up = n/s1;
for(int cc = 1;cc <= 20;++cc) {
double T0 = 1000000,Tk = 1,T = T0,d = 0.999;
while(T > Tk) {
long long newx = low + rand()%(up-low+1);
long long newE = newx * v1 + ((n-newx*s1)/s2)*v2;
if(newE < nowE || randfloat() > exp((nowE-newE)/T)) {
nowE = newE;
x = newx;
}
T *= d;
if(newE > ansE) {
ansE = newE;
ansx = newx;
}
}
long long mid = (low + up)/2;
if(ansx > mid) low = mid;
else up = mid;
}
std::cout << "Case #" << ++cas << ": " << ansE << std::endl;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin >> T;
while(T--) solve();
return 0;
}