Harbour.Space Scholarship Contest 2023-2024 (Div. 1 + Div. 2)

发布时间 2023-08-28 18:06:57作者: zsxuan

A. 给三个数 \(x\) \(y\) \(n\) 。需要构造一个长度为 \(n\) 的数组满足以下条件

  1. \(a_1 = x\), \(a_n = y\)
  2. \(a\) 严格递增。
  3. 定义 \(b_i = a_{i + 1} - a_{i}\)\(b\) 严格递减。

显然前两个条件非常宽松,定义好起始点,让 \(a\) 严格单调递增即可。
显然 \(b\)\(a\) 的差分数组,从后往前贪心地让 \(b_{n - 1} = 1,b_{n - 2} = 2, \cdots, b_2 = n - 2\) 构造出 \(a_2\)\(a_n\) 即可。然后只需要判断 \(a_2 - a_1\) 满足 \(\geq n - 1\) 时合法。

view
#include <bits/stdc++.h>

typedef long long ll;

void solve() {
	int x, y, n; std::cin >> x >> y >> n;
	std::vector<int> a(n+1);
	a[1] = x; a[n] = y;
	for (int i = n - 1, cur = 1; i >= 2; --i, cur++) {
		a[i] = a[i + 1] - cur;
	}
	if (a[2] - a[1] >= n - 1) for (int i = 1; i <= n; i++) std::cout << a[i] << " \n"[i==n];
	else std::cout << -1 << '\n';
}

int main() {
	int _ = 1; std::cin >> _;
	while ( _-- ) { solve(); }
	return 0;
}

B. 给一个长为 \(n\) 的字符串 \(s\) 和一个正整数 \(k, (k - 1 \leq n)\) 。允许做任意次以下步骤。

  1. 交换第 \(i\) 位和第 \(i + 2\) 位字符
  2. \(reverse\)\(i\) 位到 \(i + k - 1\) 位的字符

经过任意次上次操作后使 \(s\) 字典序最小。

  1. 允许无限次交换次数的情况下:第一步操作意味着可以任意排列偶数位和奇数位上的字符。
  2. 允许无限次 \(reverse\) 的情况下:
    1. \(k \& 1\)\(revrse\) 段的长度为奇数时时不会改变奇偶位。答案唯一。
    2. \(!k \& 1\)\(revrse\) 段的长度为偶数时
      1. \(k = n\) ,即整段 \(reverse\) ,互换所有的奇偶位,此时答案为两种选一。(虽然这题没这个条件,没意思)
      2. \(1 \leq k \leq n - 1\) ,可以互换任意一对奇数位元素和偶数位元素。

故讨论 \(k\&1\)\(!k\&1\ and\ k=n\)\(!k\&1\ and\ k<n\) 三种情况即可。

view
#include <bits/stdc++.h>

typedef long long ll;
char s[2000005], s_odd[2000005], s_even[2000005];
void solve() {
	int n, k; std::cin >> n >> k;
	for (int i = 1; i <= n; i++) {
		std::cin >> s[i];
		if (i & 1) s_odd[(i+1)/2] = s[i];
		else s_even[i/2] = s[i];
	}
	std::string ans = "";
	if (k & 1) {
		std::sort(s_odd + 1, s_odd + (n+1)/2 + 1, [&](char a, char b){
			return a < b;
		});
		std::sort(s_even + 1, s_even + n/2 + 1, [&](char a, char b){
			return a < b;
		});
		for (int i = 1; i <= n/2; i++) ans += s_odd[i], ans += s_even[i];
		if (n&1) ans += s_odd[n/2+1];
	}
	else {
		if (k == n) {
			std::string ts1 = "", ts2 = "";
			
			std::sort(s_odd + 1, s_odd + (n+1)/2 + 1, [&](char a, char b){
				return a < b;
			});
			std::sort(s_even + 1, s_even + n/2 + 1, [&](char a, char b){
				return a < b;
			});
			for (int i = 1; i <= n/2; i++) ts1 += s_odd[i], ts1 += s_even[i];
			if (n&1) ans += s_odd[n/2+1];
			
			for (int i = n; i; --i) {
				int l = n - i + 1;
				if (l & 1) s_odd[(l+1)/2] = s[i];
				else s_even[l/2] = s[i];
			}
			
			std::sort(s_odd + 1, s_odd + (n+1)/2 + 1, [&](char a, char b){
				return a < b;
			});
			std::sort(s_even + 1, s_even + n/2 + 1, [&](char a, char b){
				return a < b;
			});
			for (int i = 1; i <= n/2; i++) ts2 += s_odd[i], ts2 += s_even[i];
			if (n&1) ans += s_odd[n/2+1];
			
			ans = std::min(ts1, ts2);
		}
		if (k < n) {
			std::sort(s + 1, s + n + 1, [&](char a, char b){
				return a < b;
			});
			for (int i = 1; i <= n; i++) ans += s[i];
		}
	}
	
	std::cout << ans << '\n';
}

