- 模拟赛2
模拟赛2题解
- @ 2026-8-18 12:09:43
T1 矩阵交换
题意
有一个 的矩阵,元素为 。可以任意交换两行,判断能否使得每一列都单调不递减(形如 )。
思路
-
题目提到行排序后看每列内部的关系。因此自然的分析列内的性质。
-
如果则直接输出YES容易解决,考虑。
-
考虑以第列元素为关键字对行进行排序,形成了的序列。认为是三个区间,其中,$a[l_1\cdots r_1][1]=1,a[l_2\cdots r_2][1]=2,a[l_3\cdots r_3][1]=3$。此时考虑第列元素,$\max\{a[l_1\cdots r_1][2]\}\leq \min\{a[l_2\cdots r_2][2]\},\max\{a[l_2\cdots r_2][2]\}\leq \min\{a[l_2\cdots r_2][3]\}$,即第列形成的个区间在第列内要满足非递减关系。
-
总结下是:前一列形成了若干个区间在当前列满足非递减关系。
-
采用上述方法从第列推到第列,该问题解决。
T2 砖块摆放
题意
层塔,相邻两个砖块颜色相同则上方砖块同色,不同则上方为第三种颜色。给定最底层 个砖块颜色(A/B/C),求塔顶颜色。
思路
把三种颜色 A、B、C 分别映射为 (模 意义下)。
观察规律:若相邻两个砖块颜色为 ,则它们上方的砖块颜色为 (也就是 )。
这恰好是二项式展开(杨辉三角系数)的形式。自底向上第 层第 个砖块(从 开始)对顶部的贡献系数为组合数 ,并且每向上一层要乘一个 。
因此塔顶颜色为:
$$\mathrm{top} = \sum_{i=0}^{n-1} a_i \cdot C(n-1,i) \pmod 3$$若 为偶数,还需整体取负(因为从底层到顶层一共 层,每层乘 ,偶数次负数抵消的正负号要单独处理,具体见代码)。
由于模数为 (质数),组合数可以用 Lucas 定理计算( 很小,直接预处理的组合数在模 意义下只有 三种取值)。
复杂度
每组数据 (Lucas 定理)或直接预处理 组合数。
T3 学习 lis
10分:枚举然后check即可,check的过程稍微加速一下,一旦不满足就返回,否则会TLE。
20分:我们显然可以一边dfs一边check前面生成的序列是否合法,这样的数据就卡不掉了,复杂度接近答案的大小。
30分:其实我们并不关心是多少,只关心我们生成的序列,一共出现了多少种不同的数字,以及这些数字必须得是从开始连续的,比如,我们可以用来代表所有类似于类的序列,最后乘上组合数即可,所以在的过程中,记录一共用了多少种数字,以及哪些数字被用到了,稍微剪枝一下就可以了。
另外40分:考虑一个状压,我们每次填入相同数字到一个子集去,比如这个方案,我们可以先给位置填入,变成,然后再给第个位置填入数字变成,这个过程中,凡是填入的数字的LIS都是可以算出来的。
状态是:表示我考虑完了数字,填入了这个集合的方案数。
转移不表,复杂度,但是由于给定LIS数组的限制,严重卡不满。
#include <bits/stdc++.h>
#define ll long long
#define maxn 1000005
#define mod 998244353
using namespace std;
ll C[3005][3005];
void init()
{
C[0][0]=1;
for (int i=1;i<=3000;i++)
for (int j=0;j<=i;j++)
{
if (i==1 || j==0) C[i][j]=1;
else C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
}
}
int n,m;
int a[21];
ll dp[1<<20][21]; //填了i种数字,填了哪些位置的方案数
void add(ll &x,ll y) {x=(x+y)%mod; return;}
int can(int sta,int nxt)
{
int mx=0;
for (int i=0;i<n;i++)
{
if ((sta>>i)&1) mx=max(mx,a[i]);
if ((nxt>>i)&1) if (a[i]!=mx+1) return 0;
}
return 1;
}
int main()
{
init();
cin>>n>>m;
for (int i=0;i<n;i++) cin>>a[i];
dp[0][0]=1;
for (int sta=0;sta<(1<<n);sta++)
for (int i=0;i<n;i++)
if (dp[sta][i])
{
int bu=(1<<n)-1-sta;
for (int S=bu;S!=0;S=(S-1)&bu)
if (can(sta,S))
{
add(dp[sta|S][i+1],dp[sta][i]);
}
}
ll ans=0;
for (int i=1;i<=n;i++) add(ans,dp[(1<<n)-1][i]*C[m][i]);
cout<<ans<<endl;
}
100分:考虑把状压转移子集的过程拎出来:表示的是,我正在考虑数字的填写,当前在从后向前考虑第个位置填不填数字,当前状态是的方案数。
转移不表,有一个小细节是,我们并不知道有没有用到,这样你可以选择加一个状态来表示是否用过了,也可以选择就这么下来,这样你得到的答案表示的是,最多用了个不同数字,构造出的序列满足LIS数组的方案数。
容斥一下:即是恰好使用了个数字的方案数了。
时间复杂度,需要滚动一下数组。
#include <bits/stdc++.h>
#define ll long long
#define maxn 3000005
#define mod 998244353
using namespace std;
ll C[3005][3005];
void init()
{
C[0][0]=1;
for (int i=1;i<=3000;i++)
for (int j=0;j<=i;j++)
{
if (i==1 || j==0) C[i][j]=1;
else C[i][j]=(C[i-1][j]+C[i-1][j-1])%mod;
}
}
int n,m;
int a[21];
int premax[1<<20];
void init2()
{
for (int sta=0;sta<(1<<n);sta++)
{
for (int i=0;i<n;i++)
if ((sta>>i)&1)
premax[sta]=max(premax[sta],a[i]);
}
}
int dp[2][22][1<<20];
ll ans[25];
void add(int &x,int y) {x=(x+y)%mod; return;}
void DP()
{
int now=0,nxt=1;
dp[now][n-1][0]=1;
for (int turn=1;turn<=n;turn++)
{
for (int i=n-1;i>=0;i--)
for (int sta=0;sta<(1<<n);sta++)
if (dp[now][i][sta])
{
int tmp=dp[now][i][sta];
//case1:不选
if (i) add(dp[now][i-1][sta],tmp);
else add(dp[nxt][n-1][sta],tmp);
//case2:选
if (((sta>>i)&1)==0 && premax[sta&((1<<i)-1)]+1==a[i])
{
if (i) add(dp[now][i-1][sta|(1<<i)],tmp);
else add(dp[nxt][n-1][sta|(1<<i)],tmp);
}
dp[now][i][sta]=0;
}
//此时dp[nxt][n-1][sta]就是选了最多turn个数的时候的方案数
ans[turn]=dp[nxt][n-1][(1<<n)-1];
swap(now,nxt);
}
ll tot=0;
for (int i=1;i<=n;i++)
{
for (int j=i-1;j>=1;j--)
{
ans[i]=(ans[i]+(-1*C[i][j]*ans[j]%mod)+mod)%mod;
}
tot=(tot+ans[i]*C[m][i]%mod)%mod;
}
cout<<tot<<endl;
}
int main()
{
cin>>n>>m;
for (int i=0;i<n;i++) cin>>a[i];
init(); init2();
DP();
}
T4 战略轰炸
首先断环为链。
下文中 表示联通之后的军事基地的战斗力的最大值。
考虑给定 和 的轰炸机是否可以完美轰炸 次的本质是判断 $\sum\limits_{i=1}^n{\operatorname{min}(\left\lfloor\dfrac{a_i}{k}\right\rfloor,c)} \geq b \times c$。 原因是因为一共轰炸 次,一个军事基地最多被轰炸 次,而若 ,则代表每次都可以选择轰炸它。否则对剩余的 的军事基地轮流轰炸,即求和之后除以 下取整,最后加起来判断是否能否有大于等于 个军事基地被一同选中。
- 特殊性质
即每次询问一个 ,求 $\sum\limits_{i=1}^n{\operatorname{min}(\left\lfloor\dfrac{a_i}{v}\right\rfloor,c)}$。 考虑对 数组排序,设定一个阈值 ,当 时,预处理计算。当 时,枚举 用树状数组统计。再特判一下 的数的个数即可。取 ,则复杂度为 。
- 特殊性质
即给定一个长度为 的数组 , 次操作。
-
表示将 。
-
表示求 $\sum\limits_{i=1}^n{\operatorname{min}(\left\lfloor\dfrac{a_i}{v}\right\rfloor,c)}$。 。
考虑对于 的每个 建立一个树状数组直接维护,预处理复杂度 ,单次询问复杂度 ,单次修改复杂度 。
当 时与特殊性质 的方法一样做即可,预处理复杂度 ,单次询问复杂度 ,单次修改复杂度 。
取 ,则复杂度为 。
对于 ,暴力判断目前这个区间是否被其他区间覆盖。
对于 ,用特殊性质 的方法暴力做即可。
- 正解
重点是要维护军事基地间的连通性,然后并查集记录连通块大小即可用特殊性质 的方法直接做了。
考虑维护一个连通块中每个元素连接到下一个同连通块内的点。
设 为 号点上方的边数。设我们要合并 和 ()。
若 ,不妨设 ,则我们向右找到第一个位置 满足 ,因为 ,且此时 所在的连通块必定被 所在的连通块包含,故直接合并 与 。反之同理。
若 ,依旧找到 ,若 则如上操作,否则代表 与 之间没有其他连通块阻碍了,直接连接即可。
寻找 可以直接线段树上二分。
而合并连通块时分类讨论:
首先定义 表示 所在连通块最左边的位置, 同理。这个直接用并查集维护。
若 与 所在连通块无包含关系,则直接给区间 的 值加一。
否则需要断掉 这条边,加上 和 这两条边,同样线段树上区间加法维护。
这部分总复杂度 。
然后维护一下连通块的 之和直接继续做即可。
总复杂度 $O(n\log n\operatorname{\alpha}(n)+n\log n \sqrt{A}))$。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
namespace FastIO {
char buf[1 << 21], buf2[1 << 21], a[20], *p1 = buf, *p2 = buf, hh = ' ';
int p, p3 = -1;
void read() {}
void print() {}
inline int getc() {
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 21, stdin), p1 == p2) ? EOF : *p1++;
}
inline void flush() {
fwrite(buf2, 1, p3 + 1, stdout), p3 = -1;
}
template <typename T, typename... T2>
inline void read(T &x, T2 &...oth) {
int f = 0;
x = 0;
char ch = getc();
while (!isdigit(ch)) {
if (ch == '-')
f = 1;
ch = getc();
}
while (isdigit(ch)) {
x = x * 10 + ch - 48;
ch = getc();
}
x = f ? -x : x;
read(oth...);
}
template <typename T, typename... T2>
inline void print(T x, T2... oth) {
if (p3 > 1 << 20)
flush();
if (x < 0)
buf2[++p3] = 45, x = -x;
do {
a[++p] = x % 10 + 48;
} while (x /= 10);
do {
buf2[++p3] = a[p];
} while (--p);
buf2[++p3] = hh;
print(oth...);
}
} // namespace FastIO
#define read FastIO::read
#define print FastIO::print
#define flush FastIO::flush
const int B = 100,A = 100000;
int n,q,c,sid,timer = 0,cnt[100010],a[100010],s[100010];
int LSB(int i){
return i & (-i);
}
struct Bit{
int bit[100010];
void upd(int i,int k){
if(!i) return;
while(i <= A){
bit[i] += k;
i += LSB(i);
}
}
int psq(int i){
int s = 0;
while(i){
s += bit[i];
i -= LSB(i);
}
return s;
}
}bt[B + 5];
void modify(int pre,int v){
for(int i = 1; i <= B; i++){
if(!cnt[i]) continue;
bt[i].upd(pre,-pre / i);
bt[i].upd(v,v / i);
}
bt[B + 1].upd(pre,-1);
bt[B + 1].upd(v,1);
}
namespace DSU{
int fa[100010],l[100010],r[100010],sz[100010],val[100010];
void init(){
for(int i = 1; i <= n; i++) fa[i] = l[i] = r[i] = i,sz[i] = 1,val[i] = a[i];
}
int Find(int i){
return fa[i] == i ? i : fa[i] = Find(fa[i]);
}
void unite(int u,int v){
int fu = Find(u),fv = Find(v);
if(fu == fv) return;
if(sz[fu] > sz[fv]) swap(fu,fv);
fa[fu] = fv;
l[fv] = min(l[fu],l[fv]);
r[fv] = max(r[fu],r[fv]);
modify(val[fv],val[fu] + val[fv]);
modify(val[fu],0);
val[fv] += val[fu];
val[fu] = 0;
}
};
using DSU::init;
using DSU::Find;
using DSU::unite;
int mn[400010],tag[400010];
#define ls(i) (i << 1)
#define rs(i) (i << 1 | 1)
void pushup(int i){
mn[i] = min(mn[ls(i)],mn[rs(i)]);
}
void Tag(int i,int v){
mn[i] += v,tag[i] += v;
}
void pushdown(int i){
if(tag[i]){
Tag(ls(i),tag[i]);
Tag(rs(i),tag[i]);
tag[i] = 0;
}
}
void upd(int i,int l,int r,int ql,int qr,int v){
if(ql <= l && r <= qr){
Tag(i,v);
return;
}
pushdown(i);
int mid = (l + r) >> 1;
if(ql <= mid) upd(ls(i),l,mid,ql,qr,v);
if(qr > mid) upd(rs(i),mid + 1,r,ql,qr,v);
pushup(i);
}
int query(int i,int l,int r,int pos){
if(l == r) return mn[i];
pushdown(i);
int mid = (l + r) >> 1;
if(pos <= mid) return query(ls(i),l,mid,pos);
else return query(rs(i),mid + 1,r,pos);
}
int queryl(int i,int l,int r,int pos,int v){
if(r <= pos){
if(mn[i] >= v) return 0;
if(l == r) return l;
pushdown(i);
int mid = (l + r) >> 1;
if(mn[rs(i)] < v) return queryl(rs(i),mid + 1,r,pos,v);
return queryl(ls(i),l,mid,pos,v);
}
pushdown(i);
int mid = (l + r) >> 1,res = 0;
if(mid + 1 <= pos) res = queryl(rs(i),mid + 1,r,pos,v);
if(!res) res = queryl(ls(i),l,mid,pos,v);
return res;
}
int queryr(int i,int l,int r,int pos,int v){
if(pos <= l){
if(mn[i] >= v) return n + 1;
if(l == r) return l;
pushdown(i);
int mid = (l + r) >> 1;
if(mn[ls(i)] < v) return queryr(ls(i),l,mid,pos,v);
return queryr(rs(i),mid + 1,r,pos,v);
}
pushdown(i);
int mid = (l + r) >> 1,res = n + 1;
if(pos <= mid) res = queryr(ls(i),l,mid,pos,v);
if(res == n + 1) res = queryr(rs(i),mid + 1,r,pos,v);
return res;
}
void merge(int x,int y){
x = Find(x),y = Find(y);
if(DSU::r[x] < DSU::l[y]) upd(1,1,n,DSU::r[x] + 1,DSU::l[y] - 1,1);
else upd(1,1,n,max(DSU::l[x],DSU::l[y]),min(DSU::r[x],DSU::r[y]),-1);
unite(x,y);
}
int OP[100010],U[100010],V[100010],C[100010];
signed main(){
read(n),read(q),read(sid);
for(int i = 1; i <= n; i++) read(a[i]);
for(int i = 1; i <= B; i++){
for(int j = 1; j <= n; j++) bt[i].bit[a[j]] += (a[j] / i);
for(int j = 1; j <= A; j++) s[j] = s[j - 1] + bt[i].bit[j];
for(int j = 1; j <= A; j++) bt[i].bit[j] = s[j] - s[j - LSB(j)];
}
for(int i = 1; i <= n; i++) bt[B + 1].bit[a[i]]++;
for(int i = 1; i <= A; i++) s[i] = s[i - 1] + bt[B + 1].bit[i];
for(int i = 1; i <= A; i++) bt[B + 1].bit[i] = s[i] - s[i - LSB(i)];
init();
for(int i = 1; i <= q; i++){
read(OP[i]),read(U[i]),read(V[i]);
if(OP[i] == 2){
read(C[i]);
if(U[i] <= B) cnt[U[i]]++;
}
}
int op,u,v;
for(int t = 1; t <= q; t++){
op = OP[t],u = U[t],v = V[t];
if(op == 1){
if(u > v) swap(u,v);
int vu = query(1,1,n,u),vv = query(1,1,n,v);
while(Find(u) != Find(v)){
if(vu > vv) merge(u,queryr(1,1,n,u,vu)),vu--;
else if(vu < vv) merge(queryl(1,1,n,v,vv),v),vv--;
else{
int pos = queryr(1,1,n,u,vu);
if(pos < v) merge(u,pos),vu--;
else merge(u,v);
}
}
}
else{
c = C[t];
swap(c,v);
int sum = (bt[B + 1].psq(A) - bt[B + 1].psq(u * v - 1)) * v;
if(u <= B) sum += bt[u].psq(u * v - 1),cnt[u]--;
else{
for(int cc = u; cc <= min(A,u * v - 1); cc += u){ //cc ~ min(u * v - 1,cc + u - 1)
timer++;
sum += (bt[B + 1].psq(min(u * v - 1,cc + u - 1)) - bt[B + 1].psq(cc - 1)) * (cc / u);
}
}
if(sum >= c * v) printf("Yes\n");
else printf("No\n");
}
}
return 0;
}
1 条评论
-
eroengine MOD @ 2026-8-18 15:02:08已修改
1. 问题转化:从"圆上的弦"到"链上的区间"
军事基地等距离围成圆,编号 。一座桥梁连接 ,在圆上是一根弦。题面说"两个桥梁交叉则互相连通"。
我们把圆断环为链:展开成一条直线 (断掉 与 之间的边)。
于是每座桥 (设 )变成一个区间。两根弦 与 交叉,当且仅当它们"端点交错":
$$u_1 < u_2 < v_1 < v_2 \quad \text{或} \quad u_2 < u_1 < v_2 < v_1$$(注意:包含关系不算交叉。例如 包含 ,它们不交叉。)
因此整个问题变成:维护若干区间,相交(端点交错)的区间合并成一个连通块,连通块的战斗力是块内所有基地战斗力之和。
2. 核心数据结构:
p数组(覆盖层数)给每个连通块一个区间 ( 是该块最左、最右的基地编号)。
定义数组 :覆盖点 的"拱"的数量。
想象每个连通块 是链上方的一道"拱",从 跨越到 。点 位于几个拱下方, 就是几。
初始: 个单点连通块 ,没有任何跨越的拱,所以 。
例子
桥 [1,3]:拱从 1 跨越到 3 位置: 1 2 3 4 5 拱: ┌───────┐ p 值: 0 1 0 0 0 ↑ ↑ ↑ 1被拱 2被拱 3不在拱内点 2 被拱 覆盖,所以 ;点 1 是拱的左端点、点 3 是右端点(端点本身 p 记为 0 或 1 均可,关键是中间跨越的部分 p 会 +1)。
3.
merge(x, y):合并两个连通块时如何更新p合并连通块 (区间 )与 (区间 ),分两种情况:
情况一: 完全在 左边()
x: [l_x ─── r_x] y: [l_y ─── r_y] ↑ 空隙 ↑合并后形成一个更大的拱 ,它会跨越中间的空隙 。所以对这段空隙的每个点
p+1:if (r[x] < l[y]) upd(1, 1, n, r[x]+1, l[y]-1, 1);情况二: 与 有重叠()
x: [l_x ───── r_x] y: [l_y ───── r_y] └─重叠─┘重叠部分 原本同时被 和 两个拱覆盖( 至少为 2),合并后只剩一个大拱覆盖,所以重叠部分的每个点
p-1:else upd(1, 1, n, max(l[x],l[y]), min(r[x],r[y]), -1);最后无论如何都要把两个块合并:
unite(x, y); // 并查集合并,更新 l、r、战斗力
4. 线段树维护
p,支持"二分找边界"p数组需要支持两类操作:- 区间加(
upd,合并时 p 值 +1/-1); - 二分查找:找到某个位置左/右第一个
p < v的位置(queryl/queryr)。
线段树的每个节点维护区间最小值
mn(外加懒标记tag)。为什么用"最小值"?因为要找"第一个 的位置",如果某区间的最小值都 ,说明该区间内不存在 的点,可以直接跳过整段;否则递归往下找。
queryr(pos, v):从pos向右找第一个p < v的位置queryr(区间 [l,r], 目标位置 pos, 阈值 v): 如果 r <= pos(整个区间都在 pos 右边): 如果 mn >= v: 返回 n+1 # 区间内没有 p<v 如果 l == r: 返回 l # 找到叶子 优先去左子树(更靠左) 否则(区间跨过 pos): 先查包含 pos 的那一侧,再查另一侧queryl对称(从 pos 向左找第一个p<v)。为什么这个二分能给出"拱的边界"?
假设点 的 (被 道拱覆盖)。这 道拱从内到外的右端点依次为 。
从 向右看, 值的变化是:
位置: u ───── r_1 ───── r_2 ───── r_k ───── p 值: k k-1 k-2 0所以:
- 第一个
p < k的位置 = (最内层拱的右端点); - 第一个
p < 1的位置 = (最外层拱的右端点)。
同理向左可以找到最内层拱的左端点。线段树二分就是 地找到这些边界。
5. 连接操作
op=1的完整流程给定连接 (设 ),目标是让它们最终在同一个连通块,同时正确处理中间所有相交的连通块。
if (u > v) swap(u, v); int vu = query(u); // p[u] int vv = query(v); // p[v] while (Find(u) != Find(v)) { if (vu > vv) { // u 比 v "更深"(被更多拱覆盖) merge(u, queryr(u, vu)); // 合并 u 与它最内层拱的右端点 vu--; } else if (vu < vv) { // v 比 u 更深 merge(queryl(v, vv), v); // 合并 v 与它最内层拱的左端点 vv--; } else { // 一样深 int pos = queryr(u, vu); // u 最外层拱的右端点 if (pos < v) { merge(u, pos); vu--; } else merge(u, v); // 中间没有阻碍了,直接连 } }直觉解释
p[u]、p[v]反映 被多少道拱"压住"。被压得更深的那个点,其"最内层拱"需要先跟相邻块合并,逐步"解套"。- 每
merge一次,对应点所在的最内层拱被合并掉,vu/vv减一(覆盖层数变浅一层)。 - 当
vu == vv且中间没有其他拱阻碍时(queryr返回的位置 ≥ v),说明 之间可以直接相连,merge(u,v)完成。 - 循环直到
Find(u) == Find(v)。
整个过程每个连通块最多被合并一次,线段树二分与区间加都是 ,并查集接近 ,所以总复杂度 。
完整例子
,依次连接 、。
初始:全单点,。
连接 1-3:
merge(1,3):,给 的 p +1。- ,连通块 ()。
连接 2-5(桥 与 交叉,因为 ):
- ,。
queryr(2, 1):从 2 向右第一个p<1是位置 3()。merge(2, 3)。- (有重叠),给 的 p -1。
- ,合并 与 → 。
- 变为 0,此时 。
queryr(2, 0)返回 7(无p<0)≥ 5,merge(2,5)。- ,给 的 p +1,合并 → 。
- 结束,。
结果连通块 (对应交叉的两座桥),正确。
6. 询问
op=2(略)连通性由并查集维护好后,每个连通块的战斗力是块内 之和。询问"能否完美轰炸 次"等价于判断
$$\sum \min\left(\left\lfloor\frac{a_i}{k}\right\rfloor, c\right) \ge b\cdot c$$这部分用根号分治 + 树状数组(题解已述),与线段树无关,不再展开。
- 区间加(
- 1