模拟赛好题总结
模拟赛好题总结
20230924C.位运算
考虑 \(x\ xor\ y\) 的性质,若 \(popcount(x)\% 2==popcount(y)\% 2\) 则 \(popcount(x\ xor\ y)\% 2==0\) ,反之亦然
\([1,x]\) 中二进制下有奇数个 \(1\) 的数字个数为 \(x/2+(x\% 2||popcount(x)\% 2)\)
证明
by lyk
证明 \([1,x]\) 二进制表示下 \(1\) 的个数为奇数的数字个数为 \(x/2+(x\% 2||popcont(x)\% 2)\)
设 \(x\) 二进制表示下 \(1\) 的个数为 \(f(x)\)
对于任意偶数 \(k\),\(f(k)\) 与 \(f(k+1)\) 一奇一偶
同时,\(f(1)\) 为奇数
所以对于任意奇数 \(l\) 而言,\([1,l]\) 内 \(f(x)\) 为奇数的有 \(l/2+1\)
对于偶数情况,我们考虑是否与 \(f(1)\) 奇偶性相同
不同则 \(x/2\)
相同则 \((x-2)/2+1+1=x/2+1\)
by yzx
20250122A.数数
不敏感的数位题
给定 \(L,R\) 问 \([L,R]\) 中满足数位可以分成两个集合使其和相等的数的个数。\(L,R\in[1,10^9]\)
上来一眼可以想到一个 \(DP\) 是 \(f[i][j][0/1]\) 表示考虑前 \(i\) 位,两个集合的差值是 \(j\),是否顶限制上界 的方案数。
转移时枚举当前填什么,放到哪个集合。 然后发现这个东西会算重,例如 \(1111\)。
这时要发现一个数合不合法对于他的数位集合是固定的而和顺序没有关系。
也就是说我们可以先找出所有合法的数位集合,再将集合里的数随意排列,得到的数都是合法的。这就好做了。
那我们可以爆搜每个合法的数位集合对其分别统计答案即可,注意判断上界。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll s=0,k=1;
char c=getchar();
while(c>'9'||c<'0'){
if(c=='-') k=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
s=(s<<3)+(s<<1)+(c^48);
c=getchar();
}
return s*k;
}
const int N=12;
int a[N];
bitset<55>f,g[N];
vector<int>c;
ll C[30][30],ans;
void calc(vector<int>c){
for(int i=10;i>=1;i--){
for(int j=0;j<a[i];j++)
if(c[j]){
c[j]--;
ll res=1,sum=0;
for(int x:c) sum+=x;
for(int x:c) res*=C[sum][x],sum-=x;
ans+=res;
c[j]++;
}
if(c[a[i]]) c[a[i]]--;
else break;
}
}
void dfs(int dep,int lst,int sum){
if(!dep){
if(sum&1) return ;
if(!f[sum>>1]) return ;
calc(c);
return;
}
for(int i=lst;i<=9;i++){
c[i]++;
g[dep]=f;
f=f|f<<i;
dfs(dep-1,i,sum+i);
f=g[dep];
c[i]--;
}
}
ll solve(ll x){
for(int i=1;i<=10;i++){
a[i]=x%10;
x/=10;
}
f[0]=1; ans=0;
dfs(10,0,0);
return ans;
}
int main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
C[0][0]=1;
for(int i=1;i<=25;i++){
C[i][0]=1;
for(int j=1;j<=i;j++)
C[i][j]=C[i-1][j-1]+C[i-1][j];
}
c.resize(10);
int L=read(),R=read();
printf("%lld",solve(R+1)-solve(L));
return 0;
}
20250124C.彩树
给定一张 \(n\) 个点 \(m\) 条边的简单无向图 \(G\),点有值域为 \([1,k]\) 的颜色 \(a_i\),求有多少生成树满足每个颜色都在其中出现且只出现一次。\(n\leq 200,K\leq 12\)
考虑状压 \(DP\),设 \(f[S][i]\) 表示当前有集合 \(i\) 中的颜色,目前树根在 \(i\) 的方案数。
有转移 \(f[S\cup T][i]\gets f[S][i]\times f[T][j]\) 满足 \(i,j\) 间有边且 \(S\cap T=\emptyset\)。由于每种颜色只能出现一次,所以只要 \(S,T\) 不交,那他们选的点也一定不交,保证了这个一个树且不会算重。
但是这个的复杂度是 \(O(3^k\times m)\) 过不掉。
我们可以把他拆成两步,把复杂度降到 \(O(3^k\times n)\)。
设 \(g[S][i]\) 表示当前有 \(S\cup a_i\) 的颜色,树根在 \(i\),且 \(i\) 度数为 \(1\) 的方案数。\(f,g\) 的表示区域如下图:
那么 \(g\) 的转移就是 \(g[S][i]\gets f[S][j]\) 满足 \(S\) 中没有 \(i\) 且 \(i,j\) 间有边。复杂度 \(O(2^k\times n^2)\)
此时 \(f\) 的转移就是 \(f[S\cup a_i][i]\gets f[S\setminus T\cup a_i][i]\times g[T][i]\) 满足 \(T\subseteq S\),且为了避免重复,我们要求 \(T\) 中包含 \(S\) 中最小的元素。复杂度 \(O(3^k\times n)\)
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll s=0,k=1;
char c=getchar();
while(c>'9'||c<'0'){
if(c=='-') k=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
s=(s<<3)+(s<<1)+(c^48);
c=getchar();
}
return s*k;
}
const int N=205,M=1<<12,mod=998244353;
int n,m,K,a[N][N],c[N];
bool e[N][N];
ll f[N][M],g[N][M];
void Add(ll &x,ll y){
x+=y;
if(x>=mod) x-=mod;
}
ll ksm(ll a,int b){
ll t=1;
for(;b>0;b>>=1,a=a*a%mod)
if(b&1) t=t*a%mod;
return t;
}
int main(){
n=read();m=read();K=read();
for(int i=1;i<=n;i++) c[i]=read()-1;
for(int i=1;i<=m;i++){
int u=read(),v=read();
e[u][v]=e[v][u]=1;
}
for(int i=1;i<=n;i++) f[i][1<<c[i]]=1;
for(int s=0;s<1<<K;s++){
for(int i=1;i<=n;i++)
if(!(s>>c[i]&1))
for(int j=1;j<=n;j++)
if(e[i][j]&&c[i]!=c[j]) Add(g[i][s],f[j][s]);
for(int i=1;i<=n;i++)
if(!(s>>c[i]&1)){
int x=s&-s;
for(int j=s;j>0;j=(j-1)&s)
if(j&x) Add(f[i][s+(1<<c[i])],f[i][s-j+(1<<c[i])]*g[i][j]%mod);
}
}
ll ans=0;
for(int i=1;i<=n;i++) Add(ans,f[i][(1<<K)-1]);
printf("%lld",ans*ksm(K,mod-2)%mod);
return 0;
}
20250124D.石头剪刀布
对于一个包含字符 \(\{R,P,S\}\) 的字符串 \(s\),满足每相邻两个字符都不同且 \(s_1=s_n=R\)。
你可以做两种操作:
- 找出第一个 \(RS\) 或 \(SR\) 将其替换为 \(R\)。
- 找出第一个 \(SP\) 或 \(PS\) 将其替换为 \(S\)。
若无法进行任意一个操作则结束,否则继续执行,计操作序列为 \(b\)。
给你一个长为 \(n\) 的字符串 \(s\) 包含 \(\{R,P,S,?\}\),你可以将 \(?\) 替换为 \(\{R,P,S\}\) 中的一个使得其字符串满足上述条件,并对其进行操作得到 \(b\),问有多少方案,两个方案不同当且仅当 \(s\) 和 \(s'\),\(b\) 和 \(b'\) 至少有一个不同。\(n\leq 200\)
首先 \(R\) 是无法删除的,那么我们希望用 \(R\) 来把问题分成若干段。注意到,我们每次删除 \(P\) 的时候,一定是删除最开头的 \(P\)(除 \(RPR\) 这唯一一个特例删不掉 \(P\))。但 \(S\) 就没有这么好的性质了。
所以设 \(f[i][j][k]\) 表示考虑前 \(i\) 个位置,第 \(i\) 个位置是 \(R\),且第 \(i\) 个位置前面有 \(j\) 个 \(S\) 未被删除(这 \(j\) 个 \(S\) 不被 \(P\) 包裹),后面有 \(k\) 个 \(S\) 要删除时的答案。转移时,枚举下一个 \(R\) 的位置,那么中间的部分一定是 \(S\) 和 \(P\) 交替出现。
肯定没法直接转移到因为中间的 \(S\) 可能会被 \(P\) 包裹而不能直接删掉,不满足 \(f\) 的状态定义(例如 \(RPSPR\)),所以可以考虑再记录另一个 \(dp\) 为 \(g[i][l][j][k][0/1][0/1]\) ,其中 \(i,j,k\) 与上面类似,\(l\) 表示中间的部分里 \(P\) 还有几个,中间的部分的 开头\(/\)结尾 不是\(/\)是 \(S\) (\(1\) 表示是 \(S\))。
即 \(f,g\) 定义的直观解释如:
还有一个问题就是考虑 \(RSPSPSR\) 删掉一个 \(P\) 后就会变成 \(RSSPSR\),导致它中间 \(S,P\) 不再交替,这个形式是没有状态来记录的。但是我们可以将他微调为 \(SRSPSR\),即把第一个 \(S\) 交换到 \(R\) 的前面,那这个就有状态就可以记录了,而且它仍然满足可以直接删掉。
为了方便我们写代码和转移,我们可以稍微改一下 \(g\) 的状态,我们让中间那段的左端必须是 \(P\),前面一旦出现 \(S\) 就扔到 \(R\) 左边,并将第一个 \(0/1\) 改为记录其左端 \(R\) 的左边还有没有从中间扔过去的 \(S\)。
转移比较多,可以自己先推一下试试,但其实还是有点难度的。
转移
对于 \(f[j]*\) 对 \(f[i]*,g[i]*\) 的转移:
枚举上一个 \(R\) 的位置 \(j\) 及其状态,枚举中间段的左右端点是 \(fl,fr\),可以算得中间段 \(P\) 的个数是 \(p=\frac{i-j-fl-fr}{2}\):
- 特殊处理 \(RSR\) 的情况:
- \(f[i][k][l-1]\gets f[j][k][l],l\geq 1\)(因为 \(l\geq 1\) 所以这个 \(S\) 要删。)
- \(f[i][k+1][l]\gets f[j][k][l],l==0\)(\(l==0\) 这个 \(S\) 不用删。)
- 否则有转移:
- \(g[i][p][k+fl][t][fl][fr]\gets f[j][k][l]\),对于 \(j\) 位置需要往后删掉 \(l\) 个 \(S\),那么 \([j,i]\) 这一段中目前能直接删掉的 \(S\) 就是开头和结尾的两个(如果他们是 \(S\) 的话)所以如果 \(t\geq 1,fl=1\) 则 \(t--,fl=0\),\(fr\) 同样(初始 \(t=l\))。
对于 \(g[x]*\) 和 \(f[x]*\) 的转移:
- 特判 \(RPR\) 的情况 \(f[x][j][k]\gets g[x][i][j][k][fl][fr]\)
- 删除一个 \(S\):
- \(g[x][i][j-1][k][fl][fr]\gets g[x][i][j][k][fl][fr],j>1\)
- \(g[x][i][0][k][0][fr]\gets g[x][i][j][k][fl][fr],j==1\)(如果左边只剩一个 \(S\),删除后左边没有 \(S\),\(fl=0\))
- \(g[x][i][0][k][0][0]\gets g[x][i][0][k][0][1]\)(左边没有能删的 \(S\) 了,那删除 \(fr\) 处的 \(S\))
- \(g[x][i][0][k+1][0][0]\gets g[x][i][0][k][0][0]\)(目前已经没有 \(S\) 了,那么 \(k+1\),告诉后面还要多删一个 \(S\))
- 删除一个 \(P\):一定删掉了最左边的 \(P\),那么他一定会新露出一个 \(S\),我们把他直接扔到 \(R\) 左边。注意当中间的 \(P\) 只剩一个的时候就已经不存在被 \(P\) 包裹的 \(S\) 了,所以要 \(i>1\)。\(g[x][i-1][j+1][k][1][fr]\gets g[x][i][j][k][fl][fr],i>1\)
- 当 \(P\) 只剩一个的时候,更新答案 \(f[x][j+fr][k]\gets g[x][i][j][k][fl][fr],i==1\)
转移就这么多。
\(f\) 的状态数有三维,转移 \(O(n)\)。\(g\) 的状态数有四维,转移 \(O(1)\),所以时间复杂度为 \(O(n^4)\)。
\(g\) 数组可以滚动,空间复杂度为 \(O(n^3)\)。常数很小,可以通过。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll s=0,k=1;
char c=getchar();
while(c>'9'||c<'0'){
if(c=='-') k=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
s=(s<<3)+(s<<1)+(c^48);
c=getchar();
}
return s*k;
}
const int N=1005,M=205,mod=998244353;
char s[N];
int n;
ll g[M][M][M][2][2],f[M][M][M];
void Add(ll &x,ll y){
x+=y;
if(x>=mod) x-=mod;
}
bool ok(char x,int y){
if(x=='?') return 1;
if(y==1&&x!='S') return 0;
if(y==0&&x!='P') return 0;
return 1;
}
void clear(int n,int m,int K){
for(int i=0;i<=n;i++)
for(int j=0;j<=m;j++)
for(int k=0;k<=K;k++)
for(int fll=0;fll<2;fll++)
for(int flr=0;flr<2;flr++) g[i][j][k][fll][flr]=0;
}
void solve(ll f[M][M],int n,int m,int K){
for(int i=n;i>=1;i--)
for(int j=m;j>=0;j--)
for(int k=0;k<=K;k++)
for(int fll=1;fll>=0;fll--)
for(int flr=1;flr>=0;flr--)
if(g[i][j][k][fll][flr]){
ll &v=g[i][j][k][fll][flr];
if(i==1&&!fll&&!flr){
Add(f[j][k],v);
continue;
}
if(j==1) Add(g[i][0][k][0][flr],v);
else if(j>1) Add(g[i][j-1][k][fll][flr],v);
else if(flr) Add(g[i][j][k][fll][0],v);
else Add(g[i][j][k+1][fll][flr],v);
if(i>1) Add(g[i-1][j+1][k][1][flr],v);
else Add(f[j+flr][k],v);
}
}
int main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
scanf("%s",s+1);
n=strlen(s+1);
f[1][0][0]=1;
for(int i=2;i<=n;i++){
if(s[i]!='?'&&s[i]!='R') continue;
clear(i>>1,i>>1,n-i+1>>1);
for(int flr=0;flr<2;flr++)
for(int j=i-2;j>=1;j--){
int fll=i-j-2+flr&1;
if(!ok(s[j+1],fll)) break;
for(int k=0;k<=j>>1;k++)
for(int l=0;l<=n-j+1>>1;l++)
if(f[j][k][l]){
if(j==i-2&&flr){
if(l) Add(f[i][k][l-1],f[j][k][l]);
else Add(f[i][k+1][l],f[j][k][l]);
continue;
}
int t=l,p=i-j+1-fll-flr-1>>1;
int fl0=fll,fl1=flr;
if(t&&fl0) t--,fl0--;
if(t&&fl1) t--,fl1--;
Add(g[p][k+fl0][t][fl0][fl1],f[j][k][l]);
}
}
solve(f[i],i>>1,i>>1,n-i+1>>1);
}
ll ans=0;
for(int i=0;i<=n;i++) Add(ans,f[n][i][0]);
printf("%lld",ans);
return 0;
}
20250216A.等差数列
给定一个长为 \(n\) 的序列 \(A\),将其重派为一个序列 \(B\),使其是在 \(\bmod M\)(\(M\) 是质数)意义下的等差数列。输出首项和公差,多组解任意输出一个或报告无解。
首先我们可以证明对于一个首项和公差 \((a,d)a,d\in [0,M)\) 他的前 \(M\) 项会使 \([0,M)\) 的每个数都恰好出现一次,也即在第 \(M+1\) 次成环。
证明:考虑如果出现重复的数即有 \(a+kd\equiv a(\bmod M)\) 即 \(kd\equiv 0(\bmod M)\),由于 \(M\) 是质数和 \(d<M\),所以 \(k\) 一定是 \(M\) 的倍数,所以在前 \(M\) 项中都有 \(k<M\),不会满足条件,那么 \([0,M)\) 的每个数会出现一次。
我们任意找到两个数字,他们的差记为 \(kd\)。即公差的 \(k\) 倍。我们发现:
- 如果 \(2n\leq M\),那么序列 \(A\) 中恰有 \(k\) 个数 \(x\) 满足 \(x+kd\) 不在 \(A\) 中,得到 \(d\),然后检验若 \(x-d\) 不存在,则 \(x\) 是首项。
- 如果 \(2n>M\),那么我们就对 \(A\) 取在 \([0,M)\) 中的补集做上面的算法求得首项和公差,那原序列的公差和补集的公差是一样的,首项就是补集最后一项再加 \(d\),因为这时会成环,所以 \(x+d\) 就是原序列的首项。
为什么要以 \(2n\) 和 \(M\) 的关系作为分界呢?
因为我们在获取到 \(kd\) 后计算 \(k\) 的时候,如果 \(2n\leq M\),由于前 \(M\) 个每个数只出现一次,那等差数列最后的 \(k\) 个数的 \(x+kd\) 都是还没有出现的数。如果 \(2n>M\),那最后 \(k\) 个数中有若干个会成环而导致 \(k\) 少算。
代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline ll read(){
ll s=0,k=1;
char c=getchar();
while(c>'9'||c<'0'){
if(c=='-') k=-1;
c=getchar();
}
while(c>='0'&&c<='9'){
s=(s<<3)+(s<<1)+(c^48);
c=getchar();
}
return s*k;
}
const int N=1e5+5;
ll n,a[N],mod;
ll ksm(ll a,int b){
ll t=1;
for(;b;b>>=1,a=a*a%mod)
if(b&1) t=t*a%mod;
return t;
}
pair<ll,ll> check(ll x,ll d){
ll tmp=x;
map<ll,bool>mp;
for(int i=1;i<=n;i++) mp[a[i]]=1;
for(int i=1;i<=n;i++){
if(!mp[x]) return {-1,-1};
mp[x]=0;
(x+=d)%=mod;
}
return {tmp,d};
}
pair<ll,ll> sol(){
ll kd=(a[2]-a[1]+mod)%mod;
map<ll,bool> mp;
for(int i=1;i<=n;i++) mp[a[i]]=1;
ll k=0;
for(int i=1;i<=n;i++)
if(!mp.count((a[i]+kd)%mod)) k++;
ll d=kd*ksm(k,mod-2)%mod;
for(int i=1;i<=n;i++)
if(!mp.count((a[i]-d+mod)%mod))
return check(a[i],d);
return {-1,-1};
}
void solve(){
mod=read();n=read();
for(int i=1;i<=n;i++) a[i]=read();
if(n==1){
printf("%lld 0\n",a[1]);
return ;
}
if(n==2){
printf("%lld %lld\n",min(a[1],a[2]),max(a[1],a[2])-min(a[1],a[2]));
return ;
}
if(n+n<=mod){
pair<ll,ll> x=sol();
if(x.first==-1) puts("-1");
else printf("%lld %lld\n",x.first,x.second);
}
else{
map<ll,bool>mp;
for(int i=1;i<=n;i++) mp[a[i]]=1;
n=0;
for(int i=0;i<mod;i++)
if(!mp[i]) a[++n]=i;
pair<ll,ll> x=sol();
if(x.first==-1) puts("-1");
else{
x.first=(x.first+x.second*n%mod)%mod;
printf("%lld %lld\n",x.first,x.second);
}
}
}
int main(){
// freopen(".in","r",stdin);
// freopen(".out","w",stdout);
int T=read();
while(T--) solve();
return 0;
}
<\details>