2026ZRnoi模拟赛
Day1
T1
首先,直接计数肯定不好做,考虑如何转化,想到一个假算,就是你先钦定线性基上的行,然后其他行被这个线性基组合,这个东西太容易算重了,所以不行。转为考虑计数线性空间,然后再计算每个线性空间能被多少种矩阵张成。计数线性空间,转成计数线性基,因为一个固定维数的线性空间对应的线性基数量是固定的。先计数线性基,这个就是上面的算法,每次加入一个数,考虑不能跟之前的线性组合重复,方案数就是 \(\prod_{i=0}^{m-1}2^n-2^i\),然后计数一个m维的线性空间能有多少个线性基,还是一样,只是把n改成m,然后就是\(\prod_{i=0}^{m-1}2^m-2^i\),所以说就是有\(\frac{\prod_{i=0}^{m-1}2^n-2^i}{\prod_{i=0}^{m-1}2^m-2^i}\) 种线性空间,然后对于每个线性空间,计数能张成这个线性空间的矩阵数量,考虑将每个行向量用这个线性空间的m维坐标来表示,那么将这些坐标合成一个 \(n \times m\) 的矩阵,显然充要条件就是这个矩阵的每一列是线性无关的,所以还是用上面的方法得到这个方案数是 \(\prod_{i=0}^{m-1}2^n-2^i\),所以总方案是\(\frac{\prod_{i=0}^{m-1}(2^n-2^i)^2}{\prod_{i=0}^{m-1}2^m-2^i}\),考虑将 \(2^i\) 提出来,得到 \(\frac{\prod_{i=n-m+1}^{n}(2^i-1)^2}{\prod_{i=1}^{m}2^i-1} \times 2^{m(m-1)/2}\),分号上面的可以化成 \(\frac{\prod_{i=1}^{n}2^i-1}{\prod_{i=1}^{n-m}2^i-1}\),然后预处理一下 \(f_i=\prod_{i=1}^{m}2^i-1\),就可以 \(O(n \log p)\) 做了。
T2
赛时
本来没看到路径长度为偶数,本来想着直接挑出一课生成树直接做的,然后看到要偶数,直接倒闭了。先把树的打了,然后k=2的想到直接跑欧拉路,然后,欧拉路写错了!!!由于pretest太弱了,直接挂掉了这部分分。
正解
喵喵构造。因为我们要偶数长度,可以做到将整个图割成若干条长度为2的链然后把链缩成一条边。割链的做法类似于赛时树的做法,先跑出dfs树,然后对于u这个点的所有连向子树内的边(包括树边和前向边),然后两两匹配,根据奇偶性最多剩下一个边没匹配,然后上传到父亲就行了。然后我们发现,新图点度数的奇偶性和原图是一样的,所以我们就可以在新图上跑了。我们每次找一个奇度点,然后一直往前跑,直到碰到另一个奇度点,容易发现,中间偶度点的度数不变,然后这两个奇度点的度数变成偶数,而且我们中途不可能走到一个偶度点然后停下,所以一直跑就是对的。
Code
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=3e5+5;
int n,m,dep[N];
array<int,2> tag[N];
bool vis[N],vis2[N];
vector<int> ans;
vector< array<int,2> > g[N];
vector< array<int,3> > h[N];
void dfs1(int u){
for(auto [v,i]:g[u])
if(!dep[v]||dep[v]>dep[u]){
if(!dep[v])dep[v]=dep[u]+1,dfs1(v);
if(tag[v][0]){
auto [x,j]=tag[v];
h[u].pb({x,i,j}),h[x].pb({u,j,i}),tag[v]={0,0};
}
else{
auto [x,j]=tag[u];
if(x)h[v].pb({x,i,j}),h[x].pb({v,j,i}),tag[u]={0,0};
else tag[u]={v,i};
}
}
}
int dfs2(int u){
while(vis[h[u].back()[1]])h[u].pop_back();
auto [v,i,j]=h[u].back();
ans.pb(i),ans.pb(j),vis[i]=vis[j]=1;
if(g[v].size()&1&&!vis2[v])return v;
return dfs2(v);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
g[u].pb({v,i}),g[v].pb({u,i});
}
dep[1]=1,dfs1(1);
for(int u=1;u<=n;u++)if(g[u].size()&1&&!vis2[u]){
ans.clear();
vis2[u]=1;int v=dfs2(u);vis2[v]=1;
printf("%d %d %d\n",u,v,(int)ans.size());
for(int i:ans)printf("%d ",i);
puts("");
}
return 0;
}
Day2
T1
赛时
这个东西看着很困难,所以思考有什么特殊性质。首先啊,自然想到如果只有两个数,那么肯定有一个数小于 \(\sqrt p\),可以感性理解,然后我赛时就猜4个数的情况是有3个都小于 \(p^{1/4}\),时间复杂度 \(O(p^{3/4})\),看着这个复杂度很对啊,然后正确性错了,然后我想到如果有不选的情况,所以加上选三个数和选两个数,过大洋里了,但是正确性还是错了,pretest 0直接爆炸了。
赛后
发现糖了,其实直接 \(p^3\) 暴力加上剪枝就过了
T2
赛时
赛时看到这题就是大力分讨,然后感觉不太好写,就做T1了。
正解
实际上有很简单的dp,我的思路还是不够暴力,设 \(f_{i,j}\) 表示目前填到 \(i\),上一段填j是否合法,转移非常容易,复杂度 \(O(n^3)\) 需要优化。考虑连续的一段空格我们应该怎么填,我们发现,除了填两端的数,我们只需要填1~4,容易证明,无论两边填的是什么,我都有对应的合法方案。那我们就可以节省很多分讨了,直接在dp状态上暴力记录这5个情况,优化掉了 \(j\) 这一维,时间复杂度 \(O(n^2)\),细节适中。
Code
#include<bits/stdc++.h>
using namespace std;
namespace IO{
#define gc getchar
#define pc putchar
#define ps puts
inline int in(){
int x=0,f=1;
char c=gc();
for(;!isdigit(c);c=gc())if(c=='-')f=-1;
for(;isdigit(c);c=gc())x=x*10+(c-'0');
return x*f;
}
inline void out(int x){
if(x<0)pc('-'),x=-x;
if(x>=10)out(x/10);
pc(x%10+'0');
}
inline void outsp(int x){out(x),pc(' ');}
inline void outln(int x){out(x),ps("");}
}
using namespace IO;
const int N=1e6+5;
int t,n,a[N],pre[N],nxt[N][2],f[N][5],g[N][5];
int main(){
t=in();
while(t--){
n=in();
for(int i=1;i<=n;i++)a[i]=in();
for(int i=1;i<=n;i++)pre[i]=a[i]?a[i]:pre[i-1];
nxt[n+1][0]=nxt[n+1][1]=n+1,a[n+1]=0;
for(int i=n;i>=1;i--){
if(!a[i])nxt[i][0]=nxt[i+1][0],nxt[i][1]=nxt[i+1][1];
else if(a[i]!=a[nxt[i+1][0]])nxt[i][0]=i,nxt[i][1]=nxt[i+1][0];
else nxt[i][0]=i,nxt[i][1]=nxt[i+1][1];
}
for(int i=1;i<=n;i++)for(int j=0;j<=4;j++)f[i][j]=-1;
for(int i=0;i<n;i++)for(int j=0;j<=4;j++)if(~f[i][j]){
int x=j?j:pre[i];
for(int k=1;k<=4&&k<nxt[i+1][0]-i;k++)if(k!=x)f[i+k][k]=i,g[i+k][k]=j;
int k=a[nxt[i+1][0]];
if(nxt[i+1][0]&&x!=k&&i+k<nxt[i+1][1]&&i+k>=nxt[i+1][0])f[i+k][0]=i,g[i+k][0]=j;
}
bool p=0;
for(int i=0;i<=4;i++)if(~f[n][i]){
for(int j=n,k=i,x,y;j;j=x,k=y){
x=f[j][k],y=g[j][k];
for(int l=x+1;l<=j;l++)a[l]=j-x;
}
for(int j=1;j<=n;j++)outsp(a[j]);
ps("");p=1;break;
}
if(!p)outln(-1);
}
return 0;
}
Day3
T1
赛时
我以为重心的条件是最大子树的大小最小,然后这个条件过于难刻画了,所以就跳了。
正解
其实重心还有一个更好的条件,就是最大子树小于 \(n/2\),也就是小于其他所有子树的和,那我们就可以用dp来刻画了,首先考虑容斥,算出每个点作为重心的不合法方案,有两种情况,第一种是在子树内,第二种是子树外,那我们就可以记 \(f_{u,0/1/2,x}\),在只考虑u的子树的情况下,\(f_{u,0,x}\)表示生成大小为x的连通块的方案数(包含不在连通块的子树部分),\(f_{u,1/2,x}\) 分别代表u子树内的点因为其子树内/外而不合法,且大连通块-小连通块的大小为x的答案。然后对于 \(f_{0/1}\)这种情况就在当前节点考虑,对于 \(f_2\) 在当前节点的父亲考虑,转移就是简单的树上背包。
Code
#include<bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;
namespace IO{
#define gc getchar
#define pc putchar
#define ps puts
inline int in(){
int x=0,f=1;
char c=gc();
for(;!isdigit(c);c=gc())if(c=='-')f=-1;
for(;isdigit(c);c=gc())x=x*10+(c-'0');
return x*f;
}
inline void out(int x){
if(x<0)pc('-'),x=-x;
if(x>=10)out(x/10);
pc(x%10+'0');
}
inline void outsp(int x){out(x),pc(' ');}
inline void outln(int x){out(x),ps("");}
}
using namespace IO;
const int N=3e3+5,p=998244353;
int n,ans,s[N],pw[N],a[N][3][N<<1],b[3][N<<1];
vector<int> g[N];
int &F(int x,int y,int z){return a[x][y][z+n];}
int &G(int x,int y){return b[x][y+n];}
void add(int &x,int y){x=(x+y)%p;}
void dfs(int u,int f){
s[u]=1,F(u,0,1)=1;
for(int v:g[u])if(v!=f){
dfs(v,u);
int x=s[u]+s[v];
for(int i=-s[u];i<=s[u];i++)for(int j=0;j<3;j++)G(j,i)=F(u,j,i),F(u,j,i)=0;
for(int i=-s[u];i<=s[u];i++)for(int j=-s[v];j<=s[v];j++){
add(F(u,0,i+j),G(0,i)*F(v,0,j)%p);
add(F(u,1,i+j),G(0,-i)*F(v,1,j)%p+G(1,i)*F(v,0,-j)%p+G(0,-i)*F(v,0,j)%p*u%p);
add(F(u,2,i+j),G(0,i)*F(v,2,j)%p+G(2,i)*F(v,0,j)%p+!!j*G(0,i)*F(v,0,-j)%p*v%p);
}
s[u]=x;
}
for(int i=1;i<=s[u];i++)add(ans,(F(u,1,i)+F(u,2,i))*pw[max(0ll,n-1-s[u])]%p);
F(u,0,0)=pw[s[u]-1];
}
signed main(){
n=in();
for(int i=1;i<n;i++){
int u=in(),v=in();
g[u].pb(v),g[v].pb(u);
}
pw[0]=1;
for(int i=1;i<=n;i++)pw[i]=pw[i-1]*2%p;
dfs(1,0),outln((pw[n-1]*(n*(n+1)/2%p)%p-ans+p)%p);
return 0;
}
浙公网安备 33010602011771号