Skip to content

排列 - 题解

标签与难度

标签: 二分图匹配, 最大流, Dinic算法, 数论, 质数筛, 图论, 构造 难度: 2400

题目大意喵~

哈喽,各位master!今天我们遇到的这道题,是要我们构造一个从1到 nn 的特殊排列 pp 呢,喵~ 这个排列 pp 要满足两个非常严格的条件哦:

  1. 对于每一个位置 ii (从 1 到 nn),它和它对应的值 pip_i 的和,也就是 i+pii + p_i,必须是一个质数。
  2. 同时,它们俩的差的绝对值,也就是 ipi|i - p_i|,也必须是一个质数。

如果能找到这样一个神奇的排列,我们就输出 "YES",然后把这个排列打印出来。如果找不到,就只好遗憾地输出 "NO" 啦。

举个例子,如果 n=2n=2,一个可能的排列是 p=[2,1]p = [2, 1]

  • 对于 i=1i=1, p1=2p_1=21+2=31+2=3 (质数),12=1|1-2|=1 (不是质数)。啊哦,这个不行。
  • p=[1,2]p=[1, 2] 呢?i=1,p1=1i=1, p_1=111=0|1-1|=0 (不是质数)。

看来要找到这个排列,需要一点点智慧和魔法呢,喵!

解题思路分析

这道题的限制条件看起来很数学,但它的核心其实是一个匹配问题!让我带你一步步把问题转化,最后用强大的网络流魔法来解决它吧,喵~

第一步:解开条件的数学小秘密

我们有两个条件需要满足:

  1. i+pi=P1i + p_i = P_1
  2. ipi=P2|i - p_i| = P_2

其中 P1P_1P2P_2 都是质数。

我们把绝对值拆开,不妨假设 pi>ip_i > i,那么第二个式子就是 pii=P2p_i - i = P_2。现在我们得到了一个漂亮的二元一次方程组:

