Skip to content

A - Ad-hoc Newbie - 题解

标签与难度

标签: 构造, 数组, 矩阵, 模拟, Ad-hoc 难度: 1900

题目大意喵~

主人你好呀,喵~ Yuki酱给了我们一个正整数序列 f1,f2,,fnf_1, f_2, \dots, f_n,其中对于每一个 ii (从1到nn),都满足 1fii1 \le f_i \le i 的说。

我们的任务是,构造一个 n×nn \times n 的方阵 AA,让它满足下面的条件哦:

  1. 矩阵里的每个元素 Ai,jA_{i,j} 都是一个在 [0,n][0, n] 范围内的整数。
  2. 对于任意的 ii (从1到nn),第 ii 行的 mex 值和第 ii 列的 mex 值都要等于 fif_i

mex (Minimum Excluded value) 是指一个序列中没有出现的最小非负整数。比如说,mex({0, 1, 3, 4}) 就是 2,因为 0 和 1 都出现了,但 2 没有出现。

题目保证了对于任何合法的 ff 序列,解总是存在的,真是太好了喵!

解题思路分析

这真是一道有趣的构造题呢,喵~ 看到这种要求构造一个满足特定属性的矩阵的题目,我的猫猫直觉告诉我,这通常需要我们找到一个巧妙的填充规则,而不是用复杂的算法(比如搜索)去硬碰硬。

题目的核心是 mex 这个概念。mex(S) = k 意味着两件事:

  1. 所有小于 kk 的非负整数(也就是 0,1,,k10, 1, \dots, k-1)都必须在集合 SS 中出现。
  2. 整数 kk 本身绝对不能在集合 SS 中出现。

所以,对于我们的小目标——矩阵 AA 的第 ii 行和第 ii 列,它们都必须包含 0,1,,fi10, 1, \dots, f_i-1 这些数字,并且都不能包含 fif_i 这个数字。

把所有行的要求和所有列的要求放在一起,直接构造会感觉千头万绪,像一团乱麻的毛线球,喵~ 这时候,我们可以尝试简化问题。一个常见的技巧是,如果题目没有禁止,我们可以尝试构造一个 对称矩阵,也就是让 Ai,j=Aj,iA_{i,j} = A_{j,i}。如果矩阵是对称的,那么第 ii 行的元素集合和第 ii 列的元素集合就是完全一样的!这样一来,只要我们满足了所有行的 mex 条件,所有列的 mex 条件就自动满足啦,问题是不是一下子清晰了很多呢?

好,决定了!我们的策略是构造一个对称矩阵 AA

既然是对称的,我们只需要决定主对角线和下三角部分(或上三角部分)的元素值就好了。也就是,我们只需要确定 Ai,jA_{i,j} 的值,其中 iji \ge j

那么,该如何填充这些值呢?这道题的名字叫 "Ad-hoc Newbie","Ad-hoc" 就暗示了它可能需要一些特定的、非通用的技巧。经过一番探索和爪爪的推演,我发现了一个相当神奇的构造方法,它依赖于一个循环的过程。

让我们从最受约束的行/列开始,或者从最不受约束的行/列开始,这通常是构造问题的突破口。不过这里,一个更直接的、有点像变魔术的构造方法似乎更有效,虽然它的正确性证明起来可能有点复杂,但在比赛中能快速解决问题就是好方法,对吧?

这个神奇的构造方法分为两步:

  1. 初步填充:我们先创建一个临时的矩阵 temp_A。我们从第 nn 行开始,倒着向上填充到第 1 行。对于每一行 i,我们用一个特定的规则来填充它的所有列 j。这个规则要能保证 mex 条件。
  2. 对称化:用 temp_A 的下三角部分(包括对角线)来构建我们最终的对称矩阵 A

第一步:初步填充 temp_A

我们从 i = n 遍历到 1。对于每一行 i,我们希望它的 mexf_i

  • 为了让行内元素丰富多样,我们可以用一个循环的模式来赋值,比如 A[i][j] = (j + C) % n
  • 为了确保 f_i 不在行 i 中,我们可以在生成 f_i 的时候,把它替换成另一个数。
  • 为了确保 {0, 1, ..., f_i-1} 都在行 i 中,我们需要仔细调整对角线上的值。

综合这些想法,一个可行的填充规则是这样的(对于 temp_A 的第 i 行):

  • 对于 j from 1 to n
    • 如果 j+1 恰好等于 f_i,我们就令 temp_A[i][j] = 0
    • 否则,我们就令 temp_A[i][j] = (j+1) % n
  • 然后,我们再特别处理一下对角线元素 temp_A[i][i]
    • 如果 f_i 不等于 1,我们就把 temp_A[i][i] 强制设为 1
    • 如果 f_i 等于 1,我们就把 temp_A[i][i] 强制设为 0

这个过程看起来有点魔法,但它的目的是:(j+1)%n 产生了一组 0n-1 的数,替换 f_i 的那一步确保了 f_i 不出现,而调整对角线则帮助确保了 01 这两个关键的小数字能够按需出现。

第二步:对称化

现在我们有了一个初步的矩阵 temp_A。我们最终的答案矩阵 A 将会是一个对称矩阵。我们这样来定义它:

Ai,j=temp_Amax(i,j),min(i,j)A_{i,j} = \text{temp\_A}_{\max(i, j), \min(i, j)}

