字节跳动19春招研发笔试
前言
不会做, 只会DFS (贪婪 + 分支界限都试了, 然而只过了 50%的测试用例, 其他的超时), 学!!
思路
贪婪 (先搜索最近的) 和分支界限都只能提高最优情况的性能, 在最坏情况下仍然没有帮助. 所以 DFS 最坏情况下的复杂度是 ( n − 1 ) ! (n -1)! (n−1)! (阶乘就是爆炸)
仔细观察会发现, 假如一共有6个城市, 那么
1- perm{2, 3, 4} - 5 - 6 - 1 这一共6种方案中, 都是在访问完 {1, 2, 3, 4} 后再到5, 最后访问6和1的. 所以出现了重复的子集.
因此, 可以用一个数组来保存一个状态, 表示在访问完 {1, 2, 3, 4} 这个状态后, 从5开始访问其他所有节点的最优解
此时状态用二进制编码, 出口用单独一个数字编码, 即可建表.
我再多解释几句:
表的形式是 d p [ s t a t u s ] [ o u t ] dp[status][out] dp[status][out], 其中 status 的每个位表示是否已经访问过这个城市, 而 out 表示我现在走到了这个城市, 然后以这个城市为起点寻找下一个城市. 也就是说我现在已经访问过了 status(二进制1的个数) + 1 个(out)城市, 之所以要把 out 单独从 status 中抽离出来是为了唯一确定这个状态的出口. 举个例子:
d p [ 0 ] [ 0 ] dp[0][0] dp[0][0] 表示我一个城市还没访问过, 现在在 0 这个城市 (起点), 从 0 开始寻找下一个城市.
d p [ 7 ] [ 4 ] dp[7][4] dp[7][4] 表示我已经访问过了 {0, 1, 2} ( 7 = 二进制的111), 现在走到了 4 这个城市, 然后从 4 这个城市开始寻找下一个目的地.
代码 ( C )
#include <stdio.h>
#define MAXN 20
#define INF (1 << 30)
int min(int a, int b) {
return (a < b) ? a : b;
}
int adj[MAXN][MAXN];
int dp[1 << MAXN][MAXN];
int full = 0;
int status;
/*
在调用这个函数的时刻, 表示已经走过了 status 所标注的地点,
!!以及!! out 这个地点 (还没标注)
返回的是在这个状态下走完剩余剩余城市并返回城市 0 的最优值
*/
int dfs(int out) {
// 记忆化搜索的标准起手, 如果已经存储了这个状态的值, 直接返回
int result = dp[status][out];
if (result) return result;
// (status | (1 << out)) 表示我现在已经遍历了这些城市, 如果全为 1 的话, 只需要返回 out (现在的位置) 和 0 (起点) 的距离.
if ((status | (1 << out)) == full) return adj[out][0];
// 设置状态 (已经访问过这个城市)
status