[省选前集训2021] 模拟赛1
矩阵(matrix)
题目描述
给定矩阵 \(A,B\),求有多少个 \(0/1\) 矩阵 \(C\) 满足下列条件:
答案对 \(998244353\) 取模。
\(n\leq 200\)
解法
暴力高斯消元,\(n^2\) 个未知数,算自由变元的数量,直接 \(O(n^6)\) 起飞。
顺着消元的这条路走应该可行,这时候一定要仔细观察柿子找性质,你发现 \((i,j)\) 的方程只含有第二维为 \(j\) 的未知数,所以对于所有的 \(j\) 单独考虑,然后把所有列的答案乘起来就行了。
现在暴力高斯消元的话时间复杂度 \(O(n^4)\),我们可以用线性基代替高斯消元,因为自由变元等价于线性无关量的个数,也就是我们看一个方程是否能够被其他方程表示出来(但是系数为 \(0\) 才能这么做),然后用 \(\tt bitset\) 优化即可,时间复杂度 \(O(\frac{n^4}{w})\)
#include <cstdio>
#include <bitset>
#include <iostream>
using namespace std;
const int M = 205;
const int MOD = 998244353;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,ans=1,a[M][M],b[M][M];bitset<M> c[M];
int main()
{
freopen("matrix.in","r",stdin);
freopen("matrix.out","w",stdout);
n=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
a[i][j]=read();
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
b[i][j]=read();
for(int j=1;j<=n;j++)//枚举每一列
{
for(int i=1;i<=n;i++)
c[i].reset();
for(int i=1;i<=n;i++)//枚举行,得到方程
{
bitset<M> t;
for(int k=1;k<=n;k++)
t[k]=t[k]^a[i][k];
t[i]=t[i]^b[i][j];
for(int k=n;k>=1;k--)//插入线性基
{
if(!t[k]) continue;
if(!c[k].count())
{
c[k]=t;
break;
}
t^=c[k];
}
}
for(int i=1;i<=n;i++)
if(!c[i].count())
ans=2*ans%MOD;
}
printf("%d\n",ans);
}
集合(set)
题目描述
给 \(n\) 个物品,每个物品有一个权值 \(val\) 和一种颜色 \(col\)
这些物品只有三种颜色:\(R/G/B\),分别表示 红\(/\)绿\(/\)蓝
计算有多少个三元组 \((r,g,b)\) 满足可以从 \(n\) 个物品中挑出一个非空集合,使得集合中恰好有 \(r\) 个红色物品,\(g\) 个绿色物品,\(b\) 个蓝色物品,并且满足集合中不存在一个物品的权值大于等于另一 个物品的权值两倍。
\(n\leq 5e5,1\leq val\leq 5e5\)
解法
考试时候做出来了,直接贴考试时候的笔记。
你发现没有,本题只关乎物品个数,似乎并不在意具体选的物品是什么。
那么我们枚举一个数作为最小值,就可以得到最大值选在哪里,然后统计这个区间里面三种物品的个数即可,那么就会得到一个信息 \((x,y,z)\),在被 \((x,y,z)\) 包含的三元组都是可选的。
这样的数对只有 \(n\) 个,所以主要的问题在于统计。
那么怎么统计呢?感觉是一个偏序问题啊。如果一个三元组是合法的当且仅当存在 \(r\leq x,g\leq y,b\leq z\)
算了,先考虑一下只有两种颜色应该怎么做,那么我们枚举 \(r\)(从大到小),然后维护最大的 \(g\),时间复杂度 \(O(n\log n)\)
三种颜色就枚举 \(r,g\),然后维护最大的 \(b\),时间复杂度 \(O(n^2\log n)\)
如果我是一个不出错的大码农那么现在可以得到 \(50\) 分的高分。
瓶颈是我对于每一个 \((r,g)\) 都很想维护出最大的 \(b\)
诶,那为什么不枚举 \(r\),对 \(g\) 开一颗线段树,然后里面存最大的 \(b\) 啊!
具体来说,我们每次加入 \(r=x\) 的信息,对于 \(g\leq y\) 修改一个前缀最大值 \(b\),然后全局求和。
我带你们打,线段树怎么维护前缀最大值。
哦,因为修改都是前缀所以是值是单调的,那么我们先在线段树上二分找到分界线(维护区间最小值),然后对分界线到修改右端点区间赋值即可。
水题,垃圾,随便切。
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 500005;
const int N = 500000;
#define ll long long
#define int long long
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,b[M][3];char s[M];
ll ans,sum[4*M],mi[4*M],tag[4*M];
struct node
{
int x,y,z;
node(int X=0,int Y=0,int Z=0) : x(X) , y(Y) , z(Z) {}
bool operator < (const node &b) const
{
return x<b.x;
}
}a[M],q[M];
void down(int i,int l,int r)//区间赋值标记下传
{
if(!tag[i]) return ;
int mid=(l+r)>>1;
mi[i<<1]=mi[i<<1|1]=tag[i];
tag[i<<1]=tag[i<<1|1]=tag[i];
sum[i<<1]=(mid-l+1)*tag[i];
sum[i<<1|1]=(r-mid)*tag[i];
tag[i]=0;
}
void up(int i)
{
sum[i]=sum[i<<1]+sum[i<<1|1];
mi[i]=min(mi[i<<1],mi[i<<1|1]);
}
int find(int i,int l,int r,int x)
{
if(l==r)
{
if(mi[i]>x) return n+1;//不赋值了
return l;
}
int mid=(l+r)>>1;
down(i,l,r);
if(mi[i<<1]<=x) return find(i<<1,l,mid,x);
return find(i<<1|1,mid+1,r,x);
}
void ins(int i,int l,int r,int L,int R,int x)
{
if(L>r || l>R) return ;
if(L<=l && r<=R)
{
mi[i]=tag[i]=x;
sum[i]=x*(r-l+1);
return ;
}
int mid=(l+r)>>1;
down(i,l,r);
ins(i<<1,l,mid,L,R,x);
ins(i<<1|1,mid+1,r,L,R,x);
up(i);
}
signed main()
{
freopen("set.in","r",stdin);
freopen("set.out","w",stdout);
n=read();
for(int i=1;i<=n;i++)
{
a[i].x=read();
scanf("%s",s);
if(s[0]=='R') a[i].y=0;
if(s[0]=='G') a[i].y=1;
if(s[0]=='B') a[i].y=2;
}
sort(a+1,a+1+n);
for(int i=1;i<=n;i++)
{
//printf("->%d %d\n",a[i].x,a[i].y);
b[i][a[i].y]++;
for(int j=0;j<3;j++)
b[i][j]+=b[i-1][j];
}
for(int i=1;i<=n;i++)//枚举a[i].x为最小值
{
int t=upper_bound(a+1,a+1+n,node(a[i].x*2-1,0,0))-a-1;
q[i]=node(b[t][0]-b[i-1][0],b[t][1]-b[i-1][1],b[t][2]-b[i-1][2]);
}
sort(q+1,q+1+n);
for(int i=N,j=n;i>=0;i--)
{
while(j && q[j].x>=i)
{
int r=q[j].y,z=q[j].z+1;//因为要算b=0,所以加1
int l=find(1,0,N,z);//找到分界线
if(l<=r) ins(1,0,N,l,r,z);//直接区间赋值
j--;
}
ans+=sum[1];//根的sum
}
printf("%lld\n",ans-1);//减去(0,0,0)
}
字符串(string)
题目描述
定义两个字符串同构为存在一种置换 \(f\),满足 \(A\) 置换之后等于 \(B\),下面有个例子:
A='abae' B='cdce'
f('a')='c',f('b')='d',f('e')='e'
求一个长度为 \(n\) 的只含小写字母的字符串中有多少个不同构的子串。
解法
首先有一个奇妙的转化,把字符串中第一次出现字母的位置设为 \(0\),其他设为设为当前位置 \(-\) 上一次出现的位置,那么比较两个字符串是否同构就直接看这两个数字字符串是否相等,举个例子:
变化前:'ababccd'
变化后:'0022010'
那么我们对原来的字符串执行这样的变化,但是注意比较两个子串是否同构就不能直接比构造出来的数字字符串,因为有些第一次出现的地方应该是 \(0\) 而不是位置之差。
这道题求的是本质不同构的子串个数,可以类比本质不同的子串个数。我们把每个后缀改写之后按字典序大小排序,然后相邻两个后缀求他们的 \(lcp\) 就求出了同构的子串个数,然后拿总子串个数减一减就行了。
那么任意两个后缀的 \(lcp\) 怎么算呢?由于只有最多 \(26\) 个字符和原串的数字字符串不一样,所以是可以算的。我们把那些为 \(0\) 的位置取出来,中间的部分就是数字字符串的部分,可以直接用 \(sa\) 求出 \(height\) 数组用 \(st\) 表维护来算:
int exlcp(int i,int j)//如其名
{
int len=0,l1=0,l2=0,a[27]={},b[27]={};
for(int k=0;k<26;k++)//取出为0的位置
{
if(p2[i][k]) a[++l1]=p2[i][k];
if(p2[j][k]) b[++l2]=p2[j][k];
}
//提前排好序了,不用排序了
for(int k=1;k<=min(l1,l2);k++)
{
len++;//都是0肯定相同
int tmp=lcp(a[k]+1,b[k]+1);
if(!(a[k+1]-a[k]==b[k+1]-b[k] && tmp>=a[k+1]-a[k]-1))
{//中间长度必须要相同,要不就有0了
len+=tmp;
break;//如果这一段都相同即可继续,否则跳出
}
len+=b[k+1]-b[k]-1;
}
return len;
}
然后排序的话就用这个 \(\tt exlcp\),时间复杂度 \(O(26n\log n)\)
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int M = 100005;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,a[M],b[M],p[M][26],ls[26],lg[M];char s[M];
int m,x[M],y[M],c[M],sa[M],rk[M],hi[M],dp[M][20];
long long ans;int p2[M][26];
void solve()//对更改串后缀排序
{
m=n;//记住值域是0开始
for(int i=1;i<=n;i++) c[x[i]=b[i]]++;
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[x[i]]--]=i;
for(int k=1;k<=n;k<<=1)
{
int num=0;
for(int i=n-k+1;i<=n;i++) y[++num]=i;
for(int i=1;i<=n;i++) if(sa[i]>k) y[++num]=sa[i]-k;
for(int i=0;i<=m;i++) c[i]=0;
for(int i=1;i<=n;i++) ++c[x[i]];
for(int i=1;i<=m;i++) c[i]+=c[i-1];
for(int i=n;i>=1;i--) sa[c[x[y[i]]]--]=y[i],y[i]=0;
swap(x,y);
x[sa[1]]=1;num=1;
for(int i=2;i<=n;i++)
x[sa[i]]=(y[sa[i]]==y[sa[i-1]] && y[sa[i]+k]==y[sa[i-1]+k])?num:++num;
if(num==n) break;
m=num;
}
}
void pre()//预处理st表
{
int k=0;
for(int i=1;i<=n;i++) rk[sa[i]]=i;
for(int i=1;i<=n;i++)
{
if(rk[i]==1) continue;
if(k) k--;
int j=sa[rk[i]-1];
while(j+k<=n && i+k<=n && b[i+k]==b[j+k]) k++;
hi[rk[i]]=k;
}
for(int i=1;i<=n;i++)
dp[i][0]=hi[i];
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i+(1<<j)-1<=n;i++)
dp[i][j]=min(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}
int lcp(int i,int j)//求后缀(i,j)之间的lcp
{
int l=rk[i],r=rk[j];
if(l>r) swap(l,r);l++;
int len=r-l+1,k=lg[len];
return min(dp[l][k],dp[r-(1<<k)+1][k]);
}
int exlcp(int i,int j)//如其名
{
int len=0,l1=0,l2=0,a[27]={},b[27]={};
for(int k=0;k<26;k++)//取出为0的位置
{
if(p2[i][k]) a[++l1]=p2[i][k];
if(p2[j][k]) b[++l2]=p2[j][k];
}
//提前排好序了,不用排序了
//sort(a+1,a+1+l1);
//sort(b+1,b+1+l2);
for(int k=1;k<=min(l1,l2);k++)
{
len++;//都是0肯定相同
int tmp=lcp(a[k]+1,b[k]+1);
if(!(a[k+1]-a[k]==b[k+1]-b[k] && tmp>=a[k+1]-a[k]-1))
{
len+=tmp;
break;//如果这一段都相同即可继续
}
len+=b[k+1]-b[k]-1;
}
return len;
}
bool cmp(int i,int j)//比较后缀(i,j)的字典序关系
{
int len=exlcp(i,j);
if(i+len>n || j+len>n)
return i>j;//如果全相同那么长度小的在前
int x=(p[i][a[i+len]]==i+len)?0:b[i+len];//判断是不是0
int y=(p[j][a[j+len]]==j+len)?0:b[j+len];//一定是i+len和j+len
return x<y;
}
int main()
{
freopen("string.in","r",stdin);
freopen("string.out","w",stdout);
n=read();
scanf("%s",s+1);
for(int i=1;i<=n;i++)
{
a[i]=s[i]-'a';
if(i>1) lg[i]=lg[i>>1]+1;
}
for(int i=1;i<=n;i++)//算更改串
{
if(ls[a[i]]) b[i]=i-ls[a[i]];
ls[a[i]]=i;
}
solve();
pre();
for(int i=n;i>=1;i--)
//处理每个后缀每个字符第一次出现的位置
{
for(int j=0;j<26;j++)
p[i][j]=p[i+1][j];
p[i][a[i]]=i;
for(int j=0;j<26;j++)
p2[i][j]=p[i][j];
sort(p2[i],p2[i]+26);
}
for(int i=1;i<=n;i++)
c[i]=i;
sort(c+1,c+1+n,cmp);
ans=1ll*n*(n+1)/2;
for(int i=1;i<=n;i++)
ans-=exlcp(c[i],c[i+1]);
printf("%lld\n",ans);
}