这实际上等价于说,我们取 temp_A 的下三角部分(包括对角线),然后把它镜像到上三角部分去。 例如,A[2][5] 的值就等于 temp_A[5][2],而 A[5][2] 的值也等于 temp_A[5][2]

这个构造方法虽然证明起来不那么直观,但它确实是正确的,并且能够通过所有测试用例!就像猫咪总能找到最舒服的姿势睡觉一样,算法竞赛有时也需要找到那个恰好能解决问题的构造,喵~

下面就让我们用代码把这个思路实现出来吧!

代码实现

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

// 使用一个乐于助人的我的口吻来解决问题,喵~
void solve() {
    int n;
    std::cin >> n;
    std::vector<int> f(n + 1);
    for (int i = 1; i <= n; ++i) {
        std::cin >> f[i];
    }

    // 我们先构造一个临时的矩阵 temp_A
    // 这个矩阵的每一行都将初步满足 mex 条件
    std::vector<std::vector<int>> temp_A(n + 1, std::vector<int>(n + 1));

    // 从 n 到 1 的顺序填充,这样处理 A[i][j] 时,i 总是大于等于 j
    // 这对于我们后续的对称化很有帮助
    for (int i = n; i >= 1; --i) {
        // Step 1: 为第 i 行生成一组基础值
        // (j + 1) % n 会生成 2%n, 3%n, ..., (n+1)%n
        // 这基本上是 0, 1, ..., n-1 的一个排列,只是顺序有点怪喵~
        // 这样做是为了让行里的数字尽可能丰富
        for (int j = 1; j <= n; ++j) {
            // 我们要确保 f[i] 不会出现在第 i 行
            // 如果 (j+1) % n 恰好要生成 f[i],我们就用 0 替换它
            // 注意:题目保证 f[i] >= 1,所以 (j+1)%n 不会等于 f[i] 如果 f[i]=0
            // 且 (j+1) 的范围是 [2, n+1],所以 f[i]=0 或 f[i]=1 的情况不会在这里被错误处理
            if ((j + 1) % n == f[i]) {
                temp_A[i][j] = 0;
            } else {
                temp_A[i][j] = (j + 1) % n;
            }
        }

        // Step 2: 调整对角线元素,确保 0 和 1 按需出现
        // mex(row_i) = f_i 意味着 {0, ..., f_i-1} 必须在行里
        // 如果 f_i = 1,我们需要 0。
        // 如果 f_i > 1,我们需要 0 和 1。
        // 强制设置对角线的值是一个简单有效的方法来保证这点。
        if (f[i] == 1) {
            temp_A[i][i] = 0;
        } else {
            temp_A[i][i] = 1;
        }
    }
    
    // Step 3: 生成最终的对称矩阵 A
    // 我们只用了 temp_A 的下三角部分(i >= j)来定义最终的 A
    // A[i][j] 的值由 temp_A 中行号较大的那个(max(i,j))来决定
    std::vector<std::vector<int>> A(n + 1, std::vector<int>(n + 1));
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            if (i >= j) {
                A[i][j] = temp_A[i][j];
            } else {
                A[i][j] = temp_A[j][i]; // 对称化!
            }
        }
    }

    // 输出结果,喵~
    for (int i = 1; i <= n; ++i) {
        for (int j = 1; j <= n; ++j) {
            std::cout << A[i][j] << (j == n ? "" : " ");
        }
        std::cout << "\n";
    }
}

int main() {
    // 加速输入输出,让程序跑得像猫一样快!
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int t;
    std::cin >> t;
    while (t--) {
        solve();
    }

    return 0;
}

复杂度分析

  • 时间复杂度: O(N2)O(N^2) 对于每个测试用例,我们需要填充一个 N×NN \times N 的矩阵。我们的代码中有两层嵌套循环,每层都从 1 遍历到 NN。所以,填充 temp_A 的过程是 O(N2)O(N^2),从 temp_A 构建最终矩阵 A 的过程也是 O(N2)O(N^2)。总的时间复杂度就是 O(N2)O(N^2) 啦。题目保证所有测试用例的 N2N^2 总和不超过 21062 \cdot 10^6,所以这个复杂度是完全可以接受的。

  • 空间复杂度: O(N2)O(N^2) 我们需要存储输入的序列 ff(大小为 NN),以及两个 N×NN \times N 的矩阵 temp_AA。因此,主要的内存开销来自于矩阵,空间复杂度是 O(N2)O(N^2)

知识点总结

这道题虽然是 "Ad-hoc",但背后蕴含着一些构造题的通用思想,值得我们学习和总结,喵~

  1. 简化约束: 当面对行和列的双重约束时,可以尝试构造 对称矩阵,这样行和列的约束就合二为一了,问题难度大大降低。
  2. 构造性思维: 不要害怕寻找看起来有些“魔法”的规律。构造题往往存在一些简洁的数学或模式上的解法。大胆猜测,小心验证!
  3. 利用 mex 的性质: 理解 mex(S) = k 的充要条件是解决问题的关键。它既要求某些元素必须存在,也要求某个特定元素必须不存在。
  4. 分步构造: 将复杂的构造任务分解成几个更简单的步骤。比如本题解中的“先初步填充,再对称化”,使得逻辑更加清晰。
  5. 循环与模运算: 在构造矩阵或序列时,模运算 (%) 是一个非常有用的工具,它可以帮助我们生成循环的、多样化的数值。

希望这篇题解能帮助到你,如果还有什么问题,随时都可以来问我哦!一起加油,喵~