P1171 售货员的难题 for循环状压dp写法
P1171 售货员的难题
一、题意概括
有 \(n\) 个村庄( \(2 \le n \le 20\) ),村庄之间是有向图,从村 \(i\) 到村 \(j\) 的路程为 \(s_{i,j}\) 。
售货员从 1 号村庄(商店)出发,恰好访问每个村庄一次,最后 回到 1 号村庄,要求总路程最短。
👉 本质:有向图上的旅行商问题(TSP),起点固定为 1。
二、算法思路(状态压缩 DP)
由于 \(n \le 20\) ,无法用暴力枚举全排列( \(20!\) 太大),经典做法是 状态压缩动态规划。
1️⃣ 状态设计
用一个二进制集合表示已经访问过的村庄:
- 用
mask表示访问状态- 第 \(i\) 位为 1,表示 村庄 \(i\) 已访问
dp[mask][u]表示:从 1 出发,访问了
mask中所有村庄,且**当前停在村庄 \(u\) ** 的最短路程
注意:
- 村庄编号用 0-based(0 表示原来的 1 号村)
- 初始状态:只访问了起点
\[dp[1<<0][0] = 0
\]
2️⃣ 状态转移
设当前状态为 dp[mask][u],尝试走向一个 **未访问的村庄 \(v\) **:
- 新状态:
new_mask = mask | (1 << v)
- 转移方程:
\[dp[new\_mask][v] = \min\left(dp[new\_mask][v],\ dp[mask][u] + s_{u,v}\right)
\]
3️⃣ 终止与答案
当所有村庄都访问完:
\[mask = (1<<n) - 1
\]
此时还需要 从当前村庄返回 1 号村:
\[\text{ans} = \min_{u \neq 0}\left(dp[full][u] + s_{u,0}\right)
\]
4️⃣ 时间复杂度分析
- 状态数: \(2^n \times n\)
- 转移:再乘一个 \(n\)
\[O(n^2 \cdot 2^n) \approx 20^2 \cdot 2^{20} \approx 4 \times 10^8
\]
在 OI 优化 + 剪枝下是可以通过的。
三、C++ 实现
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
// n:点数
// g[i][j]:从 i 到 j 的有向边权
// dp[mask][u]:
// 从起点 0 出发,已经访问的点集合为 mask,
// 且当前停在 u 的最短路径长度
int n, g[N][N], dp[1<<N][N];
int main()
{
cin >> n;
// 读入邻接矩阵(有向图)
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
cin >> g[i][j];
// dp 初始化为无穷大,表示状态不可达
memset(dp, 0x3f, sizeof dp);
// 初始状态:
// mask = 1(二进制 000...001),只访问了点 0
// 停在点 0,路径长度为 0
dp[1][0] = 0;
// 枚举所有访问状态 s
// s 从小到大枚举,保证:
// s 的子集一定已经被计算过(DP 的拓扑顺序)
for(int s = 1; s < (1<<n); s++)
{
// 枚举当前状态 s 中的“终点 u”
for(int u = 0; u < n; u++)
{
// 如果 u 不在集合 s 中,说明 dp[s][u] 无意义
if((s >> u) & 1)
{
// 尝试从 u 走向一个“未访问过的点 v”
for(int v = 0; v < n; v++)
{
// v 不在 s 中,才能扩展
if(((s >> v) & 1) == 0)
{
// 新状态:在 s 的基础上加入 v
int ns = s | (1 << v);
// 状态转移:
// 从 dp[s][u] 走一条 u -> v 的边
// 得到 dp[ns][v]
dp[ns][v] = min(
dp[ns][v],
dp[s][u] + g[u][v]
);
}
}
}
}
}
// 所有点都访问过的状态
int full = (1 << n) - 1;
// 最终答案:
// 枚举最后停在 i(i != 0)
// 再加上一条 i -> 0 的边,回到起点
int ans = 0x3f3f3f3f;
for(int i = 1; i < n; i++)
ans = min(ans, dp[full][i] + g[i][0]);
cout << ans << endl;
return 0;
}
四、小结
- 本题是 经典状态压缩 DP 的 TSP 问题
- 核心在于:
- 二进制集合表示访问状态
dp[mask][u]表示“走到哪 + 走过哪些”
- 适用于 ** \(n \le 20\) ** 的全排列最优路径问题
五、状压dp常见写法
for(mask)
for(u in mask)
for(v not in mask)
dp[mask | (1<<v)][v]

浙公网安备 33010602011771号