一、深度优先搜索
1、坐标
老鼠所在位置的用((x,y))来表示,所以需要声明一个结构体,用来描述坐标:
//坐标
struct Point {
double x, y;
} a[N];
2、预处理
深度优先搜索最怕算的太多TLE,能预处理的一定要提前进行预处理。本题中,每个结点间都可能要计算距离,而且可能会重复计算(a->b)的距离,预处理就更划算~,同时计算(a->b)时,也就是同时算出了(b->a),一举多得啊!
for (int i = 0; i < n; i++)
for (int j = i + 1; j <= n; j++) {
double x1 = a[i].x, y1 = a[i].y, x2 = a[j].x, y2 = a[j].y;
dis[j][i] = dis[i][j] = sqrt(abs((x1 - x2) * (x1 - x2)) + abs((y1 - y2) * (y1 - y2)));
}
3、深搜的参数
深度优先搜索中,最为关键的就是参数的设定。一般是依据题意,比如本题,需要解决如下几个关键点:
(1)、从哪个位置出发
(2)、已经吃完几个奶酪
(3)、已经走了多远的距离
而向哪一个点出发
,则不是参数,是因为它会作为下一个出发点
,也就是关键点(1),重复使用概念没有意义。
/**
*
* @param pos 当前走的是第几个奶酪
* @param num 已吃掉的奶酪个数
* @param dis 当前距离的值
*/
void dfs(int pos, int num, double len) {
...
}
上面的代码返回值是void
.那么,一定是在结果收集部分将本轮的最优解存入到一个全局变量中~,本题是变量(res)。
4、回溯
因为是不能走回头路,所以肯定要使用(vis)数组保存走过了哪些点。走完了一条分支,还想走另一条,肯定要再将状态标识为可用。
5、C++代码
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 16; //对于全部的测试点,保证 1<=n<=15,
int n; //一共多少个奶酪
double res = INF; //记录最短路径长度,也就是最终的答案
double dis[N][N]; //dis[i][j]记录第i个点到第j的点的距离.这个是预处理的二维数组,防止重复计算,预处理是搜索优化的重要手段
bool vis[N]; //记录i奶酪在当前的路径探索中是否已经尝试过(可以重复使用噢~)
//坐标
struct Point {
double x, y;
} a[N];
/**
*
* @param pos 当前走的是第几个奶酪
* @param num 已吃掉的奶酪个数
* @param dis 当前距离的值
*/
void dfs(int pos, int num, double len) {
//剪一下枝
if (len >= res) return;
//前n个都吃完了,可以进行路线长短对比了
if (num == n) {
res = min(len, res);
return;
}
//从pos出发,向1~n个奶酪进军,去吃~
for (int i = 1; i <= n; i++) {
//回溯算法的套路,如果在本次寻找过程中,没有走过这个节点
if (!vis[i]) {
//标识使用过了
vis[i] = true;
//再走一步
dfs(i, num + 1, len + dis[pos][i]);
//反向标识
vis[i] = false;
}
}
}
int main() {
//老鼠的原始位置
a[0].x = 0, a[0].y = 0;
//读入奶酪的坐标
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
//预处理(dfs能进行预处理的尽量预处理)
for (int i = 0; i < n; i++)
for (int j = i + 1; j <= n; j++) {
double x1 = a[i].x, y1 = a[i].y, x2 = a[j].x, y2 = a[j].y;
dis[j][i] = dis[i][j] = sqrt(abs((x1 - x2) * (x1 - x2)) + abs((y1 - y2) * (y1 - y2)));
}
//开始暴搜
dfs(0, 0, 0);
//输出结果
printf("%.2f", res);
return 0;
}
再想出剪枝的办法后,依然在测试点10处挂掉`TLE`了。需要想办法优化
二、记忆化+二进制状态表示法
时间复杂度最大(15^{15}),太大了,然后可以想到每次搜完纪录下来结果,接下来搜索过程中大于这个数直接退出当前层(剪枝)。但这种方法不能保证最坏情况的加速,有一部分情况和之前无异。
1、记忆化+二进制状态表示法
正确方法是记忆化搜索,数组纪录到达某个状态。下次在遇到相同状态可以调用数组即可。
怎么存状态,每个点要么走了要么没走,显然是(2)进制数就可以,且发现(n)最大(15),空间绝对没问题,但是......到这里还没完,仅仅这样存不了,因为它有路径这么一说,从不同点走过来,哪些点走了哪些点没走就算是一样的,只有最后一个点(当前点)不同,也不是一个状态,所以数组要(2)维,第二位是当前走到那个点,好了到这里就顺利储存状态了。
double f[1 << N][N];
说明:
一维
:
最大范围是(2^N)次方,比如(N=5),就需要(32)来描述,最大的比如((11111)_2=31),(1<<N)就是(2^N)的意思。含义是已经走过的节点有哪些,是一个二进制转为十进制的数字,最终表现为十进制. 每一位代表一个奶酪,比如一共有(3)个奶酪,(f[(101)_{(2)}]=f[5_{(10)}]) 表示(1)号和(3)号奶酪吃完了,(2)号没有吃。
二维
:
最大范围是(N),表示当前所在的点。
值
:
(f[i][j]) 如果以前计算过在以第(j)个位置出发,在前面已经完成(i)这种二进制表示法的节点完成情况下,最短的距离是多少。
比如(f[5][2])就是表示从(2)号状态出发,前面已经走完了(5_{(10)}=(101)_{(2)}),就是(1)和(3)走完了,(2)号还没有走的意思。
2、怎么使用二进制状态表示法
(1)、深度优先搜索,都是思考下一步的操作怎么办
比如,当前在(pos)这个位置,下一个要去的位置是(i),那我们要思考,去还是不去呢?
(a)、如果这种状态没有出现过,那么肯定要去。
(b)、如果这种状态出现过,但没有我尝试
去一下的效果好,那么我要去。
(c)、如果这种状态出现过,但比我尝试
去一下的效果好,那我就不去。(人家现有更优秀~)
前提是我假设我已经走到了(i)这个位置,那么哪些东东变化了呢?
(a)、状态会变化
状态变为原来的状态加上走了(i)的状态,就是
int nst = st + (1 << (i - 1));
(b)、距离会变化
len + dis[pos][i]
3、C++代码
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;//无穷大
const int N = 16; //对于全部的测试点,保证 1<=n<=15,
int n; //一共多少个奶酪
double res = INF; //记录最短路径长度,也就是最终的答案
double dis[N][N]; //dis[i][j]记录第i个点到第j的点的距离.这个是预处理的二维数组,防止重复计算,预处理是搜索优化的重要手段
bool vis[N]; //记录i奶酪在当前的路径探索中是否已经尝试过(可以重复使用噢~)
//坐标
struct Point {
double x, y;
} a[N];
//状态压缩
//一维:代表已经走过的节点有哪些,是一个二进制转为十进制的数字,最终表现为十进制.
// 每一位代表一个奶酪,比如一共有3个奶酪,f[(101)二进制] =f[5] 表示1号和3号奶酪吃完了,2号没有吃
// 二维:代表当前的出发点
// 值:f[i][j] 如果以前计算过在以第j个位置出发,在前面已经完成i这种二进制表示法的节点完成情况下,最短的距离是多少
double f[1 << N][N];
/**
*
* @param pos 当前的位置
* @param status 当前状态压缩表示吃到的奶酪的状态[之前都吃了谁?]eg:status=10010,代表吃了第二个和第五个奶酪(从右往左)
* @param num 已经吃掉了几个奶酪
* @param len 当前距离
*/
void dfs(int pos, int num, int st, double len) {
//剪一下枝
if (len >= res) return;
//表示n个都吃完了,可以进行路线长短对比了
if (num == n) {
res = min(len, res);
return;
}
//从pos出发,向1~n个奶酪进军,去吃~
for (int i = 1; i <= n; i++) {
//回溯算法的套路,如果在本次寻找过程中,没有走过这个节点
//注意:深度优先一直思考的是下一个动作,下一个动作,下一个动作,重要的话说三遍!
//(1)、i就是下一个点,如果选择了i,那么就要状态就会变成加入i的样子:int nst = st + (1 << (i - 1));
//(2)、如果以前记录过这个状态,那么现在有没有机会去更新它?更新的条件是比它小。
if (!vis[i]) {
int nst = st + (1 << (i - 1));//目标状态,也就是要走完第i个结点之后的状态,比如i=1,就是2^0=1,描述状态用 1<<(i-1)噢~
//f[x][y]一路保存最小值,能用上就干掉一大批!
if (f[nst][i] && f[nst][i] <= len + dis[pos][i]) continue;
//标识状态
vis[i] = true;
//更新当前状态的值
f[nst][i] = len + dis[pos][i];
//继续深搜
dfs(i, num + 1, nst, f[nst][i]);
//取消标识
vis[i] = false;
}
}
}
int main() {
//老鼠的原始位置
a[0].x = 0, a[0].y = 0;
//读入奶酪的坐标
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
//预处理(dfs能进行预处理的尽量预处理)
for (int i = 0; i <= n; i++)
for (int j = i + 1; j <= n; j++) {
double x1 = a[i].x, y1 = a[i].y, x2 = a[j].x, y2 = a[j].y;
dis[j][i] = dis[i][j] = sqrt(abs((x1 - x2) * (x1 - x2)) + abs((y1 - y2) * (y1 - y2)));
}
//深度优先搜索
dfs(0, 0, 0, 0);
//输出
printf("%.2f", res);
return 0;
}
三、全排列最笨的解法,可以过50%-60%的测试点(吸氧)
#include<bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 16; //对于全部的测试点,保证 1<=n<=15,
int n; //一共多少个奶酪
double res = INF; //记录最短路径长度,也就是最终的答案
double dis[N][N]; //dis[i][j]记录第i个点到第j的点的距离.这个是预处理的二维数组,防止重复计算,预处理是搜索优化的重要手段
//坐标
struct Point {
double x, y;
} a[N];
int per[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
int main() {
//老鼠的原始位置
a[0].x = 0, a[0].y = 0;
//读入奶酪的坐标
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i].x >> a[i].y;
//预处理
for (int i = 0; i < n; i++)
for (int j = i + 1; j <= n; j++) {
double x1 = a[i].x, y1 = a[i].y, x2 = a[j].x, y2 = a[j].y;
dis[j][i] = dis[i][j] = sqrt(abs((x1 - x2) * (x1 - x2)) + abs((y1 - y2) * (y1 - y2)));
}
//1~n的全排列,计算每一组组合的距离和,找出最小值,也就能过70%的测试点,11个数据是极限
double MIN = INF;
do {
//计算当前路线下的行走距离
double s = dis[0][per[0]];
for (int i = 0; i < n - 1; i++) s += dis[per[i]][per[i + 1]];
MIN = min(MIN, s);
} while (next_permutation(per, per + n));
//输出大吉
printf("%.2f", MIN);
return 0;
}