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]
posted @ 2026-01-08 10:30  katago  阅读(18)  评论(0)    收藏  举报