1 条题解
-
0
Trulicina 的网格构造 详细题解
问题重述
给定三个整数 ,满足 且 。要求构造一个 行 列的网格,每个格子填入 的整数,并满足:
- 每个整数 在网格中出现的总次数相同(均为 次);
- 任意两个相邻(共享一条边)的格子中的整数不同。
题目保证这样的网格总是存在。
直观尝试:自然阅读顺序填充
一个直接的想法是,按照从上到下、从左到右的顺序,循环填入 。即第 行第 列(‑based 索引,,)的数为:
例如,当 时,得到:
1 2 3 4 5 6 1 2 3 4 5 6
该方案自动满足“每个数出现次数相等”,因为总共 是 的倍数,循环填数恰好使每个 都出现 次。
该方案何时失败?
检查相邻格子的差(模 意义下):
- 水平相邻:(因为加 )。只要 ,,所以水平方向永远不会冲突。
- 垂直相邻:(因为跨过一整行,索引增加 )。因此,当 时,垂直差不为 ,相邻不同;但当 是 的倍数时,,导致 ,垂直相邻格子数字相同,违反条件。
例如 : 是 的倍数,自然顺序填入得到:
1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
垂直相邻全是相同的数字,不符合要求。
修正方案:对部分行进行循环移位
当 是 的倍数时,我们需要通过“错位”来打破垂直相邻的相同。一个通用的构造是:仍然遵循逐行循环填充 ,但当检测到当前行与上一行在某一列出现相同数字时,将整行向左循环移动一个位置。
在实现中,我们逐行生成。对于第 行,先按自然顺序生成一个临时行:
然后检查该行是否与上一行(存储在
LAST中)有任何同列相同。若有,则将临时行循环左移一位,即新行 。再将CUR作为当前行输出,并把它更新为新的LAST。为什么一次循环移位就能修正?
设 是 的倍数,且第 行的第 列数字为 。自然顺序下,第 行同列的数字也是 (因为 )。进行循环左移一位后,原来位置 的元素被原来位置 的元素取代,即当前行 列的数字变为 。
计算垂直差(模 ):
$$\begin{aligned} CUR_j - LAST_j &\equiv \big( (i\cdot m + j + 1) - ((i-1)\cdot m + j) \big) \pmod k \\ &\equiv m + 1 \pmod k. \end{aligned} $$由于 是 的倍数,,故差为 ,不为 。因此移位后,所有垂直相邻对均不同。
同时,移位操作保持水平相邻差仍为 (或 )。因为移一位后,整行仍是形如 的循环连续段,相邻位置仍差 ,不会相同。
图示示例
仍用 :
- 第 1 行():自然顺序
1 2 3 1 2 3,与上一行(空)不冲突,不移位。 - 第 2 行():自然顺序
1 2 3 1 2 3,与上一行完全相同,触发移位。左移一位后变成2 3 1 2 3 1。 - 第 3 行():自然顺序
1 2 3 1 2 3,与上一行2 3 1 2 3 1比较,有同列相同吗?第 2 行第 2 列是3,第 3 行自然顺序同列也是3,所以移位。左移一位后变成2 3 1 2 3 1吗?但代码会继续判断。实际上标准程序在此场景下会交替出现1 2 3 1 2 3和2 3 1 2 3 1,最终输出正与题面示例一致:
1 2 3 1 2 3 2 3 1 2 3 1 1 2 3 1 2 3 2 3 1 2 3 1
垂直相邻差为 ,水平差也为 ,完美符合条件。
当 不是 的倍数时
此时自然顺序已经满足条件,
shift始终为false,不会移位。输出即为自然顺序填充。特判:
当 时,网格只有一列,不存在水平相邻限制。唯一要求是垂直相邻不同且每个数出现次数相等。我们可以简单地让第 行填 ,这样相邻行差为 ,且每个数字恰好出现 次(因为 是 的倍数)。
标准程序中对 做了特殊处理,直接输出,不进入后续逻辑。
算法流程
- 读入 。
- 对每个测试用例:
- 读入 。
- 若 :
- 对于 ,输出 ,并换行。
- 继续下一个用例。
- 初始化数组
LAST长度为 ,值全为 (表示上一行,初始无限制)。 - 对于 :
- 创建当前行数组
CUR长度为 。 shift = false- 对于 :
elm = ((i * m + j) % k) + 1CUR[j] = elm- 若
elm == LAST[j],则shift = true
- 若
shift为真:- 将
CUR循环左移一位:newCUR[j] = CUR[(j + 1) % m] - 用
newCUR替换CUR
- 将
- 输出
CUR的 个整数,空格分隔,行末换行。 LAST = CUR(更新上一行)。
- 创建当前行数组
正确性证明
每个数字出现次数相等
无论是自然顺序还是经过移位,每一行都是 的某个循环排列的重复。具体来说,若一行有 个元素,且 整除 (因为我们只在 是 的倍数时才移位,而 不是 的倍数时不移位),那么这一行恰好包含完整的 套 。当 不是 的倍数时,整行仍由若干个完整周期和一个部分周期组成,但整个网格总元素数 是 的倍数,且每行的填充方式都是连续的块,可以证明每种数字出现的总次数为 。
更严谨的论证:无论是否移位,第 行第 列的数字总可以写成 的形式,其中 (不移位时 ,移位时相当于 )。整个网格的数字序列是将 的连续整数加上每行的偏移后对 取模。由于每行的偏移都是常数,且 是 的倍数,容易得知每个余数出现的次数完全相等。
相邻格子不同
- 水平相邻:不移位时差为 ;移位后由于整行循环左移,相邻元素在模意义下仍相差 (原来是连续的数,循环移位后依然是连续的数)。因此水平相邻永不相同。
- 垂直相邻:
- 若 ,不移位,差为 ,不同。
- 若 ,当不移位时垂直差为 ,但此时必然有同列相同导致
shift = true,因此一定会移位。移位后垂直差变为 ,不同。
因此所有相邻对均不相同。
复杂度分析
对于每个测试用例,我们逐行逐列生成网格,时间复杂度为 。由于所有测试用例的 之和不超过 ,总时间完全在限制内。空间复杂度同样为 ,只需存储当前行和上一行。
总结
本题通过简单的“自然顺序+条件移位”构造,巧妙解决了当列数是 的倍数时垂直相邻冲突的问题。关键在于分析垂直相邻差 ,并通过一行循环移位将差调整为 ,同时不影响水平相邻的性质。这种构造方法简洁且易于实现,是 CF 中典型的有趣构造题。
标准程序
#include <bits/stdc++.h> using namespace std; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) { int n, m, k; cin >> n >> m >> k; // 特判 m == 1 的情况:行内没有相邻限制,只需行间不同且每个数出现相等 if (m == 1) { for (int i = 0; i < n; ++i) { cout << (i % k) + 1 << '\n'; } continue; } vector<int> LAST(m, -1); for (int i = 0; i < n; ++i) { vector<int> CUR(m); bool shift = false; for (int j = 0; j < m; ++j) { int elm = ((i * m + j) % k) + 1; CUR[j] = elm; if (elm == LAST[j]) { shift = true; } } if (shift) { // 循环左移一位(对应 Python 的 CUR[(j+1)%m]) vector<int> newCUR(m); for (int j = 0; j < m; ++j) { newCUR[j] = CUR[(j + 1) % m]; } CUR = move(newCUR); } for (int j = 0; j < m; ++j) { cout << CUR[j] << " \n"[j == m - 1]; } LAST = CUR; } } return 0; }
- 1
信息
- ID
- 6743
- 时间
- 2000ms
- 内存
- 256MiB
- 难度
- 6
- 标签
- 递交数
- 2
- 已通过
- 1
- 上传者