P;
int n, m, k;
struct edge {
int to, next, w;
} e[maxn << 1];
int head[maxn << 1], tree[maxn << 1], tot;
int dp[maxn][5000], vis[maxn];
int key[maxn];
priority_queue, greater
> q;
void add(int u, int v, int w) {
e[++tot] = edge{v, head[u], w};
head[u] = tot;
}
void dijkstra(int s) {
memset(vis, 0, sizeof(vis));
while (!q.empty()) {
P item = q.top();
q.pop();
if (vis[item.second]) continue;
vis[item.second] = 1;
for (int i = head[item.second]; i; i = e[i].next) {
if (dp[tree[i]][s] > dp[item.second][s] + e[i].w) {
dp[tree[i]][s] = dp[item.second][s] + e[i].w;
q.push(P(dp[tree[i]][s], tree[i]));
}
}
}
}
int main() {
memset(dp, INF, sizeof(dp));
scanf("%d %d %d", &n, &m, &k);
int u, v, w;
for (int i = 1; i <= m; i++) {
scanf("%d %d %d", &u, &v, &w);
add(u, v, w);
tree[tot] = v;
add(v, u, w);
tree[tot] = u;
}
for (int i = 1; i <= k; i++) {
scanf("%d", &key[i]);
dp[key[i]][1 << (i - 1)] = 0;
}
for (int s = 1; s < (1 << k); s++) {
for (int i = 1; i <= n; i++) {
for (int subs = s & (s - 1); subs; subs = s & (subs - 1))
dp[i][s] = min(dp[i][s], dp[i][subs] + dp[i][s ^ subs]);
if (dp[i][s] != INF) q.push(P(dp[i][s], i));
}
dijkstra(s);
}
printf("%d\n", dp[key[1]][(1 << k) - 1]);
return 0;
}
```
另外一道经典例题 [\[WC2008\]游览计划](https://www.luogu.com.cn/problem/P4294) 。
这道题是求点权和最小的斯坦纳树,用 $f(i,S)$ 表示以 $i$ 为根的一棵树,包含集合 $S$ 中所有点的最小点权值和。 $a_i$ 表示点权。
考虑状态转移:
- $f(i,S)\leftarrow \min(f(i,S),f(i,T)+f(i,S-T)-a_i)$ 。由于此处合并时同一个点 $a_i$ ,会被加两次,所以减去。
- $f(i,S)\leftarrow \min(f(i,S),f(j,S)+w(j,i))$ 。
可以发现状态转移与上面的模板题是类似的,麻烦的是对答案的输出,在 DP 的过程中还要记录路径。
用 `pre[i][s]` 记录转移到 $i$ 为根,连通状态集合为 $s$ 时的点与集合的信息。在 DP 结束后从 `pre[root][S]` 出发,寻找与集合里的点相连的那些点并逐步分解集合 $S$ ,用 ans 数组来记录被使用的那些点,当集合分解完毕时搜索也就结束了。
### "参考实现"
```c++
#include
using namespace std;
#define mp make_pair
typedef pair P;
typedef pair PP;
const int INF = 0x3f3f3f3f;
const int dx[] = {0, 0, -1, 1};
const int dy[] = {1, -1, 0, 0};
int n, m, K, root;
int f[101][1111], a[101], ans[11][11];
bool inq[101];
PP pre[101][1111];
queue
q;
bool legal(P u) {
if (u.first >= 0 && u.second >= 0 && u.first < n && u.second < m) {
return true;
}
return false;
}
int num(P u) { return u.first * m + u.second; }
void spfa(int s) {
memset(inq, 0, sizeof(inq));
while (!q.empty()) {
P u = q.front();
q.pop();
inq[num(u)] = 0;
for (int d = 0; d < 4; d++) {
P v = mp(u.first + dx[d], u.second + dy[d]);
int du = num(u), dv = num(v);
if (legal(v) && f[dv][s] > f[du][s] + a[dv]) {
f[dv][s] = f[du][s] + a[dv];
if (!inq[dv]) {
inq[dv] = 1;
q.push(v);
}
pre[dv][s] = mp(u, s);
}
}
}
}
void dfs(P u, int s) {
if (!pre[num(u)][s].second) return;
ans[u.first][u.second] = 1;
int nu = num(u);
if (pre[nu][s].first == u) dfs(u, s ^ pre[nu][s].second);
dfs(pre[nu][s].first, pre[nu][s].second);
}
int main() {
memset(f, INF, sizeof(f));
scanf("%d %d", &n, &m);
int tot = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
scanf("%d", &a[tot]);
if (!a[tot]) {
f[tot][1 << (K++)] = 0;
root = tot;
}
tot++;
}
}
for (int s = 1; s < (1 << K); s++) {
for (int i = 0; i < n * m; i++) {
for (int subs = s & (s - 1); subs; subs = s & (subs - 1)) {
if (f[i][s] > f[i][subs] + f[i][s ^ subs] - a[i]) {
f[i][s] = f[i][subs] + f[i][s ^ subs] - a[i];
pre[i][s] = mp(mp(i / m, i % m), subs);
}
}
if (f[i][s] < INF) q.push(mp(i / m, i % m));
}
spfa(s);
}
printf("%d\n", f[root][(1 << K) - 1]);
dfs(mp(root / m, root % m), (1 << K) - 1);
for (int i = 0, tot = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (!a[tot++])
putchar('x');
else
putchar(ans[i][j] ? 'o' : '_');
}
if (i != n - 1) printf("\n");
}
return 0;
}
```
## 习题
[【模板】最小斯坦纳树](https://www.luogu.com.cn/problem/P6192)
[\[WC2008\]游览计划](https://www.luogu.com.cn/problem/P4294)
[\[JLOI2015\]管道连接](https://loj.ac/problem/2110)
[\[APIO2013\]机器人](https://www.luogu.com.cn/problem/P3638)