Skip to content

十六度空间 - 题解

标签与难度

标签: 动态规划, 状压DP, 旅行商问题(TSP), 组合数学, 容斥原理, 曼哈顿距离 难度: 2500

题目大意喵~

主人你好呀,这道题是要我们在一个 nn 维空间里玩耍哦!

我们有 mm 个给定的点,坐标都是整数。从一个点移动到另一个点,每一步只能沿着某个坐标轴移动一个单位长度(也就是坐标值 +1+11-1)。

现在,有 kk 个点被指定为可以作为起点。我们的任务是,从这 kk 个点中的任意一个出发,找到一条路径,把所有 mm 个点都访问到。我们需要计算出这条路径所需的最少总步数,以及在步数最少的前提下,有多少种不同的走法,喵~

两种走法不同,要么是总步数不一样,要么是路径中某一步的移动方向(即改变哪个维度的坐标)不一样。

简单来说,就是在一个高维网格里,解决一个带计数功能的旅行商问题(TSP)!是不是听起来就很有挑战性呀?

解题思路分析

喵哈哈,这道题看似是在高维空间里遨游,但只要我们一层层剥开它的外壳,就能发现其中的奥秘啦!让本喵带你一步步分析吧!

Step 1: 问题的核心——旅行商问题 (TSP)

首先,我们要访问所有 mm 个点,并求最小代价,这可是个经典的旅行商问题(TSP)的信号呢!看到题目中 m16m \le 16 这个小小的数字,我们的猫猫雷达就应该立刻锁定状压DP

我们可以定义一个状态 dp[mask][i],表示当前已经访问过的点的集合是 mask(一个二进制数,第 j 位为1表示点 j 已访问),并且最后停在点 i 上的 {最小总步数, 对应方案数}

Step 2: “步数”与“走法”的计算

在进行DP之前,我们得先搞清楚两点之间的“步数”和“走法”怎么算,对吧?

  • 最小步数:题目规定每一步只能动一个坐标。从点 pi=(ci,1,,ci,n)p_i = (c_{i,1}, \dots, c_{i,n}) 移动到 pj=(cj,1,,cj,n)p_j = (c_{j,1}, \dots, c_{j,n}),最少的步数就是它们在各个维度上坐标差的绝对值之和。这就是大名鼎鼎的曼哈顿距离,喵~

    d(i,j)=d=1nci,dcj,dd(i, j) = \sum_{d=1}^{n} |c_{i,d} - c_{j,d}|

  • 走法数量:从 pip_ipjp_j 总共要走 D=d(i,j)D = d(i, j) 步。其中,在第 dd 维上需要走 Δd=ci,dcj,d\Delta_d = |c_{i,d} - c_{j,d}| 步。这相当于一个排列组合问题:在 DD 个位置里,选 Δ1\Delta_1 个给第1维,再从剩下的位置里选 Δ2\Delta_2 个给第2维…… 这就是一个多项式系数

    Ways(i,j)=D!Δ1!Δ2!Δn!=(d=1nΔd)!d=1n(Δd!)\text{Ways}(i, j) = \frac{D!}{\Delta_1! \Delta_2! \dots \Delta_n!} = \frac{(\sum_{d=1}^{n} \Delta_d)!}{\prod_{d=1}^{n} (\Delta_d!)}

    为了计算这个,我们需要预处理阶乘和阶乘的逆元,喵~

Step 3: 最关键的陷阱——“路过”也算访问!

如果只是简单地用上面的距离和走法去做TSP,那就掉进陷阱啦!考虑三个点 pi,pj,pkp_i, p_j, p_k。如果从 pip_ipjp_j 的某条最短路径恰好经过了 pkp_k,那么我们实际上在走 pipjp_i \to p_j 的过程中就已经访问了 pkp_k

这个“路过”的条件用曼哈顿距离可以很优雅地表示:

d(i,k)+d(k,j)=d(i,j)d(i, k) + d(k, j) = d(i, j)

