2025 NOI 模拟赛 2
2025 NOI 模拟赛 2
得分
T1 | T2 | T3 | 得分 | 排名 |
---|---|---|---|---|
\(70\) | \(100\) | \(43\) | \(213\) | \(1/4\) |
题解
T1 多米诺
容易发现,由于 \(H,W\) 过大,所以实际上有很多列和行是没有骨牌放置的。那么我们可以将空着的列删去,这样的话网格就会被分成若干个长条,他们的宽度不超过 \(2n\)。对于长条采取同样的策略,将空着的行删去,这样我们就会得到若干个大小不超过 \(2n\times 2n\) 的方格。
先求出在这个方格中放置数字的方案数,采取轮廓线 dp,设 \(dp(i,j,S,k)\) 表示当前准备考虑格子 \((i,j)\),当前轮廓线插头状态为 \(S\)(\(1\) 表示上方有纵向骨牌放置,否则表示没有),已经放了 \(k\) 个骨牌的方案数。枚举第二维宽度然后各跑一遍即可,复杂度 \(O(n^42^{2n})=O(n^44^n)\)。
然后我们考虑合并成长条的方案数。首先有一个问题是我们上面求出的答案并不能保证每一列上都有骨牌覆盖,所以考虑容斥,枚举第一个空列,强制前面的没有空列即可。然后我们需要将这些方块合成长条,跑一边朴素的背包即可,记录段数和列数总和,最后乘一个组合数即可。直接暴力写是 \(O(n^7)\) 的,足够通过了。并没有看懂题解的 \(O(n^3)\) 实现方式是什么意思。
#include <bits/stdc++.h>
#include "domino.h"
#define il inline
using namespace std;
const int Maxn = 2e5 + 5;
const int Mod = 1e9 + 7;
il int Add(int x, int y) {return x + y >= Mod ? x + y - Mod: x + y;} il void pls(int &x, int y) {x = Add(x, y);}
il int Del(int x, int y) {return x - y < 0 ? x - y + Mod : x - y;} il void sub(int &x, int y) {x = Del(x, y);}
il int qpow(int a, int b, int P = Mod) {int res = 1; for(; b; a = 1ll * a * a % P, b >>= 1) if(b & 1) res = 1ll * res * a % P; return res;}
il int Inv(int a) {return qpow(a, Mod - 2);}
template <typename T> il void chkmax(T &x, T y) {x = (x >= y ? x : y);}
template <typename T> il void chkmin(T &x, T y) {x = (x <= y ? x : y);}
template <typename T>
il void read(T &x) {
x = 0; char ch = getchar(); bool flg = 0;
for(; ch < '0' || ch > '9'; ch = getchar()) flg = (ch == '-');
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
flg ? x = -x : 0;
}
template <typename T>
il void write(T x, bool typ = 1) {
static short Stk[50], Top = 0;
x < 0 ? putchar('-'), x = -x : 0;
do Stk[++Top] = x % 10, x /= 10; while(x);
while(Top) putchar(Stk[Top--] | 48);
typ ? putchar('\n') : putchar(' ');
}
int N, M;
int dp[16][16][1 << 14][16];
int ans[16][16][16];
void init(int W, int n) {
for(int i = 0; i <= N; i++)
for(int j = 0; j <= W + 1; j++)
for(int S = 0; S < (1 << W); S++)
for(int k = 0; k <= n; k++) dp[i][j][S][k] = 0;
dp[1][1][0][0] = 1;
for(int i = 1; i <= N; i++) {
for(int j = 1; j <= W; j++) {
for(int S = 0; S < (1 << W); S++) {
for(int k = 0; k <= n; k++) {
if(!((S >> j - 1) & 1) && !((S >> j) & 1) && j != W) pls(dp[i][j + 2][S][k + 1], dp[i][j][S][k]);
if(!((S >> j - 1) & 1)) pls(dp[i][j + 1][S ^ (1 << j - 1)][k + 1], dp[i][j][S][k]);
if((S >> j - 1) & 1) pls(dp[i][j + 1][S ^ (1 << j - 1)][k], dp[i][j][S][k]);
else pls(dp[i][j + 1][S][k], dp[i][j][S][k]);
}
}
}
for(int k = 0; k <= n; k++) ans[i][W][k] = dp[i][W + 1][0][k];
for(int S = 0; S < (1 << W); S++) {
for(int k = 0; k <= n; k++) {
if(i < N) pls(dp[i + 1][1][S][k], dp[i][W + 1][S][k]);
}
}
}
}
int C(int n, int m) {
if(n < m || n < 0 || m < 0) return 0;
int res = 1;
for(int i = 0; i < m; i++) res = 1ll * res * (n - i) % Mod * Inv(m - i) % Mod;
return res;
}
int f[30][10], g[30][10];
int ddp[30][30][10];
int work(int x, int w, int H) {
f[0][0] = g[0][0] = 1;
for(int i = 1; i <= N; i++) {
f[i][0] = 1;
for(int j = 1; j <= w; j++) {
g[i][j] = f[i][j] = ans[i][x][j];
for(int k = 1; k <= i; k++) {
for(int p = 0; p <= j; p++) {
sub(g[i][j], 1ll * g[k - 1][p] * f[i - k][j - p] % Mod);
}
}
}
}
ddp[0][0][0] = 1;
for(int i = 1; i <= N; i++) {
for(int j = 1; j <= N; j++) {
for(int k = 1; k <= w; k++) {
ddp[i][j][k] = 0;
for(int p = 1; p <= j; p++) for(int q = 1; q <= k; q++)
pls(ddp[i][j][k], 1ll * ddp[i - 1][j - p][k - q] * g[p][q] % Mod);
}
}
}
int ans = 0;
for(int i = 1; i <= N; i++) {
for(int j = 1; j <= N; j++) {
int ret = H - j;
pls(ans, 1ll * ddp[i][j][w] * C(ret + 1, i) % Mod);
}
}
return ans;
}
int F[30][30], G[30][30];
int place_dominoes(int H, int W, int n) {
N = min(H, n << 1), M = min(W, n << 1);
for(int i = 1; i <= M; i++) init(i, n);
F[0][0] = G[0][0] = 1;
for(int i = 1; i <= M; i++) {
F[i][0] = 1;
for(int j = 1; j <= n; j++) {
G[i][j] = F[i][j] = work(i, j, H);
for(int k = 1; k <= i; k++) {
for(int p = 0; p <= j; p++) {
sub(G[i][j], 1ll * G[k - 1][p] * F[i - k][j - p] % Mod);
}
}
}
}
ddp[0][0][0] = 1;
for(int i = 1; i <= M; i++) {
for(int j = 1; j <= M; j++) {
for(int k = 1; k <= n; k++) {
ddp[i][j][k] = 0;
for(int p = 1; p <= j; p++) for(int q = 1; q <= k; q++)
pls(ddp[i][j][k], 1ll * ddp[i - 1][j - p][k - q] * G[p][q] % Mod);
}
}
}
int ans = 0;
for(int i = 1; i <= M; i++) {
for(int j = 1; j <= M; j++) {
int ret = W - j;
pls(ans, 1ll * ddp[i][j][n] * C(ret + 1, i) % Mod);
}
}
return ans;
}
T2 slot
首先先得出一个答案的上界,显然如果一个 slot 中有两个相同颜色,那么最多取颜色数加一次即可。
然后考虑其他策略。首先容易发现取球最多在两个 slot 中完成。我们枚举一下第一个 slot,然后计算出这个 slot 中每个元素在这个 slot 外面的 slot 最少需要取多少次才能拿到,如果这个元素只在这个 slot 中出现设为 \(\infty\)。然后由于我们是在最坏决策下行动,所以我们一定会先拿到需要次数多的颜色。于是将每种颜色按照还需要取得次数从大到小排序,从前往后依次取然后取 \(\min\) 就是答案。
#include <bits/stdc++.h>
#include "slot.h"
#define il inline
using namespace std;
const int Maxn = 2e5 + 5;
const int Inf = 2e9;
template <typename T> il void chkmax(T &x, T y) {x = (x >= y ? x : y);}
template <typename T> il void chkmin(T &x, T y) {x = (x <= y ? x : y);}
template <typename T>
il void read(T &x) {
x = 0; char ch = getchar(); bool flg = 0;
for(; ch < '0' || ch > '9'; ch = getchar()) flg = (ch == '-');
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
flg ? x = -x : 0;
}
template <typename T>
il void write(T x, bool typ = 1) {
static short Stk[50], Top = 0;
x < 0 ? putchar('-'), x = -x : 0;
do Stk[++Top] = x % 10, x /= 10; while(x);
while(Top) putchar(Stk[Top--] | 48);
typ ? putchar('\n') : putchar(' ');
}
int len[Maxn];
multiset <int> S[Maxn];
int cnt[Maxn], vis[Maxn];
vector <vector <int>> V, W;
int solve(vector<vector<int>> C) {
int n = C.size();
int ans = Inf;
V.resize(n), W.resize(n);
for(int i = 0; i < n; i++) {
len[i] = C[i].size();
V[i].clear(), W[i].clear();
for(int j = 0; j < len[i]; j++) {
cnt[C[i][j]]++;
if(!vis[C[i][j]]) V[i].push_back(C[i][j]), vis[C[i][j]] = 1;
}
len[i] = V[i].size();
for(int j = 0; j < len[i]; j++) {
if(cnt[V[i][j]] > 1) chkmin(ans, (int)V[i].size() + 1);
S[V[i][j]].insert(V[i].size());
cnt[V[i][j]] = vis[V[i][j]] = 0;
}
}
for(int i = 0; i < n; i++) {
for(int j = 0; j < len[i]; j++) {
if(S[V[i][j]].size() <= 1) W[i].push_back(Inf);
else {
S[V[i][j]].erase(S[V[i][j]].find(V[i].size()));
int mn = *S[V[i][j]].begin();
W[i].push_back(mn); S[V[i][j]].insert(V[i].size());
}
}
sort(W[i].begin(), W[i].end(), greater<int>());
for(int j = 0; j < len[i]; j++) chkmin(ans, W[i][j] + j + 1);
}
for(int i = 0; i < n; i++) for(int j = 0; j < len[i]; j++) S[V[i][j]].clear();
return ans;
}
T3 树
令 \(f_{n,m}\) 表示 \(n,m\) 对应的答案,枚举根节点左右两侧的叶子个数,有转移方程:
然后发现这个转移形式比较类似于卡特兰数的转移,考虑用组合意义描述一下。我们对二叉树进行先序遍历,每一次走左儿子写下一个 \(1\),走右儿子写下一个 \(-1\),这样的话序列将会满足两个限制:
- 前缀和时刻 \(\ge 0\),因为每一次在走右儿子前都走了一个对应的左儿子。
- 前缀和时刻 \(\le m-2\),因为前缀和描述的实际上是走到一个点向左走的次数,而如果这个次数 \(\ge m-1\) 就意味着至少经过了 \(m\) 个点,这个树自然包含 \(m\) 连树。
而这个序列显然是由 \(n-1\) 个 \(1\) 和 \(n-1\) 个 \(-1\) 组成的,所以我们现在要求出所有满足前缀和 \(\ge 0\) 且 \(\le m-2\) 的所有序列个数。这个就是一个反射容斥的经典题目了,限制有两条所以枚举对称结果然后组合数计算即可。复杂度 \(O(n)\)。
#include <bits/stdc++.h>
#include "tree.h"
#define il inline
using namespace std;
const int Maxn = 2e7 + 5;
const int Inf = 2e9;
const int Mod = 998244353;
il int Add(int x, int y) {return x + y >= Mod ? x + y - Mod: x + y;} il void pls(int &x, int y) {x = Add(x, y);}
il int Del(int x, int y) {return x - y < 0 ? x - y + Mod : x - y;} il void sub(int &x, int y) {x = Del(x, y);}
il int qpow(int a, int b, int P = Mod) {int res = 1; for(; b; a = 1ll * a * a % P, b >>= 1) if(b & 1) res = 1ll * res * a % P; return res;}
il int Inv(int a) {return qpow(a, Mod - 2);}
template <typename T> il void chkmax(T &x, T y) {x = (x >= y ? x : y);}
template <typename T> il void chkmin(T &x, T y) {x = (x <= y ? x : y);}
template <typename T>
il void read(T &x) {
x = 0; char ch = getchar(); bool flg = 0;
for(; ch < '0' || ch > '9'; ch = getchar()) flg = (ch == '-');
for(; ch >= '0' && ch <= '9'; ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
flg ? x = -x : 0;
}
template <typename T>
il void write(T x, bool typ = 1) {
static short Stk[50], Top = 0;
x < 0 ? putchar('-'), x = -x : 0;
do Stk[++Top] = x % 10, x /= 10; while(x);
while(Top) putchar(Stk[Top--] | 48);
typ ? putchar('\n') : putchar(' ');
}
int fac[Maxn], inv[Maxn];
void init(int n) {
fac[0] = 1; for(int i = 1; i <= n; i++) fac[i] = 1ll * fac[i - 1] * i % Mod;
inv[n] = Inv(fac[n]); for(int i = n; i >= 1; i--) inv[i - 1] = 1ll * inv[i] * i % Mod;
}
int C(int n, int m) {
if(n < 0 || m < 0 || n < m) return 0;
return 1ll * fac[n] * inv[n - m] % Mod * inv[m] % Mod;
}
struct Data {
int x, y;
int calc() {return C(x + y, x);}
void ref(int p) {int tx = x, ty = y; x = ty - p, y = tx + p;}
int chk() {return x > 0 && y > 0;}
}P, Q;
int count(int n, int m) {
init(2 * n);
P = {n - 1, n - 1}, Q = {n - 1, n - 1};
int ans = P.calc();
int opt = 0, a[2] = {-1, m - 1};
while(P.chk() && Q.chk()) {
P.ref(a[opt]), Q.ref(a[opt ^ 1]);
if(opt == 0) sub(ans, Add(P.calc(), Q.calc()));
else pls(ans, Add(P.calc(), Q.calc()));
opt ^= 1;
}
return ans;
}