题解
题意:n*m的网格里,有k个宝藏,James任意从某个位置走到网格内拾取所有的宝藏再走出网格,每个网格点都有权值,可以重复走,统计走过的路径权值之和,问这个和最小是多少
看得出来,是宝藏先取后取会影响路径,而这种问题,一般都是↓
TSP:旅行商问题,即TSP问题(Traveling Salesman Problem)又译为旅行推销员问题、货郎担问题,是数学领域中著名问题之一。假设有一个旅行商人要拜访n个城市,他必须选择所要走的路径,路径的限制是每个城市只能拜访一次,而且最后要回到原来出发的城市。路径的选择目标是要求得的路径路程为所有路径之中的最小值。
状压dp是想到了,处理宝藏间的最短路用dijkstra也想到了,就是边界和宝藏之间的最短路给懵了,诶…
代码参考这篇 如果看不懂我讲的,就看看大佬讲的吧
值得学习的东西还挺多的:
- 可以看到宝藏先取后取会影响路径,所以要先求出最短路,依次枚举每个宝藏作为源点,找到到达其他宝藏和边界的最短距离,每个点的权值视为到达该点的路径
- 只要满足
(u.x == 1 || u.x == n || u.y == 1 || u.y == m)
,即视为探索到边界 - 不过宝藏有可能出在边界处,所以要小心处理
- 这道题里边界->宝藏和宝藏->边界的最短路是不一样的,多了一个宝藏本身的点权,因为源点出发时是没有加上源点本身的点权的
- 求完最短路,用状压dp求出最小的路径权值之和:
dp[i][j]
表示状态为i
且最后一个走过的宝藏点是第j
个的最优解,状态i
就相当于一个集合,其表示已经到达过的点,依次加入未到达的,具体的看代码吧
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
#define between(x, a, b) (a<=x && x<=b)
const int dir[4][2] = {1, 0, 0, 1, -1, 0, 0, -1};
const int INF = 0x3f3f3f3f;
int n, m, K;
int val[205][205];
struct node {
int x, y;
void read() {
cin >> x >> y;
x++;
y++;
}
} treasure[20];
int f[205][205];//标记珠宝的编号
bool vis[205][205];
int dis[20][20];//表示 从第i个珠宝开始探索 到达第j个珠宝的最短路 如果i=0即表示从边界开始探索
struct minpath {
int x, y, dis;
bool operator<(const minpath &b) const {
return dis > b.dis;
}
};
priority_queue<minpath> q;
void dijkstra(int id) {
while (!q.empty())q.pop();//清空
int cnt = 0;//统计获得的珠宝的个数+是否探索边界
memset(vis, 0, sizeof(vis));
q.push({treasure[id].x, treasure[id].y, 0});
while (!q.empty()) {
auto u = q.top();
q.pop();
if (vis[u.x][u.y])continue;//如果曾经到达过
vis[u.x][u.y] = 1;
if (f[u.x][u.y]) {//当前位置有珠宝
cnt++;
dis[id][f[u.x][u.y]] = u.dis;
}
//边界处理
if (dis[id][0] == INF && (u.x == 1 || u.x == n || u.y == 1 || u.y == m)) {
// dis[id][0]表示到边界的最短路 即从未访问过边界
cnt++;//注意了 珠宝和边界可能重合的 此时要多加1
dis[id][0] = u.dis;
dis[0][id] = u.dis + val[treasure[id].x][treasure[id].y];
}
if (cnt == K + 1) return;//达成成就 走人!
for (int i = 0; i < 4; ++i) {
int dx = u.x + dir[i][0];
int dy = u.y + dir[i][1];
if (between(dx, 1, n) && between(dy, 1, m) && !vis[dx][dy] && val[dx][dy] != -1) {
q.push({dx, dy, u.dis + val[dx][dy]});
}
}
}
}
int dp[(1 << 16)][20];//dp[i][j]表示状态为i且最后一个走过的珠宝是第j个的最小值
void init() {
memset(f, 0, sizeof(f));
memset(dis, INF, sizeof(dis));
memset(dp, INF, sizeof(dp));
}
int main() {
ios::sync_with_stdio(0);
int T;
cin >> T;
for (int cs = 1; cs <= T; ++cs) {
init();
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> val[i][j];
}
}
cin >> K;
for (int i = 1; i <= K; ++i) {
treasure[i].read();
f[treasure[i].x][treasure[i].y] = i;
}
if (!K) {
puts("0");
continue;
}
for (int i = 1; i <= K; ++i) {
dijkstra(i);
}
//tsp 预处理第一层
dis[0][0] = 0;
for (int i = 0; i <= K; ++i) {
dp[1 << i][i] = dis[0][i];//集合里只有自己
}
//状压dp
for (int i = 0; i < (1 << (K + 1)); ++i) {//状态集合
for (int j = 0; j <= K; ++j) {//目的地
if ((i >> j) & 1) {//当前集合里有点j存在
for (int k = 0; k <= K; ++k) {//中间转移
if ((i >> k) & 1) {//当前集合里有k点存在
dp[i][j] = min(dp[i][j], dp[i & (~(1 << j))][k] + dis[k][j]);
//上一个到达k点且集合状态里不包含j点的最优解 + k与j之间的最短路 = 最后一次达到的是j点的最优解
}
}
}
}
}
cout << dp[(1 << (K + 1)) - 1][0] << endl;
//K个珠宝全部都拿到了 全在状态集合里面 并且回到了边界
}
return 0;
}