当这个等式成立时,我们就说点 kkiijj 的最短路径上。

这意味着,我们不能简单地把问题看作是点的排列。我们的DP转移应该是从一个状态 dp[S][i] 转移到 dp[S | {j}][j],但这个转移的边 i -> j 必须是**“纯粹”**的,也就是说,从 iijj 的最短路径上不能经过任何我们还没访问过的点 kk。但这样太复杂了!

一个更清晰的思路是:我们构建的旅行路径,是由一系列**“基本路径”**组成的。所谓基本路径 i -> j,是指从 ii 直接走到 jj,中途不经过任何其他的目标点 kk

Step 4: 容斥原理求“基本路径”

那么,如何计算从 iijj 的“基本路径”的数量呢?这就要请出我们的好朋友——容斥原理啦!

我们令 w[i][j] 表示从 iijj 的总最短路径信息 {d(i, j), Ways(i, j)}。 再令 g[i][j] 表示从 iijj 的“基本路径”信息。

考虑所有从 iijj 的最短路径。它们可以被分类:

  1. 直接到达 jj,中途不经过任何其他点 kk。这就是 g[i][j]
  2. 中途经过了至少一个点。比如,第一个经过的点是 kk。那么路径可以分解为 ikji \to k \to \dots \to j

所有从 iijj 的最短路径,等于所有以 ii 为起点,以某个点 kkkki,ji,j 之间)为第一个中间点的基本路径,再乘以从 kkjj 的所有最短路径。 用公式表达就是:

w[i][j]=g[i][j]+k s.t. d(i,k)+d(k,j)=d(i,j)g[i][k]w[k][j]w[i][j] = g[i][j] + \sum_{k \text{ s.t. } d(i,k)+d(k,j)=d(i,j)} g[i][k] \otimes w[k][j]

(这里的 \otimes 表示代价相加,方案数相乘)

整理一下,就能得到计算 g[i][j] 的方法:

g[i][j]=w[i][j]k s.t. d(i,k)+d(k,j)=d(i,j)g[i][k]w[k][j]g[i][j] = w[i][j] - \sum_{k \text{ s.t. } d(i,k)+d(k,j)=d(i,j)} g[i][k] \otimes w[k][j]

为了计算 g[i][j],我们可以固定起点 i,然后按照 d(i, j) 的距离从小到大来计算。当我们计算 g[i][j] 时,所有可能的中间点 k 因为 d(i, k) < d(i, j),所以 g[i][k] 肯定已经被算出来啦!

具体算法如下 (对每个固定的 i):

  1. 预计算好所有 w[i][j]w[k][j]
  2. 将所有其他点 jd(i, j) 升序排序。
  3. 初始化 g[i][j] = w[i][j]
  4. 遍历排序后的点 p
    • 对于排在 p 后面的每个点 q
      • 如果 d(i, p) + d(p, q) == d(i, q),说明 piq 的一个潜在中间点。
      • 我们就从 g[i][q] 的方案数中,减去经过 p 的方案数,即 g[i][p].ways * w[p][q].ways

这样,我们就得到了所有点对之间的“基本路径”信息 g[i][j]

Step 5: 最终的状压DP

有了 g 矩阵,我们就可以进行标准的TSP状压DP了!

  • 状态: dp[mask][i] 表示访问了 mask 集合中的点,最后停在 i{最小代价, 方案数}
  • 初始化:
    • 对于所有 kk 个可作为起点的点 qdp[1 << q][q] = {0, 1}
    • 其他所有 dp 状态初始化为 {infinity, 0}
  • 转移: 我们从小到大枚举集合 mask,然后枚举 mask 中的点 i 作为上一个点,再枚举不在 mask 中的点 j 作为下一个点: dp[mask | (1 << j)][j] = min_update(dp[mask | (1 << j)][j], dp[mask][i] + g[i][j]) 这里的 min_update+ 都是我们为 {代价, 方案数} 这个数据结构定义的运算:
    • A + B: 代价相加,方案数相乘。
    • min_update(A, B): 如果 B 的代价更小,A 更新为 B;如果代价相同,A 的方案数加上 B 的方案数。
  • 答案: 遍历所有 i,取 dp[(1 << m) - 1][i]min_update 结果,就是最终答案啦!

