2026/09/21 模拟赛总结
NOIP 模拟赛第九场
预估分数:\(100+[20,?]+[40,?]+50\),实际分数 \(70+90+50+50\),排名 \(rk6/17\)。
问题在于这四道题目不是青紫紫紫吗,凭啥分数都这么高啊
由于这几题都是原题,就不帖代码了,可以在我账号上看。
T1
原题:P5041
经典题,可惜我写挂了。在长度为奇数的时候,千万要注意循环要多枚举一个,否则就会有神秘错误。
首先考虑能不能形成一个回文串。充要条件就是出现次数为奇数的字母个数 \(\le 1\)。
然后我们考虑如何做。不难发现直接贪心即可。在第 \(i\) 次操作中,将位置第 \(i\) 个的字母与后面相同的字母配对,这个字母显然是未配对的最后一个。我们可以将其移位,但是这可能会改变其它字母的位置,所以用树状数组实现区间查询,单点修改即可。
另外可能会有一个字母没有和它匹配的字母,所以我们需要将其放在中间最后处理,实际上到最后它也自动归位了。
for(int i = 1;i <= n;i ++)
ve[s[i]].push_back(i);
for(int i = 65;i <= 122;i ++)
{
if(!ve[i].size())continue ;
if((int)ve[i].size() % 2)cnt ++;
for(int j = 0;j <= ((int)ve[i].size() - 1) / 2;j ++)a[++m] = {ve[i][j],ve[i][(int)ve[i].size() - j - 1]};
}
if(cnt > 1)
{
cout << -1;
return 0;
}
sort(a + 1,a + m + 1,cmp);
for(int i = 1;i <= m;i ++)
{
int sa = a[i].l;
a[i].l += query(a[i].l),a[i].r += query(a[i].r);
ans += a[i].l - i;
add(1,1),add(sa,-1);
}
cout << ans;
T2
原题:P7669
雷霆题目。
首先大家肯定已经忍不住自己的控制欲打出了"随机找一条最短路,然后直接在新图上跑最短路"的代码实现,但是注意到这样做随便被出题人卡,于是你又按边的顺序反着建图再跑一遍,赛时也是通过了所有样例,puck 数据还是太水了。注意到这种诡异做法的下场就是分数波动特别大。还好没开子任务,不然我可能就只剩 \(20\) 分了。
来讲一下正解。分为三步。
-
第一步:我们求出 \(S\) 和 \(T\) 对所有题的单源最短路。
-
第二步:找出可能位于最短路上的边。具体地,若 \(dis_{S,u_i} + w_i + dis_{v_i,T} = dis_{S,T}\),那么显然存在一种最短路方案经过该边。
-
第三步:枚举一个点 \(x\),计算 \(dis_{U,x}\),那么我们还要找到一个点 \(y\),满足 \(x\) 可以只经过最短路边到达点 \(y\),那么只要算 \(\min(dis_{U,x} + dis_{y,V})\) 即可。当然因为 \(U\) 和 \(V\) 是对称的,所以实际上答案还要再对 \(\min(dis_{U,y} + dis_{x,V})\) 取 \(\min\)。
那么我们怎么知道 \(x\) 和 \(y\) 在同一条最短路上呢?注意到最短路上的边不可能出现环,所以我们直接建反边,用拓扑序 dp 来判断每个点所能到的点的最小值是什么(注意到维护 \(y\) 具体位置是难维护的,因此统一改成维护最小值)。然后再根据上面那个公式就做完了。
T3
这题好像没找到原题。
题目大意:
有一个长度为 \(n\) 的序列 \(a\),支持两种操作:求包含 \(x\) 的最大子段和(操作 1),或者全局加 \(x\)(操作 2)。\(1 \le n,m \le 2 \times 10^5,1 \le x,a_i \le 10^7\)
思路分析:
暴力显然是简单的,考虑优化。
我们设一次第一次操作前,\(2\) 操作已经累积加的数为 \(S\);
注意到我们想要求 \(s_r - s_{l - 1} + S \times (r - l + 1)\) 的最大值,其中 \(s_i\) 为 \(a_i\) 的前缀和。于是式子可以变成 \(s_r + S \times r - (s_{l - 1} + S \times (l - 1))\),注意到前后两个式子类似,并且只有 \(S\) 会变动,因此直接将 \(S\) 看成 \(x\),\(i\) 看成 \(k\),\(s_i\) 看成 \(b\),于是就变成了若干个一次函数求最大值最小值问题,李超线段树解决。
复杂度 \(\mathcal{O}(m \log n)\)。
T4
此题有个非常简单的 \(\mathcal{O}(n^2)\) 做法,我们记 \(dp_{i,j}\) 表示考虑到左部前 \(i\) 个点和右部前 \(j\) 个点匹配的最大匹配个数,那么有转移:
\(\begin{cases}\max(dp_{i-1,j},dp_{i-1,j-1}+1),\;\;\;\;\;j \in [L,R]\\ \max(dp_{i-1,j},dp_{i,j-1}),\;\;\;\;\;\;\;\;\;\;\;\;\;\;\text{otherwise.}\end{cases}\)
使用滚动数组后就会变成:
当 \(j \in [L,R]\) 时,有 \(dp_{j} = \max(dp_{j},dp_{j-1}+1)\),否则对 \(dp_{j - 1}\) 取 \(\max\)。
我们考虑优化。
首先观察得到一个性质:\(0 \le dp_i - dp_{i - 1} \le 1\)。
然后就可以发现 \(\max\) 中的 \(dp_{j-1}+1\) 一定是大于 \(dp_j\) 的,所以可以直接转移成 \(dp_{j - 1} + 1\)。
所以我们把 \(dp_i\) 看成一个长度为 \(n\) 的序列,并用平衡树维护。
对于一个整数 \(i\),我们把它分成 \([1,L - 2],[L - 1,R - 1],[R,R],[R + 1,n]\) 四个段:
-
第一段:由于完全没有转移,故保持不变。
-
第二段:因为会被它们的后一个给转移,所以它们会变成新区间的 \([L,R]\),并且进行区间加一操作。
-
第三段:由于该位置已经被 \(R - 1\) 平替了,所以新序列已经没有该位置了,所以直接删去。
-
第四段:该段没有参与直接转移,但是由于后面会有个取 \(\max\) 操作,所以可能它的前面一部分是小于 \([L,R]\) 中的一些数的。因此我们直接二分出一个数 \(k\),满足 \(dp_k < dp_R\) 且 \(dp_{k + 1} \ge dp_R\),然后将区间 \([R + 1,k]\) 进行区间加一操作。
还有一个小问题:原来的 \(dp_{L - 1}\) 去哪了?因为它不能参加转移,所以它只能保持不变。
因此,在一次转移之后,序列会变为:\([1,L - 2],[L - 1],[L - 1,R - 1](+1),[R + 1,k](+1)\)\(,[k+1,n]\)。其中 \((+1)\) 表示进行了区间加一操作。
然后这种序列分裂合并的操作可以用平衡树维护,区间加一打懒标记即可,复杂度优化至了 \(\mathcal{O}(n \log n)\)。

浙公网安备 33010602011771号