CF1668E Half Queen Cover(神仙构造题)

原题链接:https://codeforces.com/contest/1668/problem/E
这目前对我来说是极难的一道题,我只能理解解法的正确性,但究竟是如何想到的我无从得知,或许我对于这道题解法直观上的理解还差一层次。
有人曾经说过:

要掌握一个数学理论体系需要三方面的理解,即逻辑上的理解、运用上的理解和直觉上的理解,如果没有后两者,就难免觉得该理论体系枯燥难懂。

对于这题,我仅仅有了逻辑上的理解,但后两种理解还完全没有摸着门道。这种思想在之后的题目中如何应用?如何直观地理解这解法?如何让解题思路自然化?以我目前的水平是遥不可及的,如果将来我顿悟了,会在这里补上。

首先抽象化题意,构建不等式计算答案的下界。
假设放 \(k\) 个棋子就能覆盖整个棋盘,如果只考虑棋子的行列覆盖,不考虑对角线的话,不妨有 \(a\) 行没被这 \(k\) 个棋子覆盖(显然 \(n-k\le a\)),有 \(b\) 列没被这 \(k\) 个棋子覆盖(同理 \(n-k\le b\))。下一步比较巧妙,如果我们考察最左边的没有被覆盖的那一列和最上面的没有被覆盖的那一行,构成的十字型方格中共有 \(a+b-1\) 个格子没有被行列覆盖,于是它们只能被对角线覆盖,而一个对角线最多只能覆盖一个这样的格子,于是:\(a+b-1 \le k\),再根据 \(a, b\) 的范围,\(2(n-k)-1\le k\),于是 \(k\ge \lceil\dfrac{2n-1}3\rceil\)

事实上,\(k\) 能够取到下界 \(\lceil \dfrac{2n-1}3\rceil\),下面给出构造。
不妨将这个没被行列覆盖的 \((n-k)\times (n-k)\) 的子正方形摆放在棋盘的右下角。再假设放的 \(k\) 个棋子位置是 \((i, p_i)\),其中 \(i\in [1, k]\)\(p\)\(1\sim k\) 的一个全排列,根据对角线的代数性质,我们对于子正方形的最左边一列和最上面一行,建立与 \((i, p_i)\) 的等量关系:

\[\begin{cases} (k + 1) - i = (k + j) - p_i &j\in[1, n - k]\\ (k + j) - i = (k + 1) - p_i &j\in[1, n - k] \end{cases} \]

于是 \(i-p_i\in[-n+k+1, n-k-1]\),并且需要取遍范围内的所有值。这个性质和之前推导出的 \(k\) 的范围是自洽的,因为区间大小为 \(2(n-k)-1\),是小于等于 \(k\) 的。
由此,这道构造题又转化为构造映射题(这个套路我之前也见过几次)
好了,怎么构造这样的全排列 \(p\) 呢?题解给出的做法我感觉像是神来之笔,我只能理解正确性,但不知道是怎么想出来的。
思路是,分奇偶性来构造:先将范围内与 \(r-1\) 奇偶性相同的全部构造出来,再将与 \(r\) 奇偶性相同的全部构造出来,最后剩下来的一些棋子,直接让这些棋子 \(i=p_i\),即让它们等于 \(0\) 就可以了。

具体细节配合代码仔细思考(笔者感觉也只是似懂非懂):

#include <bits/stdc++.h>
using namespace std;
int n;

int main() {
	cin >> n;
	if (n == 1) { // 这种做法需要特判 n = 1 的情况
		cout << 1 << endl;
		cout << 1 << " " << 1 << endl;
		return 0;
	}
	int k = (2 * n + 1) / 3;
	cout << k << endl;
	int r = n - k - 1;
	// 下面构造了与 r - 1 奇偶性相同的项,且不会重复,注意到每次公差为 2 
	for (int i = 1, j = r; i <= j; i++, j--) {
		cout << i << " " << j << endl;
		if (i != j) cout << j << " " << i << endl;
	}
	// 下面构造了与 r 奇偶性相同的项, 且不会重复,注意到每次公差为 2 
	for (int i = r + 1, j = 2 * r + 1; i <= j; i++, j--) {
		cout << i << " " << j << endl;
		if (i != j) cout << j << " " << i << endl;
	}
	// 前面一共构造了 2(n - k) - 1 项,是小于等于 k 的
	// 剩下全部构造 0 就行 
	for (int i = 2 * r + 2; i <= k; i++) {
		cout << i << " " << i << endl;
	}
	return 0;
}

发现了一个更为简单的构造方法:https://blog.csdn.net/theophania/article/details/124350873
大致思路是归约到 \(n = 3m+2\) 的构造

posted @ 2022-05-06 13:12  alfayoung  阅读(88)  评论(0编辑  收藏  举报