线段树
比赛概述
A、C、D 来自小猫,B 来自 ZYZ.
验题人是小猫和 GPT5.
四道题都是小猫场切过的。小猫写四道题的时间大概分别是,10min + 50min + 2h + 1.5h.
小猫一合计,诶,这不就是一场 NOIP 模拟赛的 easy ver. 嘛!
于是就有了这场比赛。
关于难度
验题人(?)GPT5 在本场比赛里获得了 \(100+100+0+100\) 的分数,可喜可贺。
小猫认为难度大概是黄、绿、紫、蓝。
ydtz 认为是绿、绿、蓝/紫、蓝/紫。
关于算法
先声明这场不是数学 Round 或计数 Round.
感觉上 T4 相较于 counting 更偏 ds 和思维一点。
A. 航海马(sail)
航海的马与击球手散养的白鹅大战,发出的呱呱声吵醒了林中的猕猴,声音刺耳,令人心灵震荡不已,吵得猕猴赶紧戴上耳塞【数据删除】,却因违反禁止带耳机的校规被 lz 发了黄牌。猴兄驱赶不停嗷嗷的蛐蛐,又骑着永康的公牛飞上岁岁似今朝的浅云,公牛张翼翔飞,又不慎掉到了一条峻溪当中,发出了 hiphop 的水花声。峻溪十分险峻,还有千回百转的折壕,只有【数据删除】的光芒可以指引前路。猴兄根据【数据删除】的指引来到了涌溪,遇到了翼德天尊和 PYD1,两人问猴兄:“你掉的是这块夏令营银牌还是这两块 NOI 铜牌?”猴兄说:“Wishtoday!不要剥削!”
多年以后,当航海马追忆过去,这段话忽然出现在脑海。他虔诚地天上的神明祈祷,回应他的却只有他自己。
原题是 CF2042C.
考虑最终的序列是单调不降的,它可以拆成不超过 \(n\) 个后缀相加。
每个后缀之间的贡献其实是独立的。于是我们直接记 \(a_i\) 表示选择后缀 \([i, n]\) 对答案产生的贡献。贪心地取所有贡献 \(> 0\) 的后缀即可。
#include <bits/stdc++.h>
using namespace wqstd;
const int N = 2e5 + 5;
int n, k, suf[N];
char s[N];
void solve()
{
cin >> n >> k;
cin >> (s + 1);
suf[n + 1] = 0;
for(int i = n; i >= 1; i--)
suf[i] = suf[i + 1] + (s[i] == '1' ? 1 : -1);
priority_queue<int> q;
int ans = 1, cur = 0;
for(int i = n; i > 1; i--) q.push(suf[i]);
while(!q.empty())
{
if(q.top() <= 0) break;
cur += q.top(); q.pop();
ans++;
if(cur >= k) break;
}
if(cur < k) cout << -1 << '\n';
else cout << ans << '\n';
return;
}
int main()
{
freopen("sail.in", "r", stdin);
freopen("sail.out", "w", stdout);
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
int t; cin >> t;
while(t--) solve();
return 0;
}
验题人评价


B. 树上旅行(tree)
#include <bits/stdc++.h>
#define ll long long
#define fi first
#define se second
#define pii pair<int,int>
#define pll pair<ll,ll>
#define ull unsigned long long
using namespace std;
const int iinf=1<<30;
const ll linf=1ll<<62;
const int P=998244353;
int n,q;
stack<array<int,2>> stk;
ll base[100011],s[100011],pos=1;
ll mpow(ll x,int y){
if(y==0) return 1;
if(y&1) return mpow(x*x%P,y/2)*x%P;
return mpow(x*x%P,y/2);
}
int main(){
cin.tie(0);
cout.tie(0);
ios::sync_with_stdio(0);
cin >> n >> q;
base[0]=1;
for(int i=1;i<=100000;i++)
base[i]=base[i-1]*n%P;
s[1]=1;
for(int i=2;i<=100000;i++)
s[i]=(s[i-1]+base[i-1])%P;
while(q--){
int x,y;
cin >> x >> y;
if(x!=0){
pos=((pos*base[y]%P+(1+x-n)*s[y]%P)%P+P)%P;
stk.push({x,y});
}else{
while(stk.size()&&y>=stk.top()[1]){
pos=(pos-(1+stk.top()[0]-n)*s[stk.top()[1]]%P+P)%P;
pos=pos*mpow(base[stk.top()[1]],P-2)%P;
y-=stk.top()[1];
stk.pop();
}
if(y){
pos=(pos-(1+stk.top()[0]-n)*s[y]%P+P)%P;
// cout << pos << ' ';
pos=pos*mpow(base[y],P-2)%P;
// cout << pos << ' ';
array<int,2> t=stk.top();
t[1]-=y;
stk.pop();
stk.push(t);
}
}
cout << pos << '\n';
}
return 0;
}
验题人评价


