先看一道題目:洛谷P3959 寶藏
第一想法是最小生成樹,但是並不對,只能過40%的數據。
n<=12?想起了TSP/狀壓DP。
(不知道TSP問題戳這里。)
用 f[u][i] 表示從 u 出發,點是否可以到達的狀態為 i 。
(因為計算 f[u][i] 時只用 f[u] 里的狀態計算,可以算用了滾動數組,省略一維的 u )。
然后枚舉點 j,k,如果 j 和 k 有直接連邊,且 j 能到達,k 不能到達的話,f[ i+(1<<k) ] = min( f[ i ] + deep[ i ][ j ] * a[ j ][ k ])。
其中 deep[ i ][ j ] 表示在狀態為i的情況下, j 離初始點的距離。
這樣基本的DP模型就出來了。
但是 deep[ i ]怎么求呢?
可以在dp的過程中做,如果 i+(1<<j) 的狀態最優從 i 來,那么 deep[ i+(1<<j) ] = deep[ i ]
(除了 j 點以外,因為 j 點是新加入的。)
附代碼:
#include<iostream> #include<algorithm> #include<cstdio> #include<cstring> #define MAXN 15 #define MAX 999999999 using namespace std; int n,m,S,ans=MAX; int a[MAXN][MAXN],f[1<<MAXN],deep[MAXN][1<<MAXN]; inline int read(){ int date=0,w=1;char c=0; while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();} while(c>='0'&&c<='9'){date=date*10+c-'0';c=getchar();} return date*w; } int dp(int rt){ memset(deep,0,sizeof(deep)); memset(f,(MAX/1000),sizeof(f)); f[1<<rt]=0;deep[rt][1<<rt]=1; for(int s=0;s<=S;s++) for(int i=0;i<n;i++) if(s&(1<<i)) for(int j=0;j<n;j++) if(!(s&(1<<j))&&a[i][j]!=MAX&&a[i][j]!=0){ int u=(1<<j),x=f[s]+deep[i][s]*a[i][j]; if(f[s+u]>x){ f[s+u]=x; for(int k=0;k<n;k++)deep[k][s+u]=deep[k][s]; deep[j][s+u]=deep[i][s]+1; } } return f[S]; } int main(){ int u,v,w; n=read();m=read(); S=(1<<n)-1; for(int i=0;i<=n;i++) for(int j=0;j<=n;j++) a[i][j]=(i==j)?0:MAX; for(int i=1;i<=m;i++){ u=read()-1;v=read()-1;w=read(); if(a[u][v]>w)a[u][v]=a[v][u]=w; } for(int i=0;i<n;i++)ans=min(ans,dp(i)); printf("%d\n",ans); return 0; }
這樣程序的時間復雜度為O(2^n*n^4)。
這樣看上去會超時。。。
但是因為狀壓DP會有很多可以忽略的狀態的特性,時間復雜度遠遠沒有這么高,是可以過的。
如果真的不能過呢?(其實被卡了也就80~90分,還有個10來分遇到NOIP這種情況就算了吧。。。)
沒關系,我們還有一個殺手鐧——隨機化算法:模!擬!退!火!
先講一個前置算法:爬山算法
爬山算法,即 Hill Climbing,簡稱HC。
爬山算法每次在當前找到的方案附近尋找一個新的方案(常見方式是隨機一個差值),然后如果這個解更優那么直接轉移。
對於單峰函數來說這顯然可以直接找到最優解(不過你都知道它是單峰函數了為啥不三分呢?)
但是,假如是下面這個圖呢?
我們發現爬山算法會卡在某個“最高點”的牢籠中,無法跳出,這時候,模擬退火的優點就體現了出來。
模擬退火:
模擬退火,即 Simulated Annealing,簡稱SA。
模擬退火算法可以有效的解決這個陷入局部最優解的問題從而找到一個全局最優解。
實際上模擬退火算法也是貪心算法,只不過它在這個基礎上增加了隨機因素。
這個隨機因素就是:以一定的概率來接受一個比單前解要差的解。
通過這個隨機因素使得算法有可能跳出這個局部最優解。
然后你就可以不用管什么起源啦、參數啦啥的,只要知道退火過程就行了:
1、初始化:設置參數(初始溫度T、終止條件T<1,衰減函數T=a*T,Mapkob鏈長)
2、在給定溫度下,不斷產生新解
3、降低溫度T,回到第二步
4、結束
基本上就是這個圖(來自Wikipedia):
所以,上面的那題就可以隨便跑了。。。
但是,這樣如何保證得到全局最優解呢?有的時候得到的結果還不如貪心呢
是的,一次運行的結果並不一定最優。
但是,模擬退火的時間復雜度與貪心相同,運行幾千次甚至上萬次的時間都是可承受的,運行這么多次,得到最優解的幾率非常大。
所以這個算法經常用於騙分。。。
附代碼:
#include<iostream> #include<algorithm> #include<cstdio> #include<cstring> #include<queue> #define MAXN 15 #define MAXM 1010 #define MAX 2147483646 using namespace std; int n,m; int deep[MAXN],a[MAXN][MAXN]; bool vis[MAXN]; struct node{ int x,to; bool operator <(const node &p)const{ return deep[x]*a[x][to]>deep[p.x]*a[p.x][p.to]; } }; inline int read(){ int date=0,w=1;char c=0; while(c<'0'||c>'9'){if(c=='-')w=-1;c=getchar();} while(c>='0'&&c<='9'){date=date*10+c-'0';c=getchar();} return date*w; } int solve(int rt){ int cost=0,top=0; node u,v,past[MAXM]; memset(deep,0,sizeof(deep)); memset(vis,false,sizeof(vis)); priority_queue<node> q; deep[rt]=1; vis[rt]=true; for(int i=1;i<=n;i++) if(a[rt][i]!=MAX){ u.x=rt;u.to=i; q.push(u); } for(int i=2;i<=n;i++){ u=q.top(); q.pop(); while(!q.empty()&&(vis[u.to]||rand()%n<1)){ if(!vis[u.to])past[++top]=u; u=q.top(); q.pop(); } vis[u.to]=true; deep[u.to]=deep[u.x]+1; while(top)q.push(past[top--]); top=0; for(int i=1;i<=n;i++) if(a[u.to][i]!=MAX&&!vis[i]){ v.x=u.to;v.to=i; q.push(v); } cost+=a[u.x][u.to]*deep[u.x]; } return cost; } void work(){ int ans=MAX; for(int cases=1;cases<=1000;cases++) for(int i=1;i<=n;i++) ans=min(ans,solve(i)); printf("%d\n",ans); } void init(){ int u,v,w; srand(2002); n=read();m=read(); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) a[i][j]=MAX; for(int i=1;i<=m;i++){ u=read();v=read();w=read(); a[u][v]=a[v][u]=min(w,a[u][v]); } } int main(){ init(); work(); return 0; }
最后一點:參數調整
啊,這個問題沒有什么好的路子走,只有一條路——對拍!
其實隨機種子是多少不是什么問題,畢竟這是一個騙分算法。。。
所以這時候就要看你的信息直(ren)覺(pin)了。。。
(據說把隨機種子調成女友生日會有神秘彩蛋哦!)
后記:
附上洛谷的兩次提交:
狀壓DP: Accepted 100
152ms / 4.08MB
代碼:1.1KB C++
模擬退火:Accepted 100
280ms / 2.21MB
代碼:1.61KB C++
相比之下,模擬退火的思維難度遠遠低於狀壓DP,而性能只比狀壓DP差一點,所以還是很強的。
如果有什么題目在考場上實在寫不出來,就用模擬退火騙騙分吧!