一、哈密尔顿(Hamiltonian)问题概述
哈密尔顿图(Hamiltonian graph)是一个无向图,由天文学家哈密尔顿提出,由指定的起点前往指定的终点,途中经过所有其他节点且只经过一次。在图论中是指含有哈密尔顿回路的图,闭合的哈密尔顿路径称作哈密尔顿回路(Hamiltonian cycle),含有图中所有顶点的路径称作哈密顿路径(Hamiltonian path)。
在此基础上,延伸出了旅行商问题,即给定一系列城市和每对城市之间的距离,求解访问每一座城市⼀次并回到起始城市的最短回路。
至今为止,该问题仍旧是NP-Hard问题,即只有指数级别的解,没有多项式级别的解。通常来说,有如下解法:
- 回溯(暴力搜索)
- 记忆化搜索
- 状态压缩动态规划
二、不同路径III
原题力扣980 . - 力扣(LeetCode)
1、回溯
经典回溯写法,时间复杂度:O(4^(n*m)),其中 n 和 m 分别是地图的行列数;
空间复杂度:O(n*m),回溯深度;若算上 result 数组,则为 O(n*m*P),其中 P 是路径数
int dirs[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
vector<vector<pair<int, int>>> result;
vector<pair<int, int>> path;
void backtracking(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y, int n, int m, int total)
{
// 终止条件:到达终点而且路径长度等于总路径长度(注意要加上终点的1)
if (grid[x][y] == 2 && path.size() == total + 1)
{
result.push_back(path);
return;
}
for (const auto& dir : dirs)
{
int newx = x + dir[0];
int newy = y + dir[1];
if (newx < 0 || newx >= n || newy < 0 || newy >= m)
{
continue;
}
if (visited[newx][newy] == true || grid[newx][newy] == -1)
{
continue;
}
// 添加路径和标记
path.push_back({newx, newy});
visited[newx][newy] = true;
// 递归
backtracking(grid, visited, newx, newy, n, m, total);
// 回溯
path.pop_back();
visited[newx][newy] = false;
}
}
int uniquePathsIII(vector<vector<int>>& grid)
{
int n = grid.size();
int m = grid[0].size();
int startx, starty, total = 0;
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
// 找到起点和空格点的个数
if (grid[i][j] == 1)
{
startx = i;
starty = j;
total++;
}
if (grid[i][j] == 0)
{
total++;
}
}
}
vector<vector<bool>> visited(n, vector<bool>(m, false));
path.push_back({startx, starty});
visited[startx][starty] = true;
backtracking(grid, visited, startx, starty, n, m, total);
return result.size();
}
2、记忆化搜索+状态压缩
核心:利用二进制法保存每条路径的状态,当遇到相同的状态时,可以直接得出结果
注意:memo 哈希表保存每个状态对应的最短路径信息,当重复搜索到某个状态时,直接调用即可;使用二进制位掩码替代 visited 数组来保存格子的访问信息
状态压缩常用公式:
判断第 i 位是否为 1 | visited & (1<<i) != 0 |
如果第 i 位为0,设为 1 | visited + (1<<i) |
如果第 i 位为1,设为 0 | visited - (1<<i) |
经过优化后,时间复杂度:O(n*m*2^(n*m)),其中 n 和 m 分别是地图的行数和列数,而n*m*2^(n*m)是状态数,每个状态最多只会被计算一次。因此,空间复杂度也为 O(n*m*2^(n*m))
int dirs[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
// 动态规划函数,参数为(地图,当前横坐标,当前纵坐标,剩余的可以访问的位置数量,地图行数,地图列数,记忆哈希表)
int dp(vector<vector<int>>& grid, int x, int y, int remaining, int n, int m, unordered_map<int, int>& memo)
{
// 找到了终点,如果访问完了全部需要访问的位置,返回1,否则返回0
if (grid[x][y] == 2)
{
if (remaining == 0)
{
return 1;
}
else
{
return 0;
}
}
// 当前位置的状态键值
int key = ((x * m + y) << (n * m)) + remaining;
// 如果当前状态不在哈希表中
if (!memo.count(key))
{
int res = 0; // 路径数量
for (const auto& dir : dirs)
{
int newx = x + dir[0];
int newy = y + dir[1];
if (newx < 0 || newx >= n || newy < 0 || newy >= m)
{
continue;
}
// 如果移动后的位置可以访问
if (remaining & (1 << (newx * m + newy)))
{
// 递归计算从新位置开始的路径数量,并累加到结果中
res += dp(grid, newx, newy, remaining ^ (1 << (newx * m + newy)), n, m, memo);
}
}
memo[key] = res; // 记忆结果
}
return memo[key]; // 返回当前位置的路径数量
}
int uniquePathsIII(vector<vector<int>>& grid)
{
int n = grid.size();
int m = grid[0].size();
int startx, starty, initialRemaining = 0;
unordered_map<int, int> memo; // <当前的状态信息,不同的路径数量>
// 遍历地图,初始化各变量
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
// 如果当前位置是空地或终点,标记为可访问
if (grid[i][j] == 0 || grid[i][j] == 2)
{
initialRemaining |= (1 << (i * m + j));
}
else if (grid[i][j] == 1)
{
startx = i;
starty = j;
}
}
}
return dp(grid, startx, starty, initialRemaining, n, m, memo);
}