【1 月小记】Part 2: NOIP 模拟赛 E

2026.1.16 NOIP 模拟赛 E

T1 字符炼金术

题意:字符有颜色、形状二维属性。两个字符不兼容当且仅当两个字符颜色、形状均不同。现有一个 \(N\times M\) 的棋盘,其中放了一些字符,你需要在一个位置上放字符,这个位置必须为空,且需要与至少一个字符相邻,且放置后不能与相邻的字符不兼容,问对于所有的字符,下一步有多少个位置可放。

考虑将每个格子的两种属性抽象到一个二维矩阵上。这样的话,两个字符兼容,当且仅当这两个字符存在公共的所在的行或列。

对于每个空格子,分类讨论它们上下左右格子的数目。这里属性完全相同的格子只算一次(使用 unique 去重)。

设相邻格子数为 \(p\)

  1. \(p=0\)
    不合法,不考虑
  2. \(p=1\)
    可以选择等于该格子的 \(a\)\(b\) 的任意样式
  3. \(p=2\)
    1)如果这两个格子不共线
    只能选择它俩交点交出来的两个样式
    2)如果共线
    只能选择线上的任意样式
  4. \(p=3\)
    1)如果不存在两个格子共线
    选不出来,不合法
    2)如果存在且仅存在两个格子共线
    只能选择交点交出来的唯一样式
    3)如果三个格子都共线
    只能选择线上的任意样式
  5. \(p=4\)
    1)十字形或 T 字形
    只能选择交出来的那个样式
    2)全都共线
    选择线上的样式

这样一做,发现是 \(O(n^3)\) 的,过不去。所以我们想到为每行或每列维护一个加一行或一列的标记,统计答案的时候如果某一格属于已经打上标记的行或列,直接把这一格的答案加上这个标记。这个思想比较巧妙的。

显然有更好的枚举子集的方法,但是我码力太烂,写不出来

#include <bits/stdc++.h>
#define int long long
#define inf 1e18
#define debug cout << '!';
using namespace std;
constexpr int N = 1005, dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
int n, m, A, B;
struct Node {
	int a, b;
	Node() { a = b = 0; }
	Node(int x, int y) { a = x, b = y; }
	bool empty() { return (a == 0 && b == 0); }
	friend bool operator < (Node a, Node b) {
		if (a.a == b.a) return a.b < b.b;
		return a.a < b.a;
	}
	friend bool operator == (Node a, Node b) {
		return (a.a == b.a && a.b == b.b);
	}
} mp[N][N];
int ans[N][N]; // a, b
int sa[N], sb[N];
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	freopen("alchemy.in", "r", stdin);
	freopen("alchemy.out", "w", stdout);
	cin >> n >> m >> A >> B;
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			cin >> mp[i][j].a >> mp[i][j].b;
		}
	}
	for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= m; j++) {
			if (!mp[i][j].empty()) { continue; }
			int p = 0;
			vector<Node> info;
			for (int di = 0; di < 4; di++) {
				Node tmp = mp[i + dir[di][0]][j + dir[di][1]];
				if (!tmp.empty()) {
					info.push_back(tmp);
				}
			}
			sort(info.begin(), info.end());
			info.erase(unique(info.begin(), info.end()), info.end());
			p = info.size();
			// cout << '?' << p << '\n';
			if (p == 0) { continue; }
			else if (p == 1) {
				// = info[1].a / info[1].b
				// here O(n^3) !!!!!
				sa[info[0].a]++;
				sb[info[0].b]++;
				ans[info[0].a][info[0].b]--;
			}
			else if (p == 2) {
				// cout << info[0].a << ',' << info[0].b << '\n';
				// cout << info[1].a << ',' << info[1].b << '\n';
				if (info[0].a == info[1].a) {
					sa[info[0].a]++;
				} else if (info[0].b == info[1].b) {
					sb[info[0].b]++;
				} else {
					ans[info[0].a][info[1].b]++;
					ans[info[1].a][info[0].b]++;
				}
			}
			else if (p == 3) {
				if (info[0].a == info[1].a && info[1].a == info[2].a) {
					sa[info[0].a]++;
					continue;
				} else if (info[0].b == info[1].b && info[1].b == info[2].b) {
					sb[info[0].b]++;
					continue;
				}
				if (info[0].a == info[1].a) {
					ans[info[0].a][info[2].b]++; continue;
				} else if (info[1].a == info[2].a) {
					ans[info[1].a][info[0].b]++; continue;
				} else if (info[0].a == info[2].a) {
					ans[info[0].a][info[1].b]++; continue;
				} else if (info[0].b == info[1].b) {
					ans[info[2].a][info[0].b]++; continue;
				} else if (info[1].b == info[2].b) {
					ans[info[0].a][info[1].b]++; continue;
				} else if (info[0].b == info[2].b) {
					ans[info[1].a][info[0].b]++; continue;
				}
			}
			else if (p == 4) {
				if (info[0].a == info[1].a && info[1].a == info[2].a && info[2].a == info[3].a) {
					sa[info[0].a]++;
					continue;
				} else if (info[0].b == info[1].b && info[1].b == info[2].b && info[2].b == info[3].b) {
					sb[info[0].b]++;
					continue;
				}
				if (info[0].a == info[1].a && info[2].b == info[3].b) {
					ans[info[0].a][info[2].b]++; continue;
				} else if (info[0].a == info[2].a && info[1].b == info[3].b) {
					ans[info[0].a][info[1].b]++; continue;
				} else if (info[0].a == info[3].a && info[2].b == info[1].b) {
					ans[info[0].a][info[2].b]++; continue;
				} else if (info[1].a == info[2].a && info[0].b == info[3].b) {
					ans[info[1].a][info[0].b]++; continue;
				} else if (info[2].a == info[3].a && info[0].b == info[1].b) {
					ans[info[2].a][info[0].b]++; continue;
				} else if (info[1].a == info[3].a && info[0].b == info[2].b) {
					ans[info[1].a][info[2].b]++; continue;
				}
				 if (info[0].a == info[1].a && info[1].a == info[2].a) {
				 	ans[info[0].a][info[3].b]++; continue;
				 } else if (info[1].a == info[2].a && info[2].a == info[3].a) {
				 	ans[info[1].a][info[0].b]++; continue;
				 } else if (info[0].a == info[1].a && info[1].a == info[3].a) {
				 	ans[info[0].a][info[2].b]++; continue;
				 } else if (info[0].a == info[2].a && info[2].a == info[3].a) {
				 	ans[info[0].a][info[1].b]++; continue;
				 }
				 if (info[0].b == info[1].b && info[1].b == info[2].b) {
				 	ans[info[3].a][info[0].b]++; continue;
				 } else if (info[1].b == info[2].b && info[2].b == info[3].b) {
				 	ans[info[0].a][info[1].b]++; continue;
				 } else if (info[0].b == info[1].b && info[1].b == info[3].b) {
				 	ans[info[2].a][info[0].b]++; continue;
				 } else if (info[0].b == info[2].b && info[2].b == info[3].b) {
				 	ans[info[1].a][info[0].b]++; continue;
				 }
			}
		}
	}
	for (int ka = 1; ka <= A; ka++) {
		for (int kb = 1; kb <= B; kb++) {
			cout << ans[ka][kb] + sa[ka] + sb[kb] << ' ';
		}
		cout << '\n';
	}
	return 0;
}

