2022.7.22 模拟赛
前言
第一次和学长们模拟赛切掉 T1,真的不容易啊。。。。。
T1 数正方体
题目描述
众所周知,正方形有 \(4\) 个点,\(4\) 条边;正方体有 \(4\) 个点,\(12\) 条边,\(6\) 个面,定义点为零维基础图形,线段为一维基础图形,正方形为二维基础图形,正方体为三维基础图形...,那么请问,一个 \(n\) 维基础图形包含多少个 \(m\) 维基础图形呢 \((m≤n)\)
多次询问,输出对 \(998244353\) 取模。
第一行输入一个正整数 \(T\) 表示数据组数。
下接 \(T\) 行,每行两个自然数 \(n,m\),描述一组数据。
输出 \(T\) 行,每行一个数字表示答案。
对于全部数据,\(T≤10^5,0≤m≤n≤10^5\)
样例:
输入
7
3 0
3 1
3 2
3 3
48545 1
77625 77624
93574 83513
输出
8
12
6
1
223544257
155250
424453971
解析
赛时手搓了一个及其神奇的结论:
然后过了,当时也是没有严谨的证明,由于自己的数学基础太垃圾,根本不知道什么是四维图形,就干脆,拿一二三维乱搞。
定义 \(n\) 维基础图形的顶点为,\(n\) 维向量中每一维是 \(0/1\) 的向量集合(比如说正方体上的顶点就是 \(0/1,0/1,0/1\) )
那么把点再往多扩展一下,不难发现,\(m\) 维基础图形在 \(n\) 维基础图形上的表现为,某些维取值相同,剩下维取值唯一(是个 \(m\) 维基础图形)
答案就是 \(\binom{n}{n-m}2^{n-m}\)
代码:
#include<bits/stdc++.h>
#define rint register int
#define endl '\n'
using namespace std;
const int N = 1e5 + 5;
const int M = 1e5 + 2;
const int p = 998244353;
int inv[N],fac[N],power[N];
namespace Fast{
inline int read(){
bool flag=false;int x=0;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')flag=true;ch=getchar();}
while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+ch-'0';ch=getchar();}
return flag?-x:x;
}
template<typename T> inline void write(T X){
if(X<0){putchar('-');X=~(X-1);}
int s[200],top=0;
while (X){s[++top]=X%10;X/=10;}
if(!top) s[++top]=0;
while(top) putchar(s[top--]+'0');
return;
}
}
using namespace Fast;
int calc(int m,int n){
if(m==0){
return power[n];
}
if(n==m){
return 1;
}
else{
return (long long)fac[n]*inv[n-m]%p*power[n-m]%p*inv[m]%p;
}
}
int main(){
int T=read();
inv[0]=inv[1]=1;
fac[1]=1;
power[0]=1;
power[1]=2;
for(rint i=2;i<=M;i++){
fac[i]=(long long)fac[i-1]*i%p;
inv[i]=(long long)(p-(p/i))*inv[p%i]%p;
power[i]=(long long)(power[i-1]<<1)%p;
}
for(rint i=2;i<=M;i++) inv[i]=(long long)inv[i]*inv[i-1]%p;
while(T--){
int n=read(),m=read();
write(calc(m,n));
puts("");
}
return 0;
}
T2 数连通块
题目描述
给定一个森林,每个点都有一定概率会消失,一条 \((u,v)∈E\) 的边存在的条件是,\(u\) 存在且 \(v\) 存在
有若干次询问,每次给定 \([l,r]\) ,然后把下标不在 \([l,r]\) 的点都删掉后,问剩余点和所有边构成的图的联通块个数的期望
第一行输入三个整数 \(n,m,q\),分别表示点的个数和边的个数和询问次数
之后 \(n\) 行,第 \(i\) 行有两个整数 \(a_i,b_i\),表示第 \(i\) 个点存在的概率是 \(\frac{a_i}{b_i}\) 之后 \(m\) 行,每行有两个整数 \(u,v\) ,表示存在一条连接 \(u\) 和 \(v\) 的边,保证无重边无自环之后 \(q\) 行,每行两个整数 \([l,r]\),表示一次询问
对于每一次询问,输出一行一个整数表示答案,输出对 \(1e9+7\) 取模
对于所有数据,保证:\(1≤n≤10^5,0≤m<n,1≤q≤10^5,1≤a_i≤b_i≤10^5\)
样例
输入
2 1 1
1 1
1 1
1 2
1 2
输出
1
解析
T2 挂的非常惨,一分没有,后来才发现 \(mod\) 取 \(1e9+7\)。(要不然有部分分呢)
考虑到森林中连通块个数的 \(T\) 满足:
证明:每添加一条边,就会合并两个连通块,相应的,连通块的个数就会少一个
根据期望的线性性,可得:\(E(T)=E(n)-E(m)\)
于是问题就转化为了计算 \(E(n)\) 和 \(E(m)\)
为了方便起见,设 \(p_u\) 表示 \(u\) 的存在的概率
于是对 \(p_u\) 做前缀和数组 \(S_p\) 后,可得
之后只需要考虑 \(E(m)\) 如何计算,显然有:
于是可以把每一条 \((u,v)∈E\) 的边看成一个点 \((u,v)\),它的权值是 \(p_u·p_v\)
之后相当于询问一个矩形内的点的权值和,这显然就是一个二维数点问题,离线后树状数组统计即可。
代码:
#include<bits/stdc++.h>
#define rint register int
#define endl '\n'
using namespace std;
typedef long long ll;
const int N = 1e5 + 5;
const int mod = 1e9 + 7;
int n,m,q,p[N];
ll pw(ll a,ll b){ll r=1;for(;b;b>>=1,a=a*a%mod)if(b&1)r=r*a%mod;return r;}
ll Sp[N],a[N],ans[N];
vector<int> g[N];
vector<pair<int,int>> que[N];
void add(int x,ll y){for(;x;x-=x&-x)a[x]=(a[x]+y)%mod;}
ll ask(int x){ll r=0;for(;x<=n;x+=x&-x)r=(r+a[x])%mod;return r;}
signed main(){
cin>>n>>m>>q;
for(rint i=1,a,b;i<=n;i++){
cin>>a>>b;
p[i]=a*pw(b,mod-2)%mod;
Sp[i]=(Sp[i-1]+p[i])%mod;
}
for(rint i=1,u,v;i<=m;i++){
cin>>u>>v;
if(u>v) swap(u,v);
g[v].push_back(u);
}
for(rint i=1,l,r;i<=q;i++){
cin>>l>>r;
ans[i]=(Sp[r]-Sp[l-1])%mod;
que[r].push_back(make_pair(l,i));
}
for(rint i=1;i<=n;i++){
for(rint j=0;j<g[i].size();j++){
int v=g[i][j];
add(v,(ll)p[v]*p[i]%mod);
}
for(rint j=0;j<que[i].size();j++){
pair<int,int>t=que[i][j];
(ans[t.second]-=ask(t.first))%=mod;
}
}
for(rint i=1;i<=q;i++){
cout<<(ans[i]%mod+mod)%mod<<endl;
}
return 0;
}
T3 数大模拟
题目描述
有 \(n\) 个连续的格子,一开始某些格子上有棋子,假设格子从 \(1\) 到 \(n\) 进行编号。
你需要进行 \(k\) 轮操作,每一轮操作如下:
•选中所有满足“其左侧相邻的是空格子”的棋子,
•将这些棋子向左移动一格。
比如,对序列 \(1001101\) 进行一轮操作后,会变为 \(1010110\)(其中 \(1\) 表示这个格子上有一个棋子,\(0\) 表示这是一个空格子)。
请输出操作 \(k\) 轮后,每个格子上是否有士兵(即输出一个长度为 \(n\) 的序列,表示方式与上文相同)。
注:如果有两个相邻格子上都有棋子,那么靠右的棋子不会在这一轮操作中移动。
输入第一行两个整数 \(n,k\),分别表示格子总数与操作轮数。
第二行 \(n\)个整数 \(a_i\)
输出 \(n\) 个整数 \(b_i\),其中 \(b_i\) 表示操作 \(k\) 轮后,从左往右第 \(i\) 个格子是否有棋子
样例
输入
6 2
0 1 1 0 1 1
输出
1 1 0 1 1 0
对于 \(30\%\) 的数据,满足 \(n,k≤1000\)
对于额外 \(20\%\) 的数据,满足 \(n≤1000\)
对于 \(100\%\) 的数据,满足 \(1≤n,k≤10^6\)
解析
赛时没有拿到 \(100pts\) ,因为第一题浪费了太多了时间,第二题也写挂掉了,这道题就想了个 \(50pts\) 的做法。
三十分就是直接暴力就行了,重点在于另外二十分
由于此时 \(k\) 必然大于 \(1000\),(不大于的话那不还是暴力
所以 \(k>n\) ,那么操作完之后 \(1\) 必然在序列前面,这个时候就可以直接把 \(1\) 都输出出来,再把剩下的 \(0\) 输出。
50pts
代码
#include<bits/stdc++.h>
#define rint register int
#define endl '\n'
using namespace std;
const int N = 1e6 + 5;
bool b[N];
int a[N];
int n,k;
int main(){
cin>>n>>k;
if(k>1000){
long long cnt1=0;
long long cnt2=0;
for(rint i=1;i<=n;i++){
cin>>a[i];
if(a[i]==1){
cnt1++;
}
if(a[i]==0){
cnt2++;
}
}
for(rint i=1;i<=cnt1;i++){
cout<<1<<" ";
}
for(rint i=1;i<=cnt2;i++){
cout<<0<<" ";
}
return 0;
}//20pts
for(rint i=1;i<=n;i++){
cin>>a[i];
}
while(k--){
for(rint i=2;i<=n;i++){
if(a[i-1]==0&&a[i]==1){
b[i]=1;
}
}
for(rint i=2;i<=n;i++){
if(b[i]==1){
a[i-1]=1;
a[i]=0;
b[i]=0;
}
}
}//30pts
for(rint i=1;i<=n;i++){
cout<<a[i]<<" ";
}
return 0;
}
再来说说正解。
考虑先把已经归位的 \(1\) 删除,也就是把序列一开始的前缀 \(1\) 都删掉
那么对于每一个棋子,都有一个目标距离,即最终序列的位置
显然这个最早的归位时间是最后一个棋子的归位时间
对于一个棋子,它前面每有一个棋子,则会对它产生晚归位 \(1\) 单位时间的影响
反之,它前面每有一个空格,则会对它产生早归位 \(1\) 单位时间的影响
于是就有一个求得最晚归位时间的代码了,当然也同时可以得知每个棋子的归位时间
int getlen(){
int res=0;
for(rint i=1;i<=n;i++){
if(mp[i]==0){
int tag=0,tot=0;
for(rint j=i;j<=n;j++){
if(mp[j]==1){
tot++;
res=max(res,(j-i+1)-tot+tag);
}
if(mp[j]==0){--tag;}
else{++tag;}
if(tag<0){
tag=0;
}
}
break;
}
}
return res;
}
那么可以发现一个性质:一个棋子的晚归位时间是 \(O(n)\) 级别的
回到这个题,利用之前的打表程序再发掘一下性质
不妨让每个棋子互不相同,也就是标上标号:
0 1 2 0 3 4 (初始局面)
1 0 2 3 0 4 (第一轮)
1 2 0 3 4 0 (第二轮)
1 2 3 0 4 0 (第三轮)
1 2 3 4 0 0 (第四轮)
去掉零
1 2 3 4 (初始局面)
1 2 3 4 (第一轮)
1 2 3 4 (第二轮)
1 2 3 4 (第三轮)
1 2 3 4 (第四轮)
然有一个规律,对于数字 \(i\) 的最后位置的这一竖条,考虑转移到 \(j=I+1\) ,那么就相当于先从 \((1,j)\) 往左下角一直走,直到撞上 \(i\) ,然后把 \(i\) 剩余的一段往下平移一格,然后往右平移一格
显然这个是对的,因为最终的路线一定是先撞上上一个棋子,然后和它错开一个时间格后如此操作
那么也就可以解释一开始的打表的规律了:空格会代替你撞上一次,非空格就会让你撞上一次
虽然说是要平移,但考虑每次只平移一格,而且总的轮数不会超过 \(O(n)\) ,所以可以用一个线段树来维
护这个东西,同时用一个 \(offset\) 维护一下当前区间,于是只需要支持区间赋值上一个等差数列,区间
加一,单点查询
对于查询第一次撞到哪,既可以在线段树上维护最大值,也可以二分后在线段树上跑(虽然是 \(O(log^2\) \(n)\) 的,但也能通过)
附上算法:
1. 找到 1 <= j <= len,满足 i - j + 1 == a[offset + 1 + j] + 1
2. 把 a[offset + 1] ~ a[offset + j] 按照 i, i - 1, i - 2, ... 的值填充
3. 把 a[offset + j + 1] ~ a[offset + len] 都 +1
4. ans[a[offset + k + 1]] = 1
实际上是模拟双端队列,手写一个 \(deque\) 就行了,时间复杂度 \(O(n)\)
100pts
代码
#include<bits/stdc++.h>
#define rint register int
#define endl '\n'
#define int long long
using namespace std;
typedef long long ll;
const int N = 3e6 + 5;
int n,k,mp[N],len,ans[N],offset,lim;
int getlen(){
int res=0;
for(rint i=1;i<=n;i++){
if(mp[i]==0){
int tag=0,tot=0;
for(rint j=i;j<=n;j++){
if(mp[j]==1){
tot++;
res=max(res,(j-i+1)-tot+tag);
}
if(mp[j]==0){--tag;}
else{++tag;}
if(tag<0){
tag=0;
}
}
break;
}
}
return res;
}
namespace SEGTREE {
const int T=N*10;
int nek[T],d[T];
int val[T],tag[T];
void init() {
memset(nek,-1,sizeof nek);
memset(tag,-1,sizeof tag);
}
#define lc (id<<1)
#define rc (id<<1|1)
void inline push(int id,int l,int r){
if(nek[id]!=-1){
int mid=(l+r)>>1;
val[id]=nek[id];
nek[lc]=nek[id],d[lc]=d[id];
nek[rc]=(mid-l+1)*d[id]+nek[id],d[rc]=d[id];
tag[lc]=tag[rc]=-1;
nek[id]=-1,d[id]=0;
}
if(tag[id]!=-1){
val[id]+=tag[id];
if(tag[lc]==-1) tag[lc]=tag[id];
else tag[lc]+=tag[id];
if(tag[rc]==-1) tag[rc]=tag[id];
else tag[rc]+=tag[id];
tag[id]=-1;
}
}
int inline ask(int id,int l,int r,int pos){
int mid=(l+r)>>1;
push(id,l,r);
if(l==r){return val[id];}
else if(pos<=mid){return ask(lc,l,mid,pos);}
else{return ask(rc,mid+1,r,pos);}
}
int inline getj(int val){
int l=1,r=len,res=1;
while(l<=r){
int mid=(l+r)>>1;
int tmp=ask(1,1,lim,offset+1+mid);
if(val-mid==tmp){res=mid;r=mid-1;}
else if(val-mid>tmp){l=mid+1;}
else{r=mid-1;}
}
return res;
}
void inline set_d(int id,int l,int r,int ql,int qr,int shou_xiang,int gong_cha){
int mid=(l+r)>>1;
push(id,l,r);
if(ql<=l&&r<=qr){
if(nek[id]==-1)
nek[id]=shou_xiang,d[id]=gong_cha;
else
nek[id]+=shou_xiang,d[id]+=gong_cha;
tag[id]=-1;
}
else if(qr<=mid){
set_d(lc,l,mid,ql,qr,shou_xiang,gong_cha);
}
else if(ql>=mid+1){
set_d(rc,mid+1,r,ql,qr,shou_xiang,gong_cha);
}
else{
set_d(lc,l,mid,ql,mid,shou_xiang,gong_cha);
int new_shou_xiang=shou_xiang+(mid-ql+1)*gong_cha;
set_d(rc,mid+1,r,mid+1,qr,new_shou_xiang,gong_cha);
}
}
void inline plus_1(int id,int l,int r,int ql,int qr){
int mid=(l+r)>>1;
push(id,l,r);
if(ql<=l&&r<=qr){
if(tag[id]==-1)
tag[id]=1;
else ++tag[id];
}
else if(qr<=mid){
plus_1(lc,l,mid,ql,qr);
}
else if(ql>=mid+1){
plus_1(rc,mid+1,r,ql,qr);
}
else{
plus_1(lc,l,mid,ql,mid);
plus_1(rc,mid+1,r,mid+1,qr);
}
}
}
ll a[N];
int inline getj(int i){return SEGTREE::getj(i);}
void inline set_d(int l,int r,int i,int gc){SEGTREE::set_d(1,1,lim,l,r,i,gc);}
void inline plus_1(int l,int r){SEGTREE::plus_1(1,1,lim,l,r);}
int inline get_val(int pos){return SEGTREE::ask(1,1,lim,pos);}
signed main(){
SEGTREE::init();
cin>>n>>k;
for(rint i=1;i<=n;i++){
cin>>mp[i];
}
len=getlen();
k=min(k,len);
len++;
offset=len+10;
lim=2*(len+20);
for(rint i=1;i<=n;i++){
if(mp[i]==0){
continue;
}
int t=i;
offset--;
// 1. 找到 1 <= j <= len,满足 i - j + 1 == a[offset + 1 + j] + 1
// 2. 把 a[offset + 1] ~ a[offset + j] 按照 i, i - 1, i - 2, ... 的值填充
// 3. 把 a[offset + j + 1] ~ a[offset + len] 都 +1
// 4. ans[a[offset + k + 1]] = 1
int j=getj(i);
set_d(offset+1,offset+j,i,-1);
if(offset+j+1<=offset+len){plus_1(offset+j+1,offset+len);}
ans[get_val(offset+k+1)]=1;
}
SEGTREE::init();
for(rint i=1;i<=n;i++){
putchar(ans[i]+'0');
putchar(' ');
}
}
T4 数字符串
题目描述
有一个长度为 \(n\) 的字符串 \(S\),对于每一个前缀 \(S[1,i]\),求有多少个 \(x\) 满足:
1.\(1≤x≤i\)
2.\(S[1,x]=S[i−x+1,i]\)
3.\(x≥i-x+1\)
4.\(x−(i−x+1)+1≡0(modk)\)
输出 \(\prod_{i=1}^{n}(F_i+1) mod 998244353\)
其中 \(Fi\) 表示前缀 \(S[1,i]\) 中满足条件的 \(x\) 的个数
第一行输入一个由小写字母构成的字符串。
第二行一个整数 \(k\),表示题目描述中的常数 \(k\);
输出一行一个整数表示答案。
样例
输入
abababac
2
输出
24
满足 \(1≤k≤|S|≤10^6\),并且 \(S\) 仅由小写字母组成。
分析
考虑求完 next
数组后的大暴力:
for(rint i=1;i<=n;i++){
if(i%k==0) ++cnt[i];
for(rint j=nxt[i];j&&j>=i-j+1;j=nxt[j]){
if((2*j-i)%k==0){
++cnt[i];
}
}
}
由于数据范围很小,直接建完 next
树后链上差分即可
代码
#include<bits/stdc++.h>
#define rint register int
#define endl '\n'
#define int long long
using namespace std;
const int mod=998244353;
const int N = 2e6 + 9;
int n,k,z[N],tag[N];
char s[N];
inline void MOD(int &x){x=x>=mod?x-mod:x;}
inline int power(int x,int y){
int ans=1;
while(y){
if(y&1)ans=ans*x%mod;
x=x*x%mod;
y>>=1;
}
return ans;
}
signed main(){
scanf("%s",s+1);
n=strlen(s+1);
scanf("%d",&k);
int l=0;
for(rint i=2;i<=n;++i){
if(l+z[l]>i)z[i]=min(z[i-l+1],l+z[l]-i);
while(i+z[i]<=n&&s[z[i]+1]==s[i+z[i]])z[i]++;
if(i+z[i]>l+z[l])l=i;
}
z[1]=n;
for(rint i=0;i<n;++i){
int len=z[i+1];
if(len>=i+k){
len-=i;
len=(len/k)*k;
tag[2*i+k]++;
if(2*i+len+k<=n)tag[2*i+len+k]--;
}
}
int Ans=1,sum=0;
for(rint i=1;i<=n;i++){
if(i+k<=n)tag[i+k]+=tag[i];
Ans=Ans*(tag[i]+1)%mod;
sum+=tag[i];
}
for(rint i=0;i<=n;i++)
z[i]=tag[i]=0;
cout<<Ans;
return 0;
}