什么是禁忌搜索?
禁忌搜索(Tabu Search,TS,又称禁忌搜寻法)是一种现代启发式算法,由美国科罗拉多大学教授Fred Glover在1986年左右提出的,是一个用来跳脱局部最优解的搜索方法。其先创立一个初始化的方案;基于此,算法“移动”到一相邻的方案。经过许多连续的移动过程,提高解的质量。
算法简介
禁忌(Tabu Search)算法是一种亚启发式(meta-heuristic)随机搜索算法,它从一个初始可行解出发,选择一系列的特定搜索方向(移动)作为试探,选择能够实现让特定的目标函数值变化最多的移动。为了避免陷入局部最优解,TS搜索中采用了一种灵活的“记忆”技术,对已经进行的优化过程进行记录和选择,指导下一步的搜索方向,这就是禁忌表的建立。
TS是人工智能的一种体现,是局部领域搜索的一种扩展。是在领域搜索的基础上,通过设置禁忌表来禁忌一些已经历的操作,并利用藐视准则来奖励一些优良状态,其中涉及邻域(neighborhood)
、禁忌表(tabu list)
、禁忌长度(tabu length)
、候选解( candidate )
、特赦准则(candidate)
等影响禁忌搜索算法性能的关键因素。迄今为止,TS算法在组合优化生产调度、机器学习、电路设计和神经网络等领域取得了很大的成功,近年来又在函数全局优化方面得到较多的研究,并大有发展的趋势。
原理解释
- 兔子们知道一个兔的力量是渺小的。他们互相转告着,哪里的山已经找过,并且找过的每一座山他们都留下一只兔子做记号。他们制定了下一步去哪里寻找的策略。禁忌搜索算法如图所示
-
兔子们找到了泰山,它们之中的一只就会留守在这里,其他的再去别的地方寻找。当兔子们再寻找时,一般地会有意识地避开泰山,因为他们知道,这里已经找过,并且有一只兔子在那里看着了。这就是禁忌搜索中“
禁忌表(tabulist)
”的含义。 -
那只留在泰山的兔子一般不会就安家在那里了,它会在一定时间后重新回到找最高峰的大军,因为这个时候已经有了许多新的消息。这个归队时间,在禁忌搜索里面叫做“
禁忌长度(tabu length)
”; -
如果在搜索的过程中,留守泰山的兔子还没有归队,但是找到的地方全是华北平原等比较低的地方,兔子们就不得不再次考虑选中泰山,泰山毕竟也有一个不错的高度,也就是说,当一个有兔子留守的地方优越性太突出,超过了“best so far”“的状态,就可以不顾及有没有兔子留守,都把这个地方考虑进来,这就叫“
特赦准则(aspiration criterion)
”
这三个概念就是禁忌搜索和一般搜索准则最不同的地方,算法的优化也关键在这里。
构成要素
-
评价函数 (Evaluation Function)
:评价函数是用来评价邻域中的邻居、判断其优劣的衡量指标。大多数情况下,评价函数为目标函数。但自定义的形式也可存在,算法也可使用多个评价函数,以提高解的分散性(区分度)。 -
邻域移动(Move Operator)
:邻域移动是进行解转移的关键,又称“算子”,影响整个算法的搜索速度。邻域移动需要根据不同的问题特点来自定义,而整个邻近解空间是由当前解通过定义的移动操作构筑的所有邻域解构成的集合。 -
禁忌表(Tabu Table)
:禁忌表记录被禁止的变化,以防出现搜索循环、陷入局部最优。对其的设计中最关键的因素是禁忌对象(禁忌表限定的对象)和禁忌步长(对象的禁忌在多少次迭代后失效)禁忌表是禁忌搜索算法的核心,禁忌表的对象、步长及更新策略在很大程度上影响着搜索速度和解的质量。若禁忌对象不准确或者步长过小,算法避免陷入局部最优的能力会大打折扣;若禁忌表步长过大,搜索区域将会限制,好的解就可能被跳过。)。 -
禁忌长度
:禁忌表中所能接受的最多禁忌对象的数量。 -
禁忌步长
:对象的禁忌在多少次迭代后失效。
算法流程
例题
禁忌搜索用的比较少,模拟退火比较多
一般来说禁忌搜索的题目模拟退火都可以做(其实我是在模拟退火的题目上找几个能够用禁忌搜索来做的,貌似我还没看到有人用禁忌搜索解题)。
最短Hamilton路径
题目大意:
给定一张
n
n
n个点的带权无向图,点从
0
∼
n
−
1
0∼n−1
0∼n−1 标号,求起点
0
0
0 到终点
n
−
1
n−1
n−1 的最短 Hamilton 路径。(Hamilton 路径的定义是从
0
0
0 到
n
−
1
n−1
n−1 不重不漏地经过每个点恰好一次。)
#include<bits/stdc++.h>
using namespace std;
const int N=1010;
const int L=5,R=10; //禁忌步长
int tabu[N];//禁忌表
int w[N];
int dis[50][50];
int ans;
int n;
int fun(){ //评价函数
int res=0;
for(int i=0;i<n-1;i++)
res+=dis[w[i]][w[i+1]];
return res;
}
//禁忌搜索框架
void solve(){
memset(tabu,0,sizeof(tabu));
random_shuffle(w+1,w+n-1); //打乱位置
//领域移动
for(int t=1;t<=50;t++){
int tmp=INT_MAX;
int nx,ny;
for(int i=1;i<n-1;i++)
for(int j=i+1;j<n-1;j++){
if(tabu[i*20+j]>t) continue; //在禁忌步长内会失效,对象(i,j)的禁忌在多次迭代后失效
swap(w[i],w[j]); //交换节点
int now=fun();
if(now<tmp){
tmp=now;
nx=i,ny=j;
}
swap(w[i],w[j]); //回溯
}
if(tmp!=INT_MAX){
tabu[nx*15+ny]=t+rand()%(R-L+1)+L; //放入禁忌表中
ans=min(ans,tmp); //更新当前解
swap(w[nx],w[ny]);
}
}
}
int main(){
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++) cin>>dis[i][j];
for(int i=0;i<n;i++) w[i]=i; //对应节点的下标
ans=fun();//随便去一个可行解
//迭代多次取最优解
for(int i=0;i<500;i++) solve();
cout<<ans<<endl;
return 0;
}
两个数组最小的异或值之和
题目大意:
给定两个长度一样的数组A,B,你可以随便排列A,B,使得他们的异或
A
[
1
]
A[1]
A[1]xor
B
[
1
]
B[1]
B[1],…,
A
[
n
]
A[n]
A[n]xor
B
[
n
]
B[n]
B[n]的和最小
当然题目的正确解法是DP,最大完美匹配KM算法。
这里本着乱搞的精神,用智能算法将它的后台数据A了。
这题暴力的做法时枚举A或B的全排列然后取最小。
这个是一个组合优化的问题,对于这个排列解是个多峰分布的,这题当然可以用模拟退火过。
联想兔子爬山的解释,用禁忌搜索框架求解。
const int L=5,R=10;
int tabu[500];
class Solution {
public:
vector<int> a,b;
int n;
int fun(){
int res=0;
for(int i=0;i<n;i++) res+=(a[i]^b[i]);
return res;
}
int ans;
void solve(){
ans=fun();
memset(tabu,0,sizeof(tabu));
for(int t=1;t<=500;t++){
int tmp=INT_MAX;
int nx=-1,ny=-1;
for(int i=0;i<n;i++){
for(int j=i+1;j<n;j++){
if(tabu[i*15+j]>t) continue;
swap(b[i],b[j]);
int now=fun();
if(now<tmp){ //评价
tmp=now;
nx=i;
ny=j;
}
swap(b[i],b[j]);
}
}
if(tmp!=INT_MAX){
tabu[nx*15+ny]=t+rand()%(R-L+1)+L;
ans=min(ans,tmp);
swap(b[nx],b[ny]);
}
}
}
int minimumXORSum(vector<int>& nums1, vector<int>& nums2) {
n=nums1.size();
for(int i:nums1) a.push_back(i);
for(int i:nums2) b.push_back(i);
solve();
return ans;
}
};
[TJOI2010]分金币
**题目大意:**现在有 n n n枚金币,它们可能会有不同的价值,第 i i i枚金币的价值为 v i v_i vi
现在要把它们分成两部分,要求这两部分金币数目之差不超过1,问这样分成的两部分金币的价值之差最小是多少?
因为本题是要求平分,分成两个组别,每次在把一个组和另一个组的金币交换。
这又是一个排列优化问题。(无脑禁忌搜索…)
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int L=5,R=10;
const int N=1000;
int tabu[N];
int n;
int w[N];
ll a[N];
ll fun(){
ll res=0;
for(int i=0;i<n;i++){
if(i<n/2) res+=a[w[i]];
else res-=a[w[i]];
}
return abs(res);
}
ll ans;
void solve(){
random_shuffle(w,w+n);
memset(tabu,0,sizeof(tabu));
for(int t=1;t<=50;t++){
ll tmp=LONG_LONG_MAX;
int nx,ny;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
if(tabu[i*n+j]>t) continue;
swap(w[i],w[j]);
ll now=fun();
if(now<tmp){
tmp=now;
nx=i,ny=j;
}
swap(w[i],w[j]);
}
}
if(tmp!=LONG_LONG_MAX){
tabu[nx*n+ny]=t+rand()%(R-L+1)+L;
swap(w[nx],w[ny]);
ans=min(ans,tmp);
}
}
}
int main(){
int t;
cin>>t;
while(t--){
cin>>n;
for(int i=0;i<n;i++) {
cin>>a[i];
w[i]=i;
}
ans=LONG_LONG_MAX;
for(int i=0;i<30;i++)
solve();
cout<<ans<<endl;
}
return 0;
}
完结撒花!