T2 取数游戏

博弈型的 DP。这种类型的 DP 在 2025 洛谷 NOIP 模拟赛里也有一道挺好的。

一个显然的性质:两名玩家取走的数字一定构成一段连续的序列。

\(n = 3\)\(n = 7\) 的做法比较简单,暴力即可。比较理想的得分是 30 pts。

考虑 DP。设 \(f_{i, j}\) 表示第 \(i\) 回合,目前被取走的区间开头为 \(j\) 的情况下,先手最终可以取走的最大数值。

考虑将区间分段。第 \(i\) 回合能取的数字有 \(2^i\) 个,所以区间 \([1, j - 1]\) 以及区间 \([j + 2^i - 1, n]\) 一定已经被取走了(但不一定被谁取走)。

因为对手就是下一轮的先手,所以我们需要最小化下一回合的取数总和,即

\[f_{i, j} = \sum_{k = 1}^{j - 1}k + \sum_{k = j + 2 ^ i - 1}^{n}k - \min_{k = j - 2 ^ i}^{j}f_{i + 1, k} \]

设回合数为 \(r\),则一定满足

\[r = \log_2{(n + 1)} \]

答案是什么呢?

我们知道,\(f_{2,j}\) 是对手从第 \(2\) 轮开始(当已取区间是 \([j,j]\),即 \(a_j\) 这一个数时)能获得的最大分数。

要最小化对手的分数,即最小化 \(f_{2,j}\),等价于最大化 \(\sum_{k=1}^{n}k-f_{2,j}\),即答案为

\[\max{\Bigg(\sum_{k=1}^{n}k-f_{2,j}\Bigg)} \]

发现在取 \(\min f_{i + 1, k}\) 时,\(j\) 前面所依赖的转移区间长度总是固定的(即 \(2 ^ i\)),于是想到固定区间维护最值,用滑动窗口优化以下,从 70 pts 到 100 pts。

这里回合数用 0-indexed 写更方便。

#include <bits/stdc++.h>
#define int long long
#define inf 1e18
#define debug cout << '!';
using namespace std;
constexpr int N = 1048580;
int n, r, a[N], s[N];
int f[22][N];
signed main() {
	cin.tie(0) -> sync_with_stdio(0);
	freopen("game.in", "r", stdin);
	freopen("game.out", "w", stdout);
	cin >> n;
	r = log2(n + 1);
	for (int i = 1; i <= n; i++) {
		cin >> a[i];
		s[i] = s[i - 1] + a[i];
	}
	deque<int> dq;
	for (int i = r - 1; i >= 0; i--) {
		dq.clear();
		for (int j = 1; j <= n; j++) {
			// 区间长度为 (1 << i)
			while (!dq.empty() && dq.front() < max(1ll, j - (1 << i))) {
				dq.pop_front();
			}
			while (!dq.empty() && f[i + 1][dq.back()] >= f[i + 1][j]) {
				dq.pop_back();
			}
			dq.push_back(j);
			if (!dq.empty()) {
				f[i][j] = s[j - 1] + s[n] - s[min(n, max(1ll, j + (1 << i) - 2))] - f[i + 1][dq.front()];
			}
		}
	}
	int ans = 0;
	for (int j = 1; j <= n; j++) {
		ans = max(ans, s[n] - f[1][j]);
	}
	cout << ans;
	return 0;
}

总结

这场比赛理想的分数应为 100 + 30 + 12 + 15

T1 码力太弱

不要怕枚举答案,这样做暴力很有用

posted @ 2026-01-16 16:07  L-Coding  阅读(0)  评论(0)    收藏  举报