To boldly go where no one has gone before.

题解 P1049 【装箱问题】

2018-12-01 01:53:08


我要用模拟退火来做这道题

这道题真的太没意思了,模板3分钟敲完查错都不查都能AC。我不会告诉你我第一次交测评把数据范围看错了甚至丢了20分 所以我决定弄点其他好玩的。之前见过有仁兄直接用O(1e6)的随机跑dp得了90分,这件事给我留下了深刻的印象。然后,我受到了极大的启发。

考虑一下背包问题,我们可以很容易地发现其可以与模拟退火相类比。 至于什么是模拟退火,我太懒了无法在这里详细讲解,所以请移步吊打XXX的题解,以及各大牛的CSDN博客等。这里用模拟退火做的正确性是显然的

简而言之,模拟退火就是一种对贪心的优化。贪心是贪心,如果我们偶尔不贪心一下,或许就可以跳出局部最优解,然后一步步逼近全局最优解了。比如我们的背包问题,如果你随机到一个东西它放过了,如果纯粹靠贪心的话你肯定不会把它拿出看来。然而我们的模拟退火会以一定的概率把东西给拿出来,以寻找更好的解,这个概率是e^(-dE/kT)。具体怎么来的,你可以查询热力学的相关资料。反正就算我不知道是怎么来的直接用好了。

反正呢,这道题的数据范围只有30对不对,就算是面对30!的可能结果数,模拟退火依然可以坚挺如故,如果你参数调得好甚至可以直接拿去做MAXN=300的dp题。总之,模拟退火面对这种可以用能量类比的问题的时候总是能表现出无敌的兼容性,并且靠它无比玄学的复杂度鹤立鸡群,傲立于众TLE之中。

注意,我的用词是无比玄学的复杂度,在模拟退火里头你甚至可以利用clock()卡时间以保证不会超时,或者保证进行足够次的随机,以便更精确地找到最值。比如卡个700ms,再怎么也不会TLE了 XD


下面是代码。

#include<bits/stdc++.h>
#define MAXN 40 //第一次把MAXN看成20,结果WA了20分orz
#define Tk 0.99789 //降温系数,可调,调得好可上天入地
#define rd (rand() % n + 1)
using namespace std;

int v[MAXN];
int V, n, ans = 0, tot = 0;
bool vis[MAXN];
double T = 1926; //初温,大部分模拟退火用这个初温都能AC

bool accept(int del) {
    return ((del>0)||exp(del/T) > (double)rand()/RAND_MAX);
} //转移概率表达式

int main() {
    srand(time(0));
    scanf("%d%d", &V ,&n);
    for(int i=1; i<=n; i++) 
        scanf("%d", &v[i]);
    int a;
    memset(vis, 0, sizeof(vis));
    while(T > 1e-14) {
        ans = ans<tot ? tot : ans; //维护最优答案,以防非酋情况发生
        a = rd; //进行随机
        int dE = v[a];
        if(vis[a]) dE *= -1; //产生能量差
        if(accept(dE)) { //以概率发生转移
            if(vis[a]) {
                vis[a] = false;
                tot -= v[a];
            }else{
                if(tot + v[a] > V) continue;
                vis[a] = true;
                tot += v[a];
            }
        }
        T *= Tk; //降温
    }
    cout << V - ans;
    return 0;
}

洗把脸就AC了(逃