P6497 Prosječni 题解
题目大意
一道构造题。
总结一下限制条件:
-
每行的平均数要在该行中出现;
-
每列的平均数要在该列中出现;
-
矩阵内的数字互不相同。
看一眼数据范围,发现 \(n\) 的范围只是 \([1, 100]\),而矩阵中的数不大于 \(10 ^ 9\) 即可,所以操作空间还是很大的。
思路
首先可以发现的事情是:如果 \(n\) 是 奇数 的话,直接从上到下,从左到右,从 \(1\) 开始,每次 \(+ 1\),直接填即可(参考样例 \(1\))。并且,如果 \(n\) 为奇数,那么最大为 \(99\),而根据此方法,最后一个数最大,是 \(9801\),并没有超过 \(10 ^ 9\)。
下面是简要证明:
可以发现填完之后,每一行每一列都是一个 等差序列,并且是 奇数 项,那么这就保证了 该序列的平均数一定出现在该序列中。同时,由于每次都 \(+ 1\),所以矩阵中也不会有相同的数。
接下来考虑 偶数。
从 奇数 的角度出发,但是如果直接填的话,虽然还是 等差序列,但是“平均数在序列中出现”这个限制就无法满足了。所以考虑进行一些调整,并且最终尽量缩小序列的值域(为避免超过 \(10 ^ 9\))。
可以发现当 \(n = 2\) 时是 无解 的,因为任意一行,任意一列都只有两个数,如果要使 “平均值出现在序列中” 的话,那么只有在 这两个数相等 的时候才能成立,可是题目又要求不能有相同的数字,所以 无解(换句话说就是根本没有调整的空间)。
那么接下来考虑 \(n = 4\) 的情况。
先只考虑一个序列。如果直接填,可得 \(1, 2, 3, 4\),很明显,平均数不在序列中,于是进行微调。由于是“微调”,所以尽量让平均数还是在靠近中间的位置。那么我们钦定平均数为 \(3\)(可能有人会问为什么不是 \(2\),原因是如果选了 \(2\),那么比平均数小的就只有一个 \(1\),对平均数负方向的贡献就只有 \(2 - 1 = 1\),而 \(2\) 的后面还有两个数,无法平衡),那么 \(1\) 和 \(2\) 对平均数负方向的贡献分别为 \(2\) 和 \(1\),那么第 \(4\) 个数对平均值正方向的贡献就必须是 \(1 + 2 = 3\),所以第 \(4\) 个数就应该是 \(6\)。于是便得到了如下序列:\(1, 2, 3, 6\)。
但这只是一个序列,后面还有好多个序列要填,怎么办?
考虑一个很经典的做法,将序列的每一项都乘上同一个数。
正确性还是很显然的,每一项都扩大一个相同的倍数,那么平均数也会扩大相同的倍数,并且肯定也还在序列里(如果读者还是不信,请手玩几个序列)。
同时还可以发现一个性质:将序列的每一项都加上同一个数。
每个数都加上相同的数,那么平均数也会加上相同的数,同时依然还在序列中(这不用再证明了吧)。
那么我们就可以将第一行作为 基底,第二行乘 \(2\) 倍,第三行乘 \(3\) 倍,以此类推。然后我们就会发现,虽然每一行满足条件了,但是每一列却变成了等差数列(公差为第一行对应的那一列的那个数),并且是偶数项,是不满足题意的。
那么我们可以想到,现在每一行已经满足题意了,那么就让每一行之间 相差的倍数 也满足条件即可。
这里提出了“相差的倍数”这么一个东西,是什么意思呢?
举个例子:现在给出一个合法的序列:\(1, 2, 3, 6\)。计算相邻两项的差,得到 \(1,1,3\)。如果将 差 的序列乘上 \(2\) 倍,再得到的新序列便是 \(1, 3, 5, 11\),可以发现该序列依然合法,并且平均数还是在第 \(3\) 个位置。
那么我们就让每一行之间相差的数也是一个合法的 差 的序列就可以了。
那么我们可以考虑填完第一行之后填第一列,然后根据每一行第一列的数,填完该行剩下的数。
那么整体上,我们要求每一行,小的数在左,大的数在右;每一列,小的数在上,大的数在下。
现在考虑当 \(n = 4\) 时如何填。
首先第一行前面已经讨论过:\(1, 2, 3, 6\)。
由于数字不可重复,所以第一列中除了 \(1\),不能再有 \(2, 3, 6\),那么为了方便,此处可以暴力一点,直接取 \(7\)(即为 \(6 + 1\)),也就是 \(1, 7, 13, 31\)。如下:
1 2 3 6
7
13
31
再填第二行(同理,也可以填第二列),\(7, 8, 9, 12\)(鉴于在第一列,我们已经控制好了每行之间的 差,所以对于每一行,类比于第一行,不用乘倍数,直接按照原始的 差的序列 填即可)。
第三行:\(13, 14, 15, 18\)。
第四行:\(31, 32, 33, 36\)。
于是得到了:
1 2 3 6
7 8 9 12
13 14 15 18
31 32 33 36
(最终输出时是不用管缩进的,此处只是为了看起来整齐)
同时我们可以发现,每一列中的某个数都是上面那个数再加上 差的序列 的对应的那一位再乘上第一行最后一个数那么多倍。
本来第一行中 \(1\) 和 \(2\) 之间差 \(1\),但是最后一项是 \(6\),于是第一列中 \(1\) 和 \(7\) 之间就差了 \(6\),相当于是将 差的序列 扩大了 \(6\) 倍。
举几个例子:\(8 = 2 + 1 \times 6, 15 = 9 + 1 \times 6, 36 = 18 + 3 \times 6\)。
那么这样我们还可以省掉单独填第一列的操作,直接根据第一行填剩下的所有行即可。
这就是一个合法的矩阵,但可能还是会有人质疑为什么这样就一定不会重复。下面简单证明一下(可能说得有些模糊,得感性理解)。
首先每一行填的方法肯定是合法的,那么最右边的数就是这一行的最大值,而下一行的第一个数(也就是在填第一列时已经确定的数)是已经规定要比“最大值”大的。举个例子:第一行为 \(1, 2, 3, 6\),我们规定第二行第一个数是 \(7\),同时第二行是 \(7, 8, 9, 12\),而我们第一行进行了缩放,所以第三行就刚好是 \(13\),也是大于 \(12\) 的。所以,这样填的方法一定是合法的(毕竟我也 \(AC\) 了啊)。
但是我们可以注意到一件事,由于 \(n = 4\),同时钦定第 \(3\) 个数为平均数,那么前两个数对平均数负方向的贡献全部由第 \(4\) 个数来承担了,那如果后面不只有一个数呢?也就是 \(n\) 大于 \(4\),并且是偶数的情况。
下面考虑 \(n = 6\) 的情况。
还是先看第一行,我们还是以中间点右边的的那个数作为平均数,即 \(1, 2, 3, 4, 5, 6\) 中的 \(4\)。那么前三个数对平均数负方向的贡献就是 \(3 + 2 + 1 = 6\),要分给后面两个数承担。但是我们希望最后一个数(也就是最大的数)尽量小,这样才更不会超出 \(10 ^ 9\)。那么就尽量 均摊。
接下来说明如何进行 均摊。
首先由于不能有相同的数,所以直接分成 \(3 + 3\) 是不行的,那么就还是要拆成一个序列。
还是举上面的例子,负方向的贡献分别是 \(3, 2, 1\),但是正方向却只有两个数,那很明显,最好的方法就是将 \(1\) 加到最大的数上面去,变成 \(2, 4\)(如果加到 \(2\) 上面,就又变成 \(3, 3\) 了)。
那么推广一下,正方向的贡献就应该是从 \(2\) 开始,后面有一段,每次 \(+ 1\),之后还有一个 \(+ 2\) 的(因为把 \(1\) 加上去了)。比如当 \(n = 10\),负方向为 \(5, 4, 3, 2, 1\),那么正方向就为 \(1, 2, 3, 4, 5 \to 2, 3, 4, 6\)。
不过当 \(n = 4\) 时是特殊的,只有最后一个数是正方向 \(+ 3\)。
现在算一下最大值会不会超过 \(10 ^ 9\)。
奇数的情况前面已经算过了,现在只考虑偶数。那么最大的偶数便是 \(100\)。第一行中选定的平均数为 \(51\),那么最后一列第一个数就是 \(102\)。那么最后一行最后一列的数就是 \(102 + (50 + 49 + 48 + \ldots + 1 + 2 + 3 + \ldots + 49 + 51) \times 102 = 260202 < 10 ^ 9\)。
实现
首先特判掉奇数和 \(n = 2\) 的情况,然后引入 差分数组,并进行一些微调,最后再根据发现的规律递推填充即可。
差分数组 的好处其实就是大部分相同,操作起来方便。比如当 \(n = 8\) 时,那么 差分数组 就应该是 \(1, 1, 1, 1, 2, 1, 2\)。只有两个位置是 \(2\),其余都是 \(1\)(当然要特判 \(n = 4\))。
\(Code\)
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n;
int mp[N][N]; // 填充之后的二维数组
int cha[N]; // 差分数组
signed main() {
cin >> n;
if (n == 2) return puts("-1"), 0; // 特判 n = 2 的情况
if (n & 1) { // n 为奇数
int idx = 0;
for (int i = 1; i <= n; i ++ ) {
for (int j = 1; j <= n; j ++ )
cout << ++ idx << ' '; // 每次 +1,依次输出即可
puts("");
}
return 0;
}
for (int i = 2; i <= n; i ++ ) cha[i] = 1; // 都初始成 1
if (n == 4) {
cha[n] = 3; // n = 4 的特判
} else { // 对两个位置进行更改
int mid = (n >> 1) + 2; // 虽然变量名为 mid,但并不是最中间,而是中间靠右那个数的下一位
cha[mid] ++, cha[n] ++ ; // 变成 2
}
mp[1][1] = 1;
for (int i = 2; i <= n; i ++ ) mp[1][i] = mp[1][i - 1] + cha[i]; // 填第一行
for (int i = 2; i <= n; i ++ ) { // 枚举行
for (int j = 1; j <= n; j ++ ) { // 枚举列
mp[i][j] = mp[i - 1][j] + cha[i] * mp[1][n]; // 根据第一行填下面的所有行
}
}
for (int i = 1; i <= n; i ++ ) {
for (int j = 1; j <= n; j ++ )
cout << mp[i][j] << ' '; // 输出
puts("");
}
return 0;
}