【1 月小记】Part 2: NOIP 模拟赛 E
2026.1.16 NOIP 模拟赛 E
T1 字符炼金术
题意:字符有颜色、形状二维属性。两个字符不兼容当且仅当两个字符颜色、形状均不同。现有一个 \(N\times M\) 的棋盘,其中放了一些字符,你需要在一个位置上放字符,这个位置必须为空,且需要与至少一个字符相邻,且放置后不能与相邻的字符不兼容,问对于所有的字符,下一步有多少个位置可放。
考虑将每个格子的两种属性抽象到一个二维矩阵上。这样的话,两个字符兼容,当且仅当这两个字符存在公共的所在的行或列。
对于每个空格子,分类讨论它们上下左右格子的数目。这里属性完全相同的格子只算一次(使用 unique 去重)。
设相邻格子数为 \(p\)。
- \(p=0\)
不合法,不考虑 - \(p=1\)
可以选择等于该格子的 \(a\) 或 \(b\) 的任意样式 - \(p=2\)
1)如果这两个格子不共线
只能选择它俩交点交出来的两个样式
2)如果共线
只能选择线上的任意样式 - \(p=3\)
1)如果不存在两个格子共线
选不出来,不合法
2)如果存在且仅存在两个格子共线
只能选择交点交出来的唯一样式
3)如果三个格子都共线
只能选择线上的任意样式 - \(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]\) 一定已经被取走了(但不一定被谁取走)。
因为对手就是下一轮的先手,所以我们需要最小化下一回合的取数总和,即
设回合数为 \(r\),则一定满足
答案是什么呢?
我们知道,\(f_{2,j}\) 是对手从第 \(2\) 轮开始(当已取区间是 \([j,j]\),即 \(a_j\) 这一个数时)能获得的最大分数。
要最小化对手的分数,即最小化 \(f_{2,j}\),等价于最大化 \(\sum_{k=1}^{n}k-f_{2,j}\),即答案为
发现在取 \(\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 码力太弱
不要怕枚举答案,这样做暴力很有用

浙公网安备 33010602011771号