好啦,思路清晰了,可以开始写代码了,喵~

代码实现

cpp
#include <iostream>
#include <vector>
#include <numeric>
#include <algorithm>

using namespace std;

const int MOD = 998244353;
const int INF = 0x3f3f3f3f;

// 用于存储 {最小代价, 方案数} 的结构体
struct State {
    int cost;
    int ways;

    // 默认构造,设为无效状态
    State() : cost(INF), ways(0) {}
    State(int c, int w) : cost(c), ways(w) {}

    // 路径拼接:代价相加,方案数相乘
    State operator+(const State& other) const {
        if (cost == INF || other.cost == INF) {
            return State(INF, 0);
        }
        return State(cost + other.cost, (1LL * ways * other.ways) % MOD);
    }
};

// 用于DP更新,取最优解
void update_state(State& current, const State& candidate) {
    if (candidate.cost < current.cost) {
        current = candidate;
    } else if (candidate.cost == current.cost) {
        current.ways = (current.ways + candidate.ways) % MOD;
    }
}

long long power(long long base, long long exp) {
    long long res = 1;
    base %= MOD;
    while (exp > 0) {
        if (exp % 2 == 1) res = (res * base) % MOD;
        base = (base * base) % MOD;
        exp /= 2;
    }
    return res;
}

long long modInverse(long long n) {
    return power(n, MOD - 2);
}

const int MAX_TOTAL_DIST = 3200005;
vector<long long> fact;
vector<long long> invFact;

void precompute_factorials(int max_val) {
    fact.resize(max_val + 1);
    invFact.resize(max_val + 1);
    fact[0] = 1;
    invFact[0] = 1;
    for (int i = 1; i <= max_val; ++i) {
        fact[i] = (fact[i - 1] * i) % MOD;
        invFact[i] = modInverse(fact[i]);
    }
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int m, n, k;
    cin >> m >> n >> k;

    vector<vector<int>> points(m, vector<int>(n));
    int max_coord_val = 0;
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < n; ++j) {
            cin >> points[i][j];
            max_coord_val = max(max_coord_val, abs(points[i][j]));
        }
    }
    
    // 预处理阶乘,最大距离可能是 m * n * 2 * max_coord_diff
    precompute_factorials(m * 2 * 100000);

    // w[i][j]: 从i到j的总最短路径信息
    vector<vector<State>> w(m, vector<State>(m));
    for (int i = 0; i < m; ++i) {
        for (int j = 0; j < m; ++j) {
            if (i == j) {
                w[i][j] = {0, 1};
                continue;
            }
            int total_dist = 0;
            vector<int> diffs;
            for (int dim = 0; dim < n; ++dim) {
                int diff = abs(points[i][dim] - points[j][dim]);
                total_dist += diff;
                diffs.push_back(diff);
            }
            
            long long ways = fact[total_dist];
            for (int diff : diffs) {
                ways = (ways * invFact[diff]) % MOD;
            }
            w[i][j] = {total_dist, (int)ways};
        }
    }

    // g[i][j]: 从i到j的“基本路径”信息
    vector<vector<State>> g(m, vector<State>(m));
    for (int i = 0; i < m; ++i) {
        vector<int> order(m);
        iota(order.begin(), order.end(), 0);
        sort(order.begin(), order.end(), [&](int a, int b) {
            return w[i][a].cost < w[i][b].cost;
        });

        for (int j = 0; j < m; ++j) {
            g[i][j] = w[i][j];
        }

        for (int p_idx = 0; p_idx < m; ++p_idx) {
            int p = order[p_idx];
            if (i == p) continue;
            for (int q_idx = p_idx + 1; q_idx < m; ++q_idx) {
                int q = order[q_idx];
                if (w[i][p].cost + w[p][q].cost == w[i][q].cost) {
                    long long subtract_ways = (1LL * g[i][p].ways * w[p][q].ways) % MOD;
                    g[i][q].ways = (g[i][q].ways - subtract_ways + MOD) % MOD;
                }
            }
        }
    }

    // 状压DP
    int full_mask = (1 << m) - 1;
    vector<vector<State>> dp(1 << m, vector<State>(m));

    for (int i = 0; i < k; ++i) {
        int start_node;
        cin >> start_node;
        --start_node;
        dp[1 << start_node][start_node] = {0, 1};
    }

    for (int mask = 1; mask < full_mask; ++mask) {
        for (int i = 0; i < m; ++i) {
            if ((mask >> i) & 1) { // 如果i在mask中
                if (dp[mask][i].cost == INF) continue;
                for (int j = 0; j < m; ++j) {
                    if (!((mask >> j) & 1)) { // 如果j不在mask中
                        int next_mask = mask | (1 << j);
                        update_state(dp[next_mask][j], dp[mask][i] + g[i][j]);
                    }
                }
            }
        }
    }

    State final_ans;
    for (int i = 0; i < m; ++i) {
        update_state(final_ans, dp[full_mask][i]);
    }

    cout << final_ans.cost << endl;
    cout << final_ans.ways << endl;

    return 0;
}