int main() {
	int _ = 1; std::cin >> _;
	while ( _-- ) { solve(); }
	return 0;
}

C. 给一个数 \(x\) ,把它变成 \(1\) 。具体操作为每次可以让 \(x\) 减去一个它的因子,但是每个因子不能减去超过两次。

题目其实有引导性地让我们分两步做,每一步每个因子至多会用到一次。

方法:以二进制形式观察 \(x\)

  1. 第一步,让 \(x\) 每次减去最低位的 \(1\) 直到 \(x\) 只余一个 \(1\)
  2. 第二部,让 \(x\)\(1\) 每次右移一位直到变为 \(\cdots 0001\)

第一步,显然二进制上任意一个 \(1\) 的权重不同,不会重复。

只需证: \(x\) 最低位的 \(1\) 一定是它的因子 。(此题主要考点)
证明:将二进制低位到高位编号,最低位为 \(0\) 。第 \(k\)\(1\) 代表 \(2^{i_k}\)。显然有 \(2^{i_1} | 2^{i_1} + 2^{i_2} + \cdots + 2^{i_k}\) 。qed。

第二步,也显然余下的 \(x\) 只剩下一个 \(1\)\(2^m\) 的形式,每次减去一半既是它的因子,也不重复。

两步分别用到的因子不重复,故总的用到的因子不会重复 \(2\) 次以上。

view
#include <bits/stdc++.h>
typedef long long ll;
void solve() {
	int x; std::cin >> x;
	std::vector<int> ans(1, x);
	while ( __builtin_popcount(x) != 1 ) {
		x -= ( x & ( -x ) );
		ans.push_back(x);
	} 
	while (x != 1) {
		x >>= 1;
		ans.push_back(x);
	}
	int m = ans.size(); std::cout << m << '\n';
	for (int i = 0; i < m; i++) std::cout << ans[i] << " \n"[i==m-1];
}

int main() {
	int _ = 1; std::cin >> _;
	while ( _-- ) { solve(); }
	return 0;
}

D. 给一个 \(n \times n\)\(01\) 网格。定义 \(a_{i, j}\) 的一次翻转为 \(a_{i, j} \oplus 1\)

\((i, j)\) 位置的格子翻转会产生以下影响:

  1. 自身翻转,即 \(a_{i, j} = a_{i, j} \oplus 1\)
  2. 任意 \((x, y)\) 位置满足 \(x > i\) 的格子翻转,满足 \(x - i \geq |y - j|\) 的格子翻转。

解方程:
不要被笛卡尔坐标系给荼毒了……。

\[\begin{cases} x > i \\ x - i \geq |y - j| \end{cases} \Rightarrow \begin{cases} x - y \geq i - j, \quad x > i, y \geq j \\ x + y \geq i + j, \quad x > i, y \leq j \end{cases} \]

在所给坐标系中是垂直向下九十度的扇形。

考虑:

  1. \(01\) 翻转最 \(naive\) 的性质:一个位置翻转 \(n\) 次,只有其中的 \(n\&1\) 次是有意义的。
  2. 网格内操作某个格子带动翻转一个区域且“翻转形式唯一或能转化为翻转形式唯一”的情形,具备一些性质:(只有区域翻转形式唯一情况下才能直接进行数理讨论,否则考虑 \(DP\) 等方案)
    1. 任何方向有后效性的翻转:当边界行(通常选第一行)的状态确定,整个方格的状态唯一确定。可以知晓有无解,有解时的最小操作次数。
      • 证明:当边界行状态确定,由于后效性存在,不存在一种方案可以维持已确定的边界行的状态且改变其他行。
      • 推论:当边界格的状态唯一确定,整行的状态唯一确定。
    2. 存在或可转化为一个方向无后效性的翻转:贪心地选择无后效性的方向翻转一定有解,当方向唯一时最小操作次数唯一。(比如此题的格子翻转严格影响向下的区域)
      • 证明:有解性显然。最优性可以反证按其他方向翻转不会更优。