C. 好朋友(npy)
首先不难发现序列的 LIS 和 LDS 恰有一个交点,且交点位置一定是 \(a_{n}\). 发现 \(n\) 前后是独立的,可以分别计算。
对前半部分,相当于要在 \([1, n-1]\) 填入 \(1, 2, \cdots a_{n}-1\) 这个升序序列与 \(n, n-1, \cdots, a_{n} + 1\) 这个降序序列。后半部分同理。
注意到 B 性质是有用的,它相当于左半部分和中间的值唯一确定了,只需考虑右半部分。然后我们的正解其实就是 B 性质的做法分别对两边做,然后在中间合并。
Solution B-1
B 性质,左半部分和中间的值唯一确定了,只需考虑右半部分。
\(f_{i, j, k}\) 表示后半部分从后往前填完前 \(i\) 个位置,升序序列填到 \(j\),降序序列填到 \(k\) 的方案数. 可以得到 \(0\) 分。
Solution 1
根据 Sol B-1 不难想到两半都这样 DP,然后从中间拼起来。可以得到 \(28\) 分。
Solution B-2
注意到之前做法中第三维状态是没用的(即可以根据前两维状态唯一确定第三维)。
于是考虑 \(f_{i, j}\) 表示前半部分填完前 \(i\) 个位置,当前升序序列填到了 \(j\) 的方案数。转移如下:
Solution 2
考虑最终的答案。
- 如果 \(a_n=0\),则答案是 \(\displaystyle \sum_{i=0}^{n-1} f_{n-1,i} \times g_{n+1,i}\).
- 否则,答案是 \(f_{n-1,a_n-1} \times g_{n+1,a_n+1}\).
这样可以获得 \(52\) 分。
Solution A
A 性质,容易发现 \(a_i = 0\) 时式子变成了组合数。\(O(n)\) 预处理阶乘和阶乘逆元即可。
和 Sol2 拼起来可以获得 \(56\) 分。
Solution B-3
继续思考,在做 A 性质的时候我们利用了 \(a_i = 0\) 时转移式变成组合数的性质。进一步可以发现,如果当前位置是 \(i\),上一个 \(a_i \ne 0\) 的位置是 \(i'\),则 \(f_{i', j'} \to f_{i, j}\) 的贡献就是 \(\displaystyle {i-i' \choose j-j'} \times f_{i', j'}\).
而我们可以注意到 \(a_i \ne 0\) 时,值非零的 \(f_{i, j}\) 其实只有 \(2\) 个。于是我们存下每个 \(i\) 对应的这两个 \(j\),然后暴力转移即可。
Solution 3
就是 Sol B-3 拼起来,没什么好说的。
可以获得 \(100\) 分。
#include <bits/stdc++.h>
using namespace std;
const int mod = 998244853, M = 1e6 + 5;
int n, a[M * 2], f[M], g[M], b[M], fac[M], inv[M], ls, h[M][2], s[M][2];
inline int ksm(int x, int y)
{
int res = 1;
while(y)
{
if(y & 1) res = 1ll * res * x % mod;
x = 1ll * x * x % mod, y >>= 1;
}
return res;
}
int C(int n, int m)
{
if(n < 0 || m < 0 || n < m) return 0;
return 1ll * fac[n] * inv[m] % mod * inv[n - m] % mod;
}
int calc(int ls, int p, int x)
{
int res = 0;
for(int o = 0; o <= 1; o++)
{
if(h[ls][o] == -1) continue;
res = (res + 1ll * s[ls][o] * C(p - ls, x - h[ls][o]) % mod) % mod;
}
return res;
}
void solve()
{
cin >> n;
for(int i = 1; i <= 2 * n - 1; i++) cin >> a[i];
for(int i = 1; i <= n; i++) b[i] = a[2 * n - i];
for(int j = 0; j <= n; j++) f[j] = g[j] = 0;
for(int i = 1; i <= n; i++) h[i][0] = h[i][1] = -1, s[i][0] = s[i][1] = 0;
h[0][0] = 0, s[0][0] = 1, ls = 0;
for(int i = 1; i <= n - 1; i++)
{
if(a[i])
{
if(a[i] > 0)
{
int s1 = calc(ls, i - 1, a[i] - 1); // ls 贡献到 f[i - 1][a[i] - 1];
if(s1) h[i][0] = a[i], s[i][0] = s1;
}
if(i >= n - a[i] + 1)
{
if(i - (n - a[i] + 1) < i)
{
int s2 = calc(ls, i - 1, i - (n - a[i] + 1));
if(h[i][0] != -1 && h[i][0] != i - (n - a[i] + 1) && s[i][0] != 0) h[i][1] = i - (n - a[i] + 1), s[i][1] = s2;
else h[i][0] = i - (n - a[i] + 1), s[i][0] += s2, s[i][0] %= mod;
}
}
ls = i;
}
}
for(int i = 0; i <= n - 1; i++) f[i] = calc(ls, n - 1, i);
for(int i = 1; i <= n; i++) h[i][0] = h[i][1] = -1, s[i][0] = s[i][1] = 0;
h[0][0] = 0, s[0][0] = 1, ls = 0;
for(int i = 1; i <= n - 1; i++)
{
if(b[i])
{
if(b[i] > 0)
{
int s1 = calc(ls, i - 1, b[i] - 1); // ls 贡献到 g[i - 1][b[i] - 1];
if(s1) h[i][0] = b[i], s[i][0] = s1;
}
if(i >= n - b[i] + 1)
{
if(i - (n - b[i] + 1) < i)
{
int s2 = calc(ls, i - 1, i - (n - b[i] + 1));
if(h[i][0] != -1 && h[i][0] != i - (n - b[i] + 1) && s[i][0] != 0) h[i][1] = i - (n - b[i] + 1), s[i][1] = s2;
else h[i][0] = i - (n - b[i] + 1), s[i][0] += s2, s[i][0] %= mod;
}
}
ls = i;
}
}
int ans = 0;
for(int i = 0; i <= n - 1; i++) g[i] = calc(ls, n - 1, i);
if(a[n]) cout << 1ll * f[a[n] - 1] * g[a[n] - 1] % mod << '\n';
else
{
for(int i = 0; i <= n - 1; i++)
ans += 1ll * f[i] * g[i] % mod, ans %= mod;
cout << ans << '\n';
}
return;
}
int main()
{
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
freopen("npy.in", "r", stdin);
freopen("npy.out", "w", stdout);
fac[0] = 1;
for(int i = 1; i <= 1e6; i++)
fac[i] = 1ll * fac[i - 1] * i % mod;
inv[(int) 1e6] = ksm(fac[(int) 1e6], mod - 2);
for(int i = 1e6; i >= 1; i--)
inv[i - 1] = 1ll * inv[i] * i % mod;
int t; cin >> t;
while(t--) solve();
return 0;
}
验题人评价

D. 蟠桃(peach)
喜怒哀乐都是我的心情
每站有不同浪漫风景
该走了 我和她有个约定
Solution #1
\(n \le 300\) 的部分可以直接暴力枚举区间 \([l, r]\),然后直接按照题意判断。
可以获得 \(30\) 分。
Solution #2
记 \(L_i, R_i\) 表示 \(i\) 的所有出边与 \(i\) 自身中连到的下标的最小、最大值。
注意到若一个区间包含下标 \(i\),则它必然包含 \([L_i, R_i]\).
先枚举 \(L\),然后在枚举 \(R\) 的过程中动态维护 \(\min L_i\) 和 \(\max R_i\) 即可。
时间复杂度 \(O(n^2)\),可以获得 \(45\) 分。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5, M = 2e6 + 5;
int c, n, m, x, y, l[M], r[M];
long long ans;
signed main()
{
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
freopen("peach.in", "r", stdin);
freopen("peach.out", "w", stdout);
cin >> c >> n >> m;
for(int i = 1; i <= n; i++) l[i] = r[i] = i;
for(int i = 1; i <= m; i++)
{
cin >> x >> y;
l[x] = min(l[x], y);
r[x] = max(r[x], y);
}
for(int i = 1; i <= n; i++)
{
int ll = n, rr = 0;
for(int j = i; j <= n; j++)
{
ll = min(ll, l[j]);
rr = max(rr, r[j]);
if(ll < i) break;
if(rr > j) continue;
ans++;
}
}
cout << ans;
return 0;
}
Solution #3
考虑在固定 \(l\) 枚举 \(r\) 的过程中,\(\min L_i\) 和 \(\max R_i\)(即上面代码中的 ll, rr)变化的次数至多是 \(O(\min(n, m))\) 次,而发生变化的位置只可能是有连边的点。这些点将序列分成了 \(O(m)\) 段,每段中对答案产生贡献的都是一段后缀,可以直接算出来。
时间复杂度 \(O(nm)\),实现劣的话会变成 \(O(nm \log n)\),应该也能过。
可以通过 \(m=100\) 的部分分.
Solution #4
考虑统计不合法的段,然后使用广义 HXF 容斥(即整体 - 不合法)计算合法答案。
仍然考虑合法等价于 \(\min L_i \ge l\),且 \(\max R_i \le r\). 由于 \(|x-y|\le 20\),对每个 \(l\),可能存在 \(L_i < l\) 的位置一定在 \([l, l+20]\) 内。\(r\) 的情况同理。
先暴力计算区间长度 \(\le 20\) 的答案。
对于区间长度 \(>20\) 的,我们考虑用左端点匹配右端点。一个左端点 \(l'\) 和右端点 \(r'\) 能匹配,当且仅当 \([l', l'+20]\) 与 \([r'-20, r']\) 都是“安全”的。直接做就好了。
Solution #5
我们要再次把区间合法的条件拉出来:\(\displaystyle \min_{i=l}^r L_i \ge i\),且 \(\displaystyle \max_{i=l}^r R_i \le i\).
我们尝试把条件分开处理。
对一个固定的 \(r\),满足第二个条件的 \(l\) 显然是一段后缀,可以二分得到。现在我们只需要计算一个有多少 \(l \in [p, r]\) 满足 \(\displaystyle \min_{i=l}^r L_i \ge i\).
有一个小性质:合法区间一定满足 \(L_l = l\), \(R_r = r\),证明显然。
考虑什么情况下一个左端点会不合法。显然如果 \(l, r\) 之间有一个位置 \(q\) 使得 \(L_q < l\),它就不合法了。于是我们维护单调栈,每遍历到一个右端点 \(r\) 就弹出所有 \(> L_r\) 的栈顶。如果 \(L_r = l\),就将其插入。
每次查询就是询问单调栈内有多少个 \([p, r]\) 之间的元素,二分即可。
有人场上写了 4k 线段树优化建图后缩点跑 dag 上 DP,然后才发现 DP 后得到的东西和不 DP 前一样。我不说是谁。
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5, M = 2e6 + 5;
int c, n, m, x, y, l[M], r[M], lt[M], lg[N];
long long ans;
int head[M], tot, fl[N][21], fr[N][21];
bool inst[M], vis[M];
stack<int> stk;
struct edge
{
int to, nxt;
} e[M << 2];
void addedge(int u, int v)
{
e[++tot] = {v, head[u]};
head[u] = tot;
}
int calc_fl(int l, int r)
{
int x = lg[r - l + 1];
return min(fl[l][x], fl[r - (1 << x) + 1][x]);
}
int calc_fr(int l, int r)
{
int x = lg[r - l + 1];
return max(fr[l][x], fr[r - (1 << x) + 1][x]);
}
int stk2[M], top;
signed main()
{
ios :: sync_with_stdio(false);
cin.tie(0), cout.tie(0);
freopen("peach.in", "r", stdin);
freopen("peach.out", "w", stdout);
cin >> c >> n >> m;
for(int i = 1; i <= n; i++) l[i] = r[i] = i;
for(int i = 2; i <= n; i++) lg[i] = lg[i >> 1] + 1;
for(int i = 1; i <= m; i++)
{
cin >> x >> y;
l[x] = min(l[x], y);
r[x] = max(r[x], y);
}
for(int i = 1; i <= n; i++)
fl[i][0] = l[i], fr[i][0] = r[i];
for(int j = 1; j <= 20; j++)
for(int i = 1; i + (1 << j) - 1 <= n; i++)
{
fl[i][j] = min(fl[i][j - 1], fl[i + (1 << (j - 1))][j - 1]);
fr[i][j] = max(fr[i][j - 1], fr[i + (1 << (j - 1))][j - 1]);
}
for(int i = 1; i <= n; i++)
{
while(top && stk2[top] > l[i]) top--;
if(l[i] == i) stk2[++top] = l[i];
if(i != r[i]) continue;
int L = 1, R = i, p = i;
while(L <= R)
{
int mid = (L + R) >> 1;
if(calc_fr(mid, i) <= i) p = mid, R = mid - 1;
else L = mid + 1;
}
L = 1, R = top; int q = top + 1;
while(L <= R)
{
int mid = (L + R) >> 1;
if(stk2[mid] >= p) q = mid, R = mid - 1;
else L = mid + 1;
}
ans += top - q + 1;
}
cout << ans;
return 0;
}
验题人评价


浙公网安备 33010602011771号