复杂度分析

  • 时间复杂度: O(m2n+mmlogm+m3+2mm2)O(m^2 \cdot n + m \cdot m \log m + m^3 + 2^m \cdot m^2)

    • 预处理 w 矩阵(所有点对间的距离和总方案数):O(m2n)O(m^2 \cdot n)
    • 计算 g 矩阵(基本路径):对每个起点 i (mm 个),需要排序 (O(mlogm)O(m \log m)),然后两层循环 (O(m2)O(m^2))进行容斥计算。总共是 O(m(mlogm+m2))O(m3)O(m \cdot (m \log m + m^2)) \approx O(m^3)
    • 状压DP:状态数为 2mm2^m \cdot m,每个状态的转移需要遍历 mm 个可能的下一个点。总复杂度是 O(2mm2)O(2^m \cdot m^2)
    • 由于 m16m \le 16,主要瓶颈在于状压DP部分,所以总时间复杂度为 O(2mm2)O(2^m \cdot m^2)
  • 空间复杂度: O(2mm+MaxDist)O(2^m \cdot m + \text{MaxDist})

    • points 数组:O(mn)O(m \cdot n)
    • wg 矩阵:O(m2)O(m^2)
    • dp 表:O(2mm)O(2^m \cdot m)
    • 预处理阶乘数组:最大距离可能很大,约为 m×2×105m \times 2 \times 10^5,所以是 O(MaxDist)O(\text{MaxDist})
    • 主要空间开销是DP表和阶乘数组。

知识点总结

这真是一道融合了多种算法思想的超棒题目,解开它就像是完成了一次华丽的探险,喵~

  1. 状压DP (Bitmask DP): 解决小规模集合问题的利器,尤其适用于TSP这类问题。
  2. 旅行商问题 (TSP): 问题的基本模型,需要找到访问所有节点的最短回路/路径。
  3. 曼哈顿距离: 在网格图上衡量距离的常用方式,其性质 d(i,k)+d(k,j)=d(i,j) 是解题的关键。
  4. 组合数学 (多项式系数): 用于计算两点间最短路径的方案数,需要熟练运用阶乘及其逆元。
  5. 容斥原理: 本题的灵魂!通过容斥排除了非“基本”路径的干扰,将复杂问题简化为可以在DP中直接使用的“边”,是本题从暴力想法到正解的桥梁。

希望本喵的讲解对主人有帮助哦!下次遇到难题,也请务必来找我,喵~!