于是此题变为,如何在 \(O(n^2)\) 或者 \(O(n^2 \log n)\) 的复杂度内,完成从第 \(1\) 行到第 \(n\) 行逐行操作所有 \(1\) 的格子。

  1. 仅按从上到下的方向操作所有 \(1\) 便是 \(O(n^2)\) 的,所以操作复杂度需要是 \(O(1)\)\(O(\log n)\)
  2. 于是面临两个问题:
    • 空间允许情况下容易想到二维区间修改,很像需同时修改和询问。
    • 差分空间非水平,而是斜率为 \(1\)\(-1\) 的空间。
      这调用 \(fenwik\ tree\) 将会是一套繁杂的推式子和代码实现。
  3. 向下斜率为正负 \(1\) 的区间修改问题,是经典的 \(tag\) 下放问题。
    • 我们只需要在 \(\mod 2\) 意义即位异或意义下,下放 \(tag\) 合并更新即可。
    • 三叉树中 \(tag\) 的下放比二叉树中 \(tag\) 的下放区别:
      • 二叉树的 \(tag\) 压标记时直接压到左右儿子即可,常见于二叉搜索树的 \(lazy\ tag\)
      • 三叉树的 \(tag\) 压标记时也只把标记压到左右儿子.若左右儿子都存在,则给中孙子放一个负标记(如果它存在)。更新信息时不仅要加上自己的标记,还要加上父亲的标记。
        (待补图)

时间复杂度 \(O(n^2)\)

view
#include <bits/stdc++.h>
typedef long long ll;
void solve() {
	int n; std::cin >> n;
	std::vector<std::vector<int> > a(n+1, std::vector<int>(n+1)), tag(n+1, std::vector<int>(n+1));
	
	for (int i = 1; i <= n; i++) {
		std::string s; std::cin >> s;
		for (int j = 1; j <= n; j++) {
			a[i][j] = (s[j - 1] - '0');
		}
	}
	
	auto settag = [&](int i, int j, int v) {
		tag[i][j] = ( tag[i][j] + v ) % 2;
	};
	
	auto pushdown = [&](int i, int j) {
		if (i + 1 <= n) { // 下一层存在 
			a[i + 1][j] = ( a[i + 1][j] + tag[i][j] ) % 2; // 更新中儿子信息 
			if (j - 1 >= 1) { // 标记压到左儿子,更新左右子信息 
				tag[i + 1][j - 1] = ( tag[i + 1][j - 1] + tag[i][j] ) % 2;
				a[i + 1][j - 1] = ( a[i + 1][j - 1] + tag[i][j] ) % 2;
			}
			if (j + 1 <= n) { // 标记压到右儿子,更新右儿子信息 
				tag[i + 1][j + 1] = ( tag[i + 1][j + 1] + tag[i][j] ) % 2;
				a[i + 1][j + 1] = ( a[i + 1][j + 1] + tag[i][j] ) % 2;
			}
		}
		if (i + 2 <= n && j - 1 >= 1 && j + 1 <= n) { // 中孙子在存在且左右儿子都存在则打上负标记 
			tag[i + 2][j] = ( tag[i + 2][j] - tag[i][j] + 2 ) % 2; 
			a[i + 2][j] = ( a[i + 2][j] - tag[i][j] + 2 ) % 2;
		}
		tag[i][j] = 0; // 压完标记清空 
	};
	
	int ans = 0;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= n; j++) {
			if (a[i][j]) ans++, settag(i, j, 1);
			pushdown(i, j);
		}
	}
	
	std::cout << ans << '\n';
}
int main() {
	int _ = 1; std::cin >> _;
	while ( _-- ) { solve(); }
	return 0;
}