{pi+i=P1pii=P2\begin{cases} p_i + i = P_1 \\ p_i - i = P_2 \end{cases}

把这两个方程加起来,可以得到 2pi=P1+P22 \cdot p_i = P_1 + P_2,所以 pi=P1+P22p_i = \frac{P_1 + P_2}{2}。 把第一个方程减去第二个方程,可以得到 2i=P1P22 \cdot i = P_1 - P_2,所以 i=P1P22i = \frac{P_1 - P_2}{2}

哇!这意味着任何一个满足条件的配对 (i,pi)(i, p_i),都可以由两个质数 P1P_1P2P_2 唯一确定!

第二步:奇偶性大发现!

为了让 iipip_i 是整数,(P1+P2)(P_1 + P_2)(P1P2)(P_1 - P_2) 都必须是偶数。这意味着 P1P_1P2P_2 的奇偶性必须相同!

我们知道,唯一的偶质数是 2。

  • 如果 P1P_1P2P_2 都是 2,那么 i=(22)/2=0i = (2-2)/2 = 0,这不在我们的 [1,n][1, n] 范围内,所以不行。
  • 因此,P1P_1P2P_2 必须都是奇质数

这个发现太重要了!它大大缩小了我们寻找质数对的范围。

第三步:一个超级重要的剪枝!

我们再来观察一下所有配对的总和。我们要求的是一个排列,所以 {1,2,,n}\{1, 2, \dots, n\}{p1,p2,,pn}\{p_1, p_2, \dots, p_n\} 其实是同一个集合,只是顺序不同。 把所有的 i+pii+p_i 加起来:

i=1n(i+pi)=i=1ni+i=1npi=2i=1ni=2n(n+1)2=n(n+1)\sum_{i=1}^{n} (i + p_i) = \sum_{i=1}^{n} i + \sum_{i=1}^{n} p_i = 2 \sum_{i=1}^{n} i = 2 \cdot \frac{n(n+1)}{2} = n(n+1)

我们已经知道,每一个 i+pii+p_i 都是一个奇质数(因为如果 i+pi=2i+p_i=2,则 i=pi=1i=p_i=1,但 11=0|1-1|=0 不是质数,所以和不能是2)。

现在,我们来分析 nn 的奇偶性:

  • 如果 nn 是奇数,那么我们总共有 nn 个配对。这意味着我们要把 nn 个(奇数个)奇质数加起来。奇数个奇数的和一定是奇数!
  • 但是,当 nn 是奇数时,n+1n+1 是偶数,所以它们的乘积 n(n+1)n(n+1) 必然是偶数。

这就出现了矛盾:一个奇数怎么可能等于一个偶数呢?喵呜~! 所以,当 nn 是奇数时,绝对不可能构造出这样的排列!我们可以直接输出 "NO"。这是一个非常强大的剪枝,呐!

第四步:把问题变成图论模型

我们的任务是为集合 {1,2,,n}\{1, 2, \dots, n\} 中的每一个数 ii 找到一个唯一的搭档 pip_i,这个搭档也来自集合 {1,2,,n}\{1, 2, \dots, n\}。这不就是经典的完美匹配问题嘛!

我们可以这样来建模:

  1. 建立一个二分图。图的左边有 nn 个点,代表我们的下标 1,2,,n1, 2, \dots, n
  2. 图的右边也有 nn 个点,代表我们的排列值 1,2,,n1, 2, \dots, n
  3. 如果在左边的点 ii 和右边的点 jj 之间可以配对(即满足 i+ji+jij|i-j| 都是奇质数),我们就在它们之间连一条边。
  4. 我们的目标,就是在这个二分图中找到一个大小为 nn完美匹配。如果找到了,那么匹配中的每一条边 (i,j)(i, j) 就告诉我们 pi=jp_i = j

第五步:用网络流求解!

寻找二分图的最大匹配,最经典的方法就是使用最大流算法,比如 Dinic 算法。

我们可以这样构建一个网络流图:

  1. 创建一个源点 SS 和一个汇点 TT
  2. 将二分图左边的 nn 个点(我们称之为 u1,u2,,unu_1, u_2, \dots, u_n)和右边的 nn 个点(我们称之为 v1,v2,,vnv_1, v_2, \dots, v_n)放入网络中。
  3. 从源点 SS 向每个左部点 uiu_i 连一条容量为 1 的边。这表示每个下标 ii 只能用一次。
  4. 从每个右部点 vjv_j 向汇点 TT 连一条容量为 1 的边。这表示每个值 jj 只能用一次。
  5. 对于每一个满足条件的配对 (i,j)(i, j),我们从左部点 uiu_i 向右部点 vjv_j 连一条容量为 1 的边。
    • 为了高效地找到所有这样的边,我们不遍历所有 (i,j)(i, j) 对。而是遍历所有奇质数对 (Pa,Pb)(P_a, P_b),计算出 i=PaPb/2i = |P_a - P_b|/2j=(Pa+Pb)/2j = (P_a + P_b)/2。如果 iijj 都在 [1,n][1, n] 的范围内,就在 uiu_ivjv_j 之间连边。

建好图后,我们从 SSTT 跑一遍最大流。

  • 如果最大流的值等于 nn,说明我们找到了一个完美的匹配!YES!我们可以遍历图中的边,找到那些流量为 1 的边 (ui,vj)(u_i, v_j),从而构造出排列 pp
  • 如果最大流的值小于 nn,说明无法让每个数都找到搭档,那就只能是 "NO" 了。

总结一下我们的完整策略:

  1. 预处理:用欧拉筛或埃氏筛找出 2n2n 以内的所有质数。
  2. 剪枝:判断 nn 是否为奇数。如果是,直接输出 "NO"。
  3. 建图:
    • 建立一个包含源点 SS、汇点 TT、以及 2n2n 个点的网络。
    • 连接 SuiS \to u_ivjTv_j \to T 的边。
    • 遍历所有奇质数对 (Pa,Pb)(P_a, P_b),计算出可能的配对 (i,j)(i, j),并连接 uivju_i \to v_jujviu_j \to v_i 的边。(因为 (i,j)(i, j)(j,i)(j, i) 都是可能的配对)
  4. 求解:运行 Dinic 算法计算从 SSTT 的最大流。
  5. 输出:根据最大流是否等于 nn 来判断并输出结果。

代码实现

这是我根据上面的思路,精心重构的一份代码哦~ 注释很详细,希望能帮到你!

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

// 定义一个足够大的常量,用于表示无穷大容量
const int INF = 1e9;

// --- 质数筛 ---
// 使用Sieve of Eratosthenes筛出 2*N 范围内的质数
std::vector<bool> is_prime;
std::vector<int> primes;
void sieve(int max_n) {
    is_prime.assign(max_n + 1, true);
    is_prime[0] = is_prime[1] = false;
    for (int p = 2; p * p <= max_n; ++p) {
        if (is_prime[p]) {
            for (int i = p * p; i <= max_n; i += p)
                is_prime[i] = false;
        }
    }
    for (int p = 2; p <= max_n; ++p) {
        if (is_prime[p]) {
            primes.push_back(p);
        }
    }
}

// --- Dinic最大流算法模板 ---
struct Edge {
    int to;      // 边的终点
    int capacity; // 边的容量
    int rev;     // 反向边的索引
};

std::vector<std::vector<Edge>> adj;
std::vector<int> level;
std::vector<int> iter;

void add_edge(int from, int to, int capacity) {
    adj[from].push_back({to, capacity, (int)adj[to].size()});
    adj[to].push_back({from, 0, (int)adj[from].size() - 1}); // 反向边初始容量为0
}

// BFS用于构建分层网络
bool bfs(int s) {
    level.assign(adj.size(), -1);
    std::queue<int> q;
    level[s] = 0;
    q.push(s);
    while (!q.empty()) {
        int u = q.front();
        q.pop();
        for (const auto& edge : adj[u]) {
            if (edge.capacity > 0 && level[edge.to] < 0) {
                level[edge.to] = level[u] + 1;
                q.push(edge.to);
            }
        }
    }
    return level[adj.size() - 1] != -1; // 汇点是否可达
}

// DFS用于在分层网络中寻找增广路
int dfs(int u, int t, int f) {
    if (u == t) return f;
    for (int& i = iter[u]; i < adj[u].size(); ++i) {
        Edge& e = adj[u][i];
        if (e.capacity > 0 && level[u] < level[e.to]) {
            int d = dfs(e.to, t, std::min(f, e.capacity));
            if (d > 0) {
                e.capacity -= d;
                adj[e.to][e.rev].capacity += d;
                return d;
            }
        }
    }
    return 0;
}

// Dinic主函数
int max_flow(int s, int t) {
    int flow = 0;
    while (bfs(s)) {
        iter.assign(adj.size(), 0);
        int f;
        while ((f = dfs(s, t, INF)) > 0) {
            flow += f;
        }
    }
    return flow;
}


int main() {
    // 加速输入输出,喵~
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n;
    std::cin >> n;

    // 奇数n无解,这是我们的重要剪枝!
    if (n % 2 != 0) {
        std::cout << "NO\n";
        return 0;
    }

    sieve(2 * n);

    // --- 构建网络流图 ---
    // 节点0是源点S, 2n+1是汇点T
    // 节点 1..n 是左部点 (代表下标 i)
    // 节点 n+1..2n 是右部点 (代表排列值 p_i)
    int num_nodes = 2 * n + 2;
    int S = 0, T = 2 * n + 1;
    adj.assign(num_nodes, std::vector<Edge>());

    // 1. S -> 左部点 (u_i)
    for (int i = 1; i <= n; ++i) {
        add_edge(S, i, 1);
    }

    // 2. 右部点 (v_j) -> T
    for (int j = 1; j <= n; ++j) {
        add_edge(n + j, T, 1);
    }

    // 3. 左部点 -> 右部点
    // 遍历所有奇质数对来生成可能的配对
    for (size_t i = 0; i < primes.size(); ++i) {
        for (size_t j = i + 1; j < primes.size(); ++j) {
            int p1 = primes[i];
            int p2 = primes[j];
            
            // 我们需要两个奇质数
            if (p1 == 2 || p2 == 2) continue;

            int val_a = (p2 - p1) / 2;
            int val_b = (p2 + p1) / 2;
            
            if (val_a > n || val_b > n || val_a == 0) continue;
            
            // 添加从左部点到右部点的边
            add_edge(val_a, n + val_b, 1);
            add_edge(val_b, n + val_a, 1);
        }
    }

    // --- 求解并输出 ---
    if (max_flow(S, T) == n) {
        std::cout << "YES\n";
        std::vector<int> p(n + 1);
        // 从结果中恢复排列
        for (int i = 1; i <= n; ++i) {
            for (const auto& edge : adj[i]) {
                // 如果从u_i到v_j的边满流(原容量1, 现容量0), 说明(i, j)是一对匹配
                if (edge.to > n && edge.to <= 2 * n && edge.capacity == 0) {
                    p[i] = edge.to - n;
                    break;
                }
            }
        }
        for (int i = 1; i <= n; ++i) {
            std::cout << p[i] << (i == n ? "" : " ");
        }
        std::cout << "\n";
    } else {
        std::cout << "NO\n";
    }

    return 0;
}

复杂度分析

  • 时间复杂度: O(π(2n)2+Dinic)O(\pi(2n)^2 + \text{Dinic})

    • 质数筛: 使用埃氏筛,复杂度是 O(NloglogN)O(N \log \log N),其中 N=2nN=2n
    • 建图: 这是主要的瓶颈。我们需要遍历质数对,其数量大约是 π(2n)2\pi(2n)^2π(x)\pi(x) 是小于等于 xx 的质数个数,约等于 x/ln(x)x/\ln(x)。对于题目数据范围(nn 不会太大,参考代码中暗示了 n15000n \le 15000 左右),这个操作是可行的。
    • Dinic算法: 在单位容量的二分图上,Dinic算法的理论复杂度是 O(EV)O(E\sqrt{V}),其中 V=2nV=2n 是点数, EE 是边数。虽然 EE 可能很大,但在实际问题中通常表现得更快。
  • 空间复杂度: O(n+E)O(n + E)

    • 主要空间开销是存储网络流图的邻接表。adj 数组的大小取决于点数 VV 和边数 EEEE 的大小与 π(2n)2\pi(2n)^2 相关。

知识点总结

这道题是数论和图论结合的绝佳范例,喵~ 解开它需要我们掌握以下几个关键知识点:

  1. 问题转化: 将代数约束(和与差为质数)转化为关于两个质数的方程,这是解题的突破口。
  2. 数论性质:
    • 奇偶性分析: 这是本题最巧妙的剪枝,通过分析质数的奇偶性以及和的奇偶性,直接排除了所有奇数 nn 的情况。
    • 质数筛: 高效地预处理出所需范围内的所有质数是必不可少的。
  3. 图论建模: 能够识别出问题本质是一个完美匹配问题,并将其转化为二分图匹配模型。
  4. 网络流: 掌握使用最大流算法(如Dinic)来求解二分图最大匹配的标准方法。这是解决这类问题的强力工具。

希望这篇题解能帮助你更好地理解这个问题,也让你感受到算法世界的乐趣!继续加油哦,master!喵~