[Record] NOI 2025 场外 vp+题解
前言
场外做今年 NOI 的题目,倒属于名副其实的信心赛,毕竟区分度太低,金银交界和铜银交界都是如此。虽然其实我的分数并不高,但是差距在哪里可以看得很清楚,只要肯下死功夫,很容易弥补。
这半年里除了竞赛没有太多其他任务。水平不好说,但可能是因为心态成熟一些了,偶有爆发,数据结构一如既往很弱,人类智慧也是我没有的,但常规的套路场能打到一个相对不错的分数了。
希望大家各自精彩。 希望我们共同精彩。
vp 过程及反思
省流:(假装笔试 \(100\))\(100+(100+63+8)+(100+16+35)=422\)。勉强过银牌线。
D1T2 调太久,没冲出来,血亏。D1T3 没打暴力,更亏。
考虑到今年题目难度以及正式比赛的心态影响,接下来一年时间需要在这个分数基础上提高不少才算稳当。
2025/07/15 vp Day 1
\(\texttt{T1 robot}\):开场看笑了,简单最短路。对于每个点只需拆出出度个点,原本的点可以不保留,总点数为 \(m\),感觉没有任何问题后开写,\(\texttt{9:05}\) 通过。
\(\texttt{T2 sequence}\):用了一点时间理解题意,A 性质是显然的,花了一点时间会了第一问的 \(O(n^3)\) 做法,并发现由于随机数据下第二问不会算重,且复杂度完全卡不满,因此能够通过所有 B 性质测试点。因为 \(\texttt{for}\) 循环预处理时忘记 \(\texttt{break}\) 这一智熄错误,调到 \(\texttt{12:55}\) 才通过这部分。发现优化是可行的,没时间写了。
\(\texttt{T3 tree}\):没怎么思考这道题的暴力,拿了 8pts 就回去看 T2 了。结束后发现 56pts 是非常显然的。
Reflection
队线 256pts 对应到每道题的具体做法,都是我的大脑能够理解,且再多给一定时间我能独立做出来的。这并不意味着我现在有多高的水平,今年 NOI 区分度太低,仅此而已。
T2 因为全程完全没有怀疑预处理会写错,浪费大量时间调试,以致于并未给 T3 的暴力或者 T2 的正解留足思考时间。另一方面,我几乎是调试到最后时刻压线完成了这档分数,如果在正式赛场上,我会不会因为心态影响,面对这个没有发现的小错误直接崩盘?
而且我观察到了正解的转移形式,却在 \(\text{dp}\) 的时候选择了一个极其难以转移的地方列方程,当计算极为复杂的时候,应该多去想一想是否真的一定要在我选择的地方统计答案,而不是对着一个复杂的计算死磕优化。
T3 思考时间有限,但从另一个角度而言,一直在尝试做 AB 两个特殊性质,而完全没有考虑 \(O(\alpha n^2)\) 的暴力分,也是没有拿到大众分的原因。即使有更多的时间,如果我并不能跳出这一错误思路,我依然不可能做出来这档分。
2025/07/17 vp Day2
\(\texttt{T1 ternary}\):手玩了半个小时左右注意到答案只会被 \(110\) 和 \(101\) 形式的子串影响,\(\texttt{9:15}\) 通过。
\(\texttt{T2 set}\):\(O(2^{3n})\) 的暴力 \(\text{dp}\) 是容易做的,根本没往推式子的方向想,故而止步于此。
\(\texttt{T3 defense}\):答案显然是可以二分的,\(\text{check}\) 的策略思考了一阵子。容易发现如果当前可以使用攻击牌代替防御牌打出,且后续的攻击牌依然满足数量需求,则打出攻击牌比打出防御牌更优。处理防守牌数量的前缀和后求出前缀和数组的后缀 \(\min\) 就可以做到 \(O(n)\ \text{check}\)。前缀和的后缀 \(\min\) 显然很好线段树维护,但是询问……算了,不会。
罚坐两小时。
Reflection
纯粹水平达不到,推式子的能力还非常弱,FWT 也是不会的。这点分数已经算是目前能力能够支持的最好发挥了。
题解
D1T1 机器人 robot
朴素建分层图是 \(O(nk)\) 级别,但由于 \(p>d_u\) 时,拆出的这一个点没有出边指向别的点,即这个点只会作为转移的终点,因此建出该点是完全没有必要的。
因此对于原图中的每个点 \(u\) 拆出 \(d_u\) 个点,跑一遍最短路,最后再统一更新一遍 \(p>d_u\) 的转移即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 3e5+10;
const int M = 1e6+10;
const LL INF = 0x3f3f3f3f3f3f3f3fLL;
#define I inline
int c, n, m, k, tot;
int v[N], w[N], d[N];
LL sum[N];
vector<pair<int, int>> edge[N];
vector<int> idx[N];
struct Edge { int to, next; LL w; } e[M];
int head[M], cnt;
I void add(int u, int v, LL w) {
e[cnt] = Edge{v, head[u], w};
head[u] = cnt++;
}
struct Node {
int u; LL d;
I friend bool operator < (const Node& x, const Node& y) {
return x.d > y.d;
}
};
priority_queue<Node> q;
LL dis[M], rst[N];
bool vis[M];
void dijkstra(int s) {
memset(dis, 0x3f, sizeof dis);
memset(vis, 0, sizeof vis);
q.push(Node{s, dis[s] = 0});
while (!q.empty()) {
int u = q.top().u; q.pop();
if (vis[u]) continue;
vis[u] = true;
for (int i = head[u]; ~i; i = e[i].next) {
int v = e[i].to;
if (dis[v] > dis[u]+e[i].w) {
dis[v] = dis[u]+e[i].w;
q.push(Node{v, dis[v]});
}
}
}
}
int main() {
freopen("robot.in", "r", stdin);
freopen("robot.out", "w", stdout);
scanf("%d", &c);
scanf("%d%d%d", &n, &m, &k);
for (int i = 1; i < k; i++) scanf("%d", &v[i]);
for (int i = 2; i <= k; i++) scanf("%d", &w[i]);
for (int i = 1; i <= k; i++) sum[i] = sum[i-1]+w[i];
for (int i = 1; i <= n; i++) {
scanf("%d", &d[i]);
idx[i].emplace_back(0);
for (int j = 1; j <= d[i]; j++) idx[i].emplace_back(++tot);
edge[i].emplace_back(0, 0);
for (int j = 1; j <= d[i]; j++) {
int y, z;
scanf("%d%d", &y, &z);
edge[i].emplace_back(y, z);
}
}
memset(head, -1, sizeof head);
for (int i = 1; i <= n; i++) {
for (int j = 1; j < d[i]; j++) add(idx[i][j], idx[i][j+1], v[j]);
for (int j = d[i]; j > 1; j--) add(idx[i][j], idx[i][j-1], w[j]);
for (int j = 1; j <= d[i]; j++) {
int u = edge[i][j].first, w = edge[i][j].second;
if (j > d[u]) {
add(idx[i][j], idx[u][d[u]], sum[j]-sum[d[u]]+w);
} else {
add(idx[i][j], idx[u][j], w);
}
}
}
dijkstra(idx[1][1]);
memset(rst, 0x3f, sizeof rst);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= d[i]; j++) {
rst[i] = min(rst[i], dis[idx[i][j]]);
int u = edge[i][j].first, w = edge[i][j].second;
rst[u] = min(rst[u], dis[idx[i][j]]+w);
}
}
for (int i = 1; i <= n; i++) {
if (rst[i] == INF) rst[i] = -1;
printf("%lld ", rst[i]);
}
return 0;
}
D1T2 序列变换 sequence
如果将操作按照操作方向连成箭头,如操作 \(i,i+1\) 且此时满足 \(a_{i+1}\geq a_i\),则从 \(i\) 向 \(i+1\) 连一个右箭头,反之,从 \(i+1\) 向 \(i\) 连左箭头。
最终对序列的操作类似如下形式(竖线表示分隔独立段):
可以在 \(\triangle\) 位置进行 \(\text{dp}\),可能到达它的左右箭头显然都是 \(O(n)\) 级别的。
设 \(\text{Left},\text{Right}\) 分别为左右箭头集合,\(val_x\) 表示箭头 \(x\) 按照其所指方向以此操作一遍后,\(\triangle\) 位置被减少的值。发现对于某个位置,可以转移的左右箭头需要满足的条件为 \(val_x+val_y\leq a_i\)(\(x\in\text{Left},y\in\text{Right}\))。
将左右箭头集合按照 \(val\) 排序,双指针解决这一约束。
这样可以 \(O(n^2\log n)\) 地解决第一问。
对于第二问,这样计算可能出现算重的情况,原因在于若在一个箭头按其方向依次计算的过程中,某一元素被消为 \(0\),此时操作它与不操作它得到的序列是完全相同的。因此直接通过引入判断条件,排除掉这一情况即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 5e3+10;
const int MOD = 1e9+7;
const LL INF = 1e18;
#define add(a, b) ((a+=b)>=MOD)&&(a-=MOD)
int id, T;
int n, a[N], b[N], c[N];
struct Node { LL sum; int prd, val, idx; };
vector<Node> L[N], R[N];
LL f[N], g[N];
void preparation() {
for (int i = 1; i <= n; i++) L[i].clear(), R[i].clear();
for (int i = 1; i <= n; i++) {
LL sum = b[i]; int prd = c[i], val = a[i];
for (int j = i+1; j <= n; j++) {
if (a[j] < val || !val) break;
L[j].push_back(Node{sum, prd, val, i});
sum += b[j];
prd = (LL)prd*c[j]%MOD;
val = a[j]-val;
}
sum = b[i], prd = c[i], val = a[i];
for (int j = i-1; j >= 1; j--) {
if (a[j] <= val) break;
R[j].push_back(Node{sum, prd, val, i});
sum += b[j];
prd = (LL)prd*c[j]%MOD;
val = a[j]-val;
}
}
for (int i = 1; i <= n; i++) {
sort(L[i].begin(), L[i].end(), [](const Node& x, const Node& y) {
return x.val < y.val;
});
sort(R[i].begin(), R[i].end(), [](const Node& x, const Node& y) {
return x.val > y.val;
});
}
}
void solve() {
scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) scanf("%d", &b[i]);
for (int i = 1; i <= n; i++) scanf("%d", &c[i]);
preparation();
for (int i = 1; i <= n; i++) f[i] = -INF, g[i] = 0;
f[0] = 0; g[0] = 1;
for (int i = 1; i <= n; i++) {
f[i] = max(f[i], f[i-1]);
add(g[i], g[i-1]);
auto it = L[i].begin();
int sum = 0;
for (auto x : L[i]) {
if (x.val == a[i]) {
f[i] = max(f[i], f[x.idx-1]+x.sum+b[i]);
add(g[i], (LL)g[x.idx-1]*x.prd%MOD*c[i]%MOD);
} else {
f[i] = max(f[i], f[x.idx-1]+x.sum);
add(g[i], (LL)g[x.idx-1]*x.prd%MOD);
}
}
for (auto x : R[i]) {
f[x.idx] = max(f[x.idx], f[i-1]+x.sum);
add(g[x.idx], (LL)g[i-1]*x.prd%MOD);
}
LL mx = -INF;
for (auto x : R[i]) {
while (it != L[i].end() && it->val+x.val < a[i]) {
mx = max(mx, f[it->idx-1]+it->sum);
add(sum, (LL)g[it->idx-1]*it->prd%MOD);
++it;
}
f[x.idx] = max(f[x.idx], mx+x.sum);
add(g[x.idx], (LL)sum*x.prd%MOD);
}
}
printf("%lld %lld\n", f[n], g[n]);
}
int main() {
freopen("sequence.in", "r", stdin);
freopen("sequence.out", "w", stdout);
scanf("%d%d", &id, &T);
while (T--) solve();
return 0;
}
D2T1 三目运算符 ternary
观察过程本身确实是层层递进的,但是这里实在想不到对观察过程特别有逻辑的叙述。
显然答案只与 \(110,101\) 两种连续段有关,具体可以分类讨论如下:
- 若序列中存在 \(110\),记其中 \(0\) 的位置为 \(p\),则答案为 \(n-p+1\)。
- 若序列中不存在 \(110\) 且存在 \(101\),答案为 \(1\)。
- 若序列中既不存在 \(110\) 也不存在 \(101\),答案为 \(0\)。
对于这两种连续段的维护,固然可以维护一堆东西来分类讨论,但不妨直接维护线段树区间的前后缀 \(0/1\) 数量,拼接即可得到答案。非常好写,常数也小。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 4e5+10;
const int INF = 0x3f3f3f3f;
#define I inline
int c, t, n, q;
char str[N];
namespace S100 {
struct TreeNode {
int pre[2], suf[2], pos[2], flg[2], lazy;
I void Swap() {
swap(pre[0], pre[1]);
swap(suf[0], suf[1]);
swap(pos[0], pos[1]);
swap(flg[0], flg[1]);
}
} tree[N<<2];
#define mid ((l+r)>>1)
#define lc(x) ((x)<<1)
#define rc(x) ((x)<<1|1)
I void pushup(int x, int l, int r) {
tree[x].pos[0] = min(tree[lc(x)].pos[0], tree[rc(x)].pos[0]);
tree[x].pos[1] = min(tree[lc(x)].pos[1], tree[rc(x)].pos[1]);
tree[x].flg[0] = tree[lc(x)].flg[0]|tree[rc(x)].flg[0];
tree[x].flg[1] = tree[lc(x)].flg[1]|tree[rc(x)].flg[1];
if (tree[lc(x)].suf[1]+tree[rc(x)].pre[1] >= 2 && tree[rc(x)].pre[1] < r-mid)
tree[x].pos[0] = min(tree[x].pos[0], mid+tree[rc(x)].pre[1]+1);
if (tree[lc(x)].suf[0]+tree[rc(x)].pre[0] >= 2 && tree[rc(x)].pre[0] < r-mid)
tree[x].pos[1] = min(tree[x].pos[1], mid+tree[rc(x)].pre[0]+1);
if (tree[lc(x)].suf[0] == 1 && mid-l+1 >= 2 && tree[rc(x)].pre[1] >= 1)
tree[x].flg[0] = true;
if (tree[rc(x)].pre[0] == 1 && r-mid >= 2 && tree[lc(x)].suf[1] >= 1)
tree[x].flg[0] = true;
if (tree[lc(x)].suf[1] == 1 && mid-l+1 >= 2 && tree[rc(x)].pre[0] >= 1)
tree[x].flg[1] = true;
if (tree[rc(x)].suf[1] == 1 && r-mid >= 2 && tree[lc(x)].suf[0] >= 1)
tree[x].flg[1] = true;
tree[x].pre[0] = tree[lc(x)].pre[0];
if (tree[lc(x)].pre[0] == mid-l+1) tree[x].pre[0] += tree[rc(x)].pre[0];
tree[x].pre[1] = tree[lc(x)].pre[1];
if (tree[lc(x)].pre[1] == mid-l+1) tree[x].pre[1] += tree[rc(x)].pre[1];
tree[x].suf[0] = tree[rc(x)].suf[0];
if (tree[rc(x)].suf[0] == r-mid) tree[x].suf[0] += tree[lc(x)].suf[0];
tree[x].suf[1] = tree[rc(x)].suf[1];
if (tree[rc(x)].suf[1] == r-mid) tree[x].suf[1] += tree[lc(x)].suf[1];
}
I void pushdown(int x) {
if (tree[x].lazy) {
tree[lc(x)].lazy ^= 1;
tree[lc(x)].Swap();
tree[rc(x)].lazy ^= 1;
tree[rc(x)].Swap();
tree[x].lazy = 0;
}
}
void build(int x = 1, int l = 1, int r = n) {
tree[x].lazy = 0;
if (l == r) {
tree[x].flg[0] = tree[x].flg[1] = 0;
tree[x].pos[0] = tree[x].pos[1] = INF;
if (str[l] == '0') {
tree[x].pre[0] = tree[x].suf[0] = 1;
tree[x].pre[1] = tree[x].suf[1] = 0;
} else {
tree[x].pre[1] = tree[x].suf[1] = 1;
tree[x].pre[0] = tree[x].suf[0] = 0;
}
return;
}
build(lc(x), l, mid);
build(rc(x), mid+1, r);
pushup(x, l, r);
}
void modify(int ql, int qr, int x = 1, int l = 1, int r = n) {
if (ql <= l && qr >= r) {
tree[x].Swap(); tree[x].lazy ^= 1;
return;
}
pushdown(x);
if (ql <= mid) modify(ql, qr, lc(x), l, mid);
if (qr > mid) modify(ql, qr, rc(x), mid+1, r);
pushup(x, l, r);
}
void solve() {
build();
LL rst = 0;
// cout << tree[1].pos[0] << " " << tree[1].flg[0] << endl;
if (tree[1].pos[0] != INF) rst = n-tree[1].pos[0]+1;
else if (tree[1].flg[0]) rst = 1;
for (int i = 1; i <= q; i++) {
int l, r;
scanf("%d%d", &l, &r);
modify(l, r);
LL now = 0;
// cout << tree[1].pos[0] << " " << tree[1].flg[0] << endl;
if (tree[1].pos[0] != INF) now = n-tree[1].pos[0]+1;
else if (tree[1].flg[0]) now = 1;
rst ^= now*(i+1);
}
printf("%lld\n", rst);
}
}
void solve() {
scanf("%d%d%s", &n, &q, str+1);
S100::solve();
}
int main() {
freopen("ternary.in", "r", stdin);
freopen("ternary.out", "w", stdout);
scanf("%d%d", &c, &t);
while (t--) solve();
return 0;
}
D2T2 集合 set
最不擅长的一类推式子,将二进制数看作二进制位的集合,\(P,Q\) 即为它们构成的幂集,\(f(S)\) 即为幂集 \(S\) 中的所有集合求交得到的集合。
设 \(U_0=\{i\}_{i=0}^{n-1}\),\(U_1=\{S\mid S\subseteq U_0\}\),设 \(w(S)=\prod_{x\in S}a_x\)。
开始推式子:
考虑转换 \(\sum_{P\subseteq U_1,T_p\subseteq f(P)}w(P)\sum_{Q\subseteq U_1,T_p\subseteq f(Q),P\subseteq Q=\varnothing}w(Q)\),显然设 \(S_p,S_q\) 为 \(T_p,T_q\) 所有超集构成的集合,这个式子可以转换为 \(\sum_{P\subseteq S_p}w(P)\sum_{Q\subseteq S_q,P\cap Q=\varnothing}w(Q)\)。
此时考察式子的组合意义进行转换,若集合 \(X\in S_p\cap S_q\) 则 \(X\) 可以在 \(P,Q\) 中产生贡献,也可以不选;否则只能在 \(P,Q\) 其中一个产生贡献或者不选。因此该式等价于 \(\prod_{X\in S_p\cap S_q}(2a_X+1)\prod_{X\in S_p\triangle S_q}(a_X+1)\)。
显然 \(S_q\cap S_q\) 为 \(T_p\cup T_q\) 的全体超集构成的集合,集合对称差使用容斥拆开,可以继续化简。
设 \(A(T)=\prod_{S\subseteq T}(2a_S+1),B(T)=\prod_{S\subseteq T}{a_S+1}\),根据如上过程推式子如下:
再设 \(C(T)=\dfrac{A(T)}{2^{|T|}B(T)^2},D(T)=(-1)^{|T|}2^{|T|}B(T)\),答案式如下:
发现式子为或卷积,因此直接 FWT 即可。
对于分母为 \(0\) 的情况,在分母上带 \(\epsilon\) 即可。
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = (1<<20)+10;
const int MOD = 998244353;
const int inv2 = (MOD+1)/2;
#define I inline
int id, T, n, S;
int a[N], pw[N], cnt[N];
int qmont(int x, int k) {
int rst = 1;
while (k) {
if (k & 1) rst = (LL)rst*x%MOD;
x = (LL)x*x%MOD; k >>= 1;
}
return rst;
}
struct Number {
int x, z;
Number(int x_ = 0) {
if (x_) x = x_, z = 0;
else x = z = 1;
}
Number(int x_, int z_): x(x_), z(z_) {}
I friend Number inv(const Number& a) { return Number(qmont(a.x, MOD-2), -a.z); }
I friend Number operator + (const Number& a, const Number& b) {
if (a.z == b.z) return Number((a.x+b.x)%MOD, a.z);
return (a.z < b.z) ? a : b;
}
I friend Number operator - (const Number& a, const Number& b) {
if (a.z == b.z) return Number((a.x-b.x+MOD)%MOD, a.z);
return (a.z < b.z) ? a : Number((MOD-b.x)%MOD, b.z);
}
I friend Number operator * (const Number& a, const Number& b) {
return Number((LL)a.x*b.x%MOD, a.z+b.z);
}
};
Number A[N], B[N], C[N], D[N], invB[N], tmp[N];
void Mult(Number* arr) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < S; j++)
if (!(j&(1<<i))) arr[j] = arr[j]*arr[j^1<<i];
}
}
void FWT(Number* arr, int flag) { // 快速沃尔什变换模版
if (~flag) {
for (int i = 0; i < n; i++) {
for (int j = 0; j < S; j++)
if (j&(1<<i)) arr[j] = arr[j]+arr[j^1<<i];
}
} else {
for (int i = n-1; ~i; i--) {
for (int j = 0; j < S; j++)
if (j&(1<<i)) arr[j] = arr[j]-arr[j^1<<i];
}
}
}
void preparation() {
pw[0] = 1;
for (int i = 1; i <= 20; i++) pw[i] = (LL)pw[i-1]*inv2%MOD;
for (int i = 1; i < N; i++) cnt[i] = cnt[i^(i&(-i))]+1;
}
void solve() {
scanf("%d", &n);
S = 1<<n;
for (int i = 0; i < S; i++) scanf("%d", &a[i]);
for (int i = 0; i < S; i++) {
A[i] = Number((a[i]*2+1)%MOD);
B[i] = Number((a[i]+1)%MOD);
}
Mult(A); Mult(B);
for (int i = 0; i < S; i++) invB[i] = inv(B[i]);
for (int i = 0; i < S; i++)
C[i] = A[i]*invB[i]*invB[i]*pw[cnt[i]];
for (int i = 0; i < S; i++)
D[i] = B[i]*(1<<cnt[i])*((cnt[i]&1)?MOD-1:1);
FWT(D, 1);
for (int i = 0; i < S; i++) tmp[i] = D[i]*D[i];
FWT(tmp, -1);
int rst = 0;
for (int i = 0; i < S; i++) {
Number now = C[i]*tmp[i];
if (!now.z) ((rst += now.x)>=MOD) && (rst -= MOD);
}
printf("%d\n", rst);
}
int main() {
freopen("set.in", "r", stdin);
freopen("set.out", "w", stdout);
scanf("%d%d", &id, &T);
preparation();
while (T--) solve();
return 0;
}

浙公网安备 33010602011771号