这里简单介绍下局部搜索算法。
局部搜索算法是对一个或多个状态进行评价和修改,而不是系统地从初始状态开始的路径。这些算法适用于关注那些关注解状态而不是路径代价的问题。
局部搜索算法从单个当前节点出发,通常只移动到它的临近状态,一般保存搜索路径。
局部搜素不是系统化的,但有两个关键的优点。
1. 只用很少的内存——通常是常数
2. 经常能在系统化算法不适用的很大或无限的状态空间中找到合理的解。
爬山法(最陡上升版本)搜索,简单的循环过程,不断向值增加的方向持续移动。算法在到达一个“峰顶”时终止,邻接状态中没有比它值更高的。
爬山法伪代码如下所示:
以八皇后问题为例说明爬山法。局部搜索算法一般使用完整状态形式化,即每个状态都包括在棋盘上放置8个皇后,每行各一个。后继函数指移动某个皇后到这行的另一个可能方格中,因此有7*8=56个后继。启发式评估函数h是形成互相攻击的皇后对的数量,不管是直接还是间接。在每一步移动之前,先计算棋盘上所有后继状态的启发式评估函数值h。然后选择h最小的那个后继状态,这里也就是选出移动后棋盘上皇后对冲突总个数最少的一个后继状态。
爬山法有时被称为贪婪局部搜索,因为它只是选择邻居中状态最好的一个,而不考虑下一步该如何走。尽管贪婪是七宗罪之一,贪婪算法却很有效。爬山法很快朝着解的方向进展,因为它可以很容易地改善一个坏的状态。不幸的是,爬山法经常会陷入困境。
在每种情况下,爬山法都会到达无法再取得进展的地点。从随机生成的八皇后问题开始,最陡上升的爬山法86%的情况下会被卡住,只有14%的问题实例能求得解。算法求解的速度快,成功找到解的平均步数是4步,被卡住的平均步数是3步。(这是理论值,幸运的是,后面的运行结果可以发现与理论结果一致)
爬山法有很多变形。
随机爬山法在上山移动中随机地选择下一步,被选中的概率随着上山移动的陡峭程度不同而不同。这种算法通常比最陡上升算法的收敛速度慢不少。
首选爬山法实现了随机爬山法,随机地生成后继节点直到生成一个优于当前节点的后继。这个算法在后继节点很多的时候是个好策略。
随机重启爬山法通过随机生成初始状态来导引爬山法搜索,直到找到目标。这个算法完备的概率接近于1,因为它最终会生成一个目标状态作为初始状态。
现在再来讲下模拟退火搜索算法。
模拟退火算法的内层循环与爬山法类似,只是它没有选择最佳移动,而是随机移动。如果该移动使情况改善,该移动则被接收。否则,算法以某个小于1的概率接收该移动。如果移动导致状态“变坏”,概率则成指数级下降——评估值△E变坏。这个概率也随“温度”T降低而下降:开始T高的时候可能允许“坏的”移动,T越低则越不可能发生。如果调度让T下降得足够慢,算法找到最优解的概率逼近于1。
伪代码如下所示:
对于八数码问题,可以选择很多因素作为评估函数。这里我选取的是曼哈顿距离,每次移动都是往曼哈顿距离减少得最少的方向移动。
八数码代码实现如下:
#include
#include
#include
#include
#include
#include
using namespace std;
int direction[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 右下左上
int current[3][3]; // 当前状态
int row_0, col_0; // 记录0的坐标
int totalTrial; // 统计移动步数
int Manhattan() { // 计算曼哈顿距离
int sum = 0;
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (current[i][j] == 0) continue;
int row = current[i][j]/3;
int col = current[i][j]%3;
int distance = abs(row - i) + abs(col - j);
sum += distance;
}
}
return sum;
}
void print() {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j)
cout << current[i][j] << " ";
cout << endl;
}
cout << endl;
}
void initial() {
for (int i = 0; i < 3; ++i) { // 初始状态为目标状态
for (int j = 0; j < 3; ++j) {
current[i][j] = i*3+j;
}
}
row_0 = 0, col_0 = 0;
int last = -1; // 上一次移动方向
for (int i = 0; i < 20; i++) { // 随机打乱
bool upset = false;
while (!upset) { // 打乱成功才跳出循环
int dir = rand() % 4; // 随机选取一个方向
if (last != -1 && last != dir && abs(last-dir) == 2) continue; // 避免反向走
int x = row_0 + direction[dir][0];
int y = col_0 + direction[dir][1];
if (x >= 0 && x < 3 && y >= 0 && y < 3) { // 方向可行
swap(current[row_0][col_0], current[x][y]); // 交换0和相邻数字的位置
row_0 = x, col_0 = y; // 更新0的坐标
last = dir; // 更新此次移动方向
upset = true; // 标记打乱成功
}
}
}
}
// 判定是否有解
bool check() {
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j)
if (current[i][j] != i*3+j)
return false;
}
return true;
}
// 爬山法
bool hillClimbing() {
for (int trial = 0; trial < 200; trial++) {
int curManha = Manhattan(); // 当前状态
int minMan = 99999, minX = 0, minY = 0;
for (int i = 0; i < 4; i++) { // 在后继状态中找最小值
int x = row_0 + direction[i][0];
int y = col_0 + direction[i][1];
if (x >= 0 && x < 3 && y >= 0 && y < 3) { // 方向可行
swap(current[row_0][col_0], current[x][y]); // 交换0和相邻位置
int nextManha = Manhattan();
if (nextManha < minMan) { // 获取下一状态的最小值
minMan = nextManha;
minX = x, minY = y;
}
swap(current[x][y], current[row_0][col_0]); // 复原0和相邻位置
}
}
if (curManha > minMan) { // 最小值优于当前状态
swap(current[row_0][col_0], current[minX][minY]);
row_0 = minX, col_0 = minY;
}
if (check()) { // 成功找到解
totalTrial += trial;
return true;
}
}
return false;
}
// 首选爬山法
bool firstchose() {
for (int trial = 0; trial < 500; trial++) {
// 随机选取第一个优于当前状态的下一步
bool next = false;
int times = 0;
while (!next) {
int dir = rand() % 4;
int curManha = Manhattan();
int x = row_0 + direction[dir][0];
int y = col_0 + direction[dir][1];
if (x >= 0 && x < 3 && y >= 0 && y < 3) { // 方向可行
swap(current[row_0][col_0], current[x][y]);
int nextManha = Manhattan();
if (nextManha < curManha) {
row_0 = x, col_0 = y;
next = true;
} else {
swap(current[x][y], current[row_0][col_0]);
}
}
if (++times > 20) break;
}
if (check()) { // 成功找到解
totalTrial += trial;
return true;
}
}
return false;
}
// 模拟退火算法
bool simulated() {
double temperature = 5; // 初始温度
int trial = 0;
while (temperature > 0.00001) {
vector<int> v; // 选出可行的方向
for (int i = 0; i < 4; i++) {
int x = row_0 + direction[i][0];
int y = col_0 + direction[i][1];
if (x >= 0 && x < 3 && y >= 0 && y < 3) { // 方向可行
v.push_back(i);
}
}
int curManha = Manhattan(); // 当前状态的曼哈顿距离之和
int dir = v[rand() % v.size()]; // 随机选取一个可行方向
int x = row_0 + direction[dir][0];
int y = col_0 + direction[dir][1];
swap(current[row_0][col_0], current[x][y]); // 交换0和相邻节点的位置
int nextManha = Manhattan(); // 交换之后的曼哈顿距离之和
int E = nextManha - curManha;
if (E < 0) { // 下一状态优于当前状态
row_0 = x, col_0 = y; // 更新0的位置
trial++;
} else if (exp((-1)*E/temperature) > ((double)(rand() % 1000) / 1000)) { // 以一定的概率选取
row_0 = x, col_0 = y;
trial++;
} else { // 不成功的话,复原0和相邻节点的位置
swap(current[x][y], current[row_0][col_0]);
}
temperature *= 0.999; // 温度下降
if (check()) { // 成功找到解
totalTrial += trial;
return true;
}
}
return false;
}
// 最陡上升爬山法
int steepestAscent() {
int count = 0;
for (int i = 0; i < 1000; i++) {
initial();
if (hillClimbing())
count++;
}
return count;
}
// 首选爬山法
int firstChose() {
int count = 0;
for (int i = 0; i < 1000; i++) {
initial();
if (firstchose())
count++;
}
return count;
}
// 随机重新开始爬山法
int randomRestart() {
bool find = false;
while (!find) {
initial();
find = hillClimbing();
}
return find;
}
// 模拟退火搜索
int simulatedAnnealing() {
int count = 0;
for (int i = 0; i < 1000; i++) {
initial();
if (simulated())
count++;
}
return count;
}
int main(int argc, char const *argv[]) {
srand((int)time(0));
totalTrial = 0;
cout << "steepest-Ascent searching..." << endl;
int count = steepestAscent();
cout << "steepest-Ascent[success/total]: " << count << "/1000" << endl;
cout << "average steps: " << totalTrial/count << endl;
totalTrial = 0;
cout << "random-Restart searching..." << endl;
int count2 = randomRestart();
cout << "random-Restart: " << count2 << endl;
cout << "average steps: " << totalTrial/count2 << endl;
totalTrial = 0;
cout << "first-Chose searching..." << endl;
int count3 = firstChose();
cout << "first-Chose[success/total]: " << count3 << "/1000" << endl;
cout << "average steps: " << totalTrial/count3 << endl;
totalTrial = 0;
cout << "simulated-Annealing searching..." << endl;
int count4 = simulatedAnnealing();
cout << "simulated-Annealing[success/total]: " << count4 << "/1000" << endl;
cout << "average steps: " << totalTrial/count4 << endl;
return 0;
}
八皇后代码实现如下:
#include
#include
#include
#include
// #include
#include
using namespace std;
int queens[8][8]; // 8*8棋盘
int temp[8][8];
int totalTrial; // 统计移动步数
// 随机生成初始状态
void initial() {
for (int i = 0; i < 8; ++i) {
for (int j = 0; j < 8; j++) {
queens[i][j] = 0;
}
}
for (int i = 0; i < 8; i++) {
int num = rand() % 8;
queens[i][num] = 1;
}
}
void print() {
for (int i = 0; i < 8; ++i) {
for (int j = 0; j < 8; j++)
cout << queens[i][j] << " ";
cout << endl;
}
}
// 统计在该位置下所有皇后的冲突个数
int findCollision(int row, int col) {
int count = 0;
// 该位置为1
temp[row][col] = 1;
for (int k = 0; k < 64; k++) {
if (temp[k/8][k%8] == 1) {
for (int i = 0; i < 8; i++) // 同一列
if (i != k/8 && temp[i][k%8] == 1)
count++;
for (int i = k/8, j = k%8; i < 8 && j < 8; i++, j++) // 右下方
if (i != k/8 && temp[i][j] == 1)
count++;
for (int i = k/8, j = k%8; i >= 0 && j >= 0; i--, j--) // 左上方
if (i != k/8 && temp[i][j] == 1)
count++;
for (int i = k/8, j = k%8; i < 8 && j >= 0; i++, j--) // 左下方
if (i != k/8 && temp[i][j] == 1)
count++;
for (int i = k/8, j = k%8; i >= 0 && j < 8; i--, j++) // 右上方
if (i != k/8 && temp[i][j] == 1)
count++;
}
}
temp[row][col] = 0; // 复原位置
return count/2;
}
bool check(int h[8][8]) {
for (int i = 0; i < 8; i++) {
bool flag = false;
for (int j = 0; j < 8; j++) {
if (queens[i][j] == 1 && h[i][j] == 0) { //皇后所在位置没有冲突
flag = true;
break;
}
}
if (!flag) { // 皇后所在位置仍有冲突,还需要继续查找
return false;
}
}
return true;
}
int ct = 0;
// 爬山法
bool hillClimbing() {
// 尝试次数大于100则判定为无解
for (int trial = 0; trial <= 100; trial++) {
// 拷贝原始棋盘数据到temp
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
temp[i][j] = queens[i][j];
}
}
int h[8][8];
int minH = 9999, minX = 0, minY = 0, curState;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
// 在计算h(i, j)之前,对i行所有位置赋值为0
for (int k = 0; k < 8; k++)
temp[i][k] = 0;
// 查找h(i, j)
h[i][j] = findCollision(i, j);
// 当前状态的h值
if (queens[i][j] == 1) {
curState = h[i][j];
}
// 先找出冲突个数最小的位置
if (h[i][j] < minH) {
minH = h[i][j];
minX = i;
minY = j;
}
// 计算h(i,j)之后要复原数据,避免计算错误
for (int k = 0; k < 8; k++)
temp[i][k] = queens[i][k];
}
}
// 将皇后放在该行冲突最少的位置处
if (curState > minH) {
for (int i = 0; i < 8; i++)
queens[minX][i] = 0;
queens[minX][minY] = 1;
}
// 判断是否找到解, 有解则返回值为真
if (check(h)) {
totalTrial += trial;
return true;
}
}
return false;
}
// 首选爬山法
bool firstchose() {
// 尝试次数大于100则判定为无解
for (int trial = 0; trial <= 100; trial++) {
// 拷贝原始棋盘数据到temp
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
temp[i][j] = queens[i][j];
}
}
int h[8][8], curState;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
// 在计算h(i, j)之前,对i行所有位置赋值为0
for (int k = 0; k < 8; k++)
temp[i][k] = 0;
// 查找h(i, j)
h[i][j] = findCollision(i, j);
// 当前状态的h值
if (queens[i][j] == 1) {
curState = h[i][j];
}
// 计算h(i,j)之后要复原数据,避免计算错误
for (int k = 0; k < 8; k++)
temp[i][k] = queens[i][k];
}
}
// 随机选取第一个优于当前状态的下一状态
bool better = false;
int next, nextState, times = 0;
while (!better) {
next = rand() % 64;
nextState = h[next/8][next%8];
if (nextState < curState) {
better = true;
}
if (++times > 100) break;
}
if (better) {
for (int i = 0; i < 8; i++)
queens[next/8][i] = 0;
queens[next/8][next%8] = 1; // 放置皇后
}
// 判断是否找到解, 有解则返回值为真
if (check(h)) {
totalTrial += trial;
return true;
}
}
return false;
}
// 模拟退火搜索
bool simulated() {
double temperature = 5;
int trial = 0;
while (temperature > 0.00001) {
// 拷贝原始棋盘数据到temp
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
temp[i][j] = queens[i][j];
}
}
int h[8][8], curState;
for (int i = 0; i < 8; i++) {
for (int j = 0; j < 8; j++) {
// 在计算h(i, j)之前,对i行所有位置赋值为0
for (int k = 0; k < 8; k++)
temp[i][k] = 0;
// 查找h(i, j)
h[i][j] = findCollision(i, j);
// 当前状态的h值
if (queens[i][j] == 1) {
curState = h[i][j];
}
// 计算h(i,j)之后要复原数据,避免计算错误
for (int k = 0; k < 8; k++)
temp[i][k] = queens[i][k];
}
}
// 随机选取一个下一状态
bool better = false;
int next, nextState, times = 0;
next = rand() % 64;
nextState = h[next/8][next%8];
int E = nextState - curState;
if (E < 0) {
better = true;
} else if (exp((-1)*E/temperature) > ((double)(rand() % 1000) / 1000)) {
better = true;
}
if (better) {
for (int i = 0; i < 8; i++)
queens[next/8][i] = 0;
queens[next/8][next%8] = 1; // 放置皇后
trial++;
}
// 判断是否找到解, 有解则返回值为真
if (check(h)) {
totalTrial += trial;
return true;
}
temperature *= 0.99;
}
return false;
}
// 最陡上升爬山法
int steepestAscent() {
int count = 0;
for (int i = 0; i < 1000; i++) {
initial();
if (hillClimbing())
count++;
}
return count;
}
// 首选爬山法
int firstChose() {
int count = 0;
for (int i = 0; i < 1000; i++) {
initial();
if (firstchose())
count++;
}
return count;
}
// 随机重新开始爬山法
int randomRestart() {
bool find = false;
while (!find) {
initial();
find = hillClimbing();
}
return find;
}
// 模拟退火搜索
int simulatedAnnealing() {
int count = 0;
for (int i = 0; i < 1000; i++) {
initial();
if (simulated())
count++;
}
return count;
}
int main(int argc, char const *argv[]) {
srand((int)time(0));
totalTrial = 0;
cout << "steepest-Ascent searching..." << endl;
int count = steepestAscent();
cout << "steepest-Ascent[success/total]: " << count << "/1000" << endl;
cout << "average steps: " << totalTrial/count << endl;
totalTrial = 0;
cout << "random-Restart searching..." << endl;
int count2 = randomRestart();
cout << "random-Restart: " << count2 << endl;
cout << "average steps: " << totalTrial/count2 << endl;
totalTrial = 0;
cout << "first-Chose searching..." << endl;
int count3 = firstChose();
cout << "first-Chose[success/total]: " << count3 << "/1000" << endl;
cout << "average steps: " << totalTrial/count3 << endl;
totalTrial = 0;
cout << "simulated-Annealing searching..." << endl;
int count4 = simulatedAnnealing();
cout << "simulated-Annealing[success/total]: " << count4 << "/1000" << endl;
cout << "average steps: " << totalTrial/count4 << endl;
return 0;
}
八数码运行结果如下:
从实验运行结果看出,求解八数码和八皇后问题,用最陡上升爬山法和首选爬山法的查找成功率差不多大,八皇后的成功率为14~15%左右,八数码的成功率则为3~4%左右。
随机重启爬山法是遇到无解情况则重新开始初始状态直至找到有解,所以查找成功率几乎接近于1(理论上存在失败情况)。
而模拟退火搜索算法因为在选取下一状态的时候做了改进,所以无论是八皇后还是八数码,它的查找成功率是很高的。这里我的实现是八皇后77%,八数码30%。
在搜索代价方面, 这里仅显示了成功求解问题所需的平均移动步数。很明显,最陡上升爬山法、随机重启爬山法和首选爬山法的移动步数很少,几步之内就能很快求出解;模拟退火搜索算法虽然成功率高,但是移动步数却非常多,查找时间也比较长,代价也因此大了很多。