半生风雪
Typesetting math: 1%

NOIP模拟88(多校21)

Varuxn·2021-11-02 21:19·189 次阅读

NOIP模拟88(多校21)

前言

对于这套题的总体感觉就是难,然后就是自己很菜。。。

对于 T1 考试时只会一个最垃圾的背包,考完之后对于思路这一块也不是很顺利,大概这就是薄弱的地方吧。

然后 T2 是比较简单的一道题了,但是考试的时候只是拿了一点部分分,对于正解的思路也只有一点点。

大概和看错数据范围有一点关系吧,主要还是菜。

T3 T4 是那种我最害怕的题目了,T3 一看感觉根本不可做,然后一点思路没有。

T4 的博弈论更别提,推了个傻瓜特殊性质就跑路了。。。

T1 按位或

解题思路#

容斥DP好题。

先来考虑复杂度为 3log2tlogn3log2tlogn 的做法。

那么由于每一次操作都是等价的因此我们考虑计算出每一次操作可能的贡献,然后做一个 n 次方进行求解。

对于 t 可以背包 DP 每一个二进制位,算出对于 mod 3 不同余数的方案,然后直接整一个 n 次方???

当然没有那么简单,发现有一些情况是不合法的,有一些二进制位的组合可能或在一起并不是我们要求的 t 但是他们每一个数字确实是 3 的倍数。

于是我们需要枚举 t 的所有子集,进行容斥,最后需要再容斥一下 0 的情况。code

但是显然这不是正解的复杂度。。。

于是我们可以考虑每一个二进制位的贡献,对于 2i 如果 i 是奇数,那么 2imod3=2 否则就是 1 。

那么对于所有奇数或者说所有偶数而言他们对于答案的贡献都是相同的。

于是我们可以直接枚举奇数偶数位选择多少个,背包 DP 算出对应方案数,然后类似于上面的不合法情况直接容斥。

最后依旧做一个 n 次方进行求解,也需要考虑 0 的情况。

然后我考场上拿 n3 的背包打了个表,然后。。代码文件过大??大吗??

然后我尝试这分块打表,然后。。。。

code#

Copy
#include<bits/stdc++.h> #define int long long #define ull unsigned long long #define f() cout<<"RP++"<<endl #define count __builtin_popcount using namespace std; inline int read() { int x=0,f=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*f; } const int N=70,mod=998244353; int n,m,ans,cnt,cnt1,cnt2,f[3],g[3],c[N][N]; int power(int x,int y,int p=mod) { int temp=1; n%=(p-1); while(y) { if(y&1) temp=temp*x%p; x=x*x%p; y>>=1; } return temp; } void solve(int x,int y) { f[1]=f[2]=0; f[0]=1; for(int i=1;i<=x;i++) { for(int j=0;j<=2;j++) g[j]=f[j]; for(int j=0;j<=2;j++) f[(j+1)%3]=(f[(j+1)%3]+g[j])%mod; } for(int i=1;i<=y;i++) { for(int j=0;j<=2;j++) g[j]=f[j]; for(int j=0;j<=2;j++) f[(j+2)%3]=(f[(j+2)%3]+g[j])%mod; } } #undef int int main() { #define int long long freopen("or.in","r",stdin); freopen("or.out","w",stdout); n=read(); m=read(); int temp=m,pos=0; while(temp) { if(temp&1) cnt++,cnt1+=pos&1; pos++; temp>>=1; } c[0][0]=1; for(int i=1;i<=cnt;i++) { c[i][0]=c[i][i]=1; for(int j=1;j<i;j++) c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod; } for(int i=1,bas=(cnt&1)?1:-1;i<=cnt;i++,bas=-bas) for(int j=0;j<=min(i,cnt1);j++) solve(i-j,j),ans=(ans+bas*power(f[0],n)*c[cnt1][j]%mod*c[cnt-cnt1][i-j]%mod+mod)%mod; printf("%lld",(ans+((cnt&1)?-1:1)+mod)%mod); return 0; }

T2 最短路径

解题思路#

并不是一道难题。

显然对于固定的 k 个点最短路径的起点终点一定是其中的两个。

假设最后必须回到起点,一条最短路径就可以视为遍历这 k 个点所需要经过的边的数量乘上 2 。

那么加上终点之后呢?我们一定是选择一对距离最长的点作为终点和起点。

考虑分别计算贡献,对于第一部分一条边有贡献当且仅当它两边的两个部分都有被选中的点,方案数直接组合数计算就好了, n 不是特别大 n 或者 n2 均可。

对于第二部分,直接暴力枚举所选的点对并且计算可以选的其他点就好了,为了防止重复计算,对于两个点到某个点距离相同且都是最长路径的情况我们选择便后较小来计算贡献。

对于点对 (x,y) 一个合法的 z 一定满足以下条件:

  • disx,y>disx,z

  • disx,y=disx,zy<z

  • disx,y>disy,z

  • disx,y=disy,zx<z

假设合法的点有 cnt 个,那么这条路径的贡献就是 (k2cnt)×len

code#

Copy
#include<bits/stdc++.h> #define int long long #define ull unsigned long long #define f() cout<<"RP++"<<endl using namespace std; inline int read() { int x=0,f=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*f; } const int N=2e3+10,INF=1e18,mod=998244353; int n,m,k,root,ans,top,sta[N],s[N],c[N][N],bas[N]; int tim,dfn[N],siz[N],son[N],dep[N],fa[N],topp[N]; int tot=1,head[N],nxt[N<<1],ver[N<<1]; void add_edge(int x,int y){ver[++tot]=y;nxt[tot]=head[x];head[x]=tot;} void dfs1(int x) { siz[x]=1; for(int i=head[x];i;i=nxt[i]) { int to=ver[i]; if(to==fa[x]) continue; fa[to]=x; dep[to]=dep[x]+1; dfs1(to); siz[x]+=siz[to]; bas[x]+=bas[to]; if(siz[to]>siz[son[x]]) son[x]=to; if(!bas[to]||bas[to]==m) continue; ans=(ans+(c[m][k]-c[bas[to]][k]-c[m-bas[to]][k]+2*mod)*2)%mod; } } void dfs2(int x,int tp) { topp[x]=tp; dfn[x]=++tim; if(son[x]) dfs2(son[x],tp); for(int i=head[x];i;i=nxt[i]) if(!dfn[ver[i]]) dfs2(ver[i],ver[i]); } int LCA(int x,int y) { while(topp[x]^topp[y]) { if(dep[topp[x]]<dep[topp[y]]) swap(x,y); x=fa[topp[x]]; } if(dep[x]>dep[y]) swap(x,y); return x; } inline int dist(int x,int y){return dep[x]+dep[y]-2*dep[LCA(x,y)];} int power(int x,int y,int p=mod) { int temp=1; while(y) { if(y&1) temp=temp*x%p; x=x*x%p; y>>=1; } return temp; } void solve(int x,int y) { int cnt=0,dis=dist(x,y); for(int i=1;i<=m;i++) { if(s[i]==x||s[i]==y) continue; if(dist(x,s[i])>dis||dist(y,s[i])>dis) continue; if(dist(x,s[i])==dis&&s[i]<y) continue; if(dist(y,s[i])==dis&&s[i]<x) continue; cnt++; } if(cnt>=k-2) ans=(ans-dis*c[cnt][k-2]%mod+mod)%mod; } #undef int int main() { #define int long long freopen("tree.in","r",stdin); freopen("tree.out","w",stdout); n=read(); m=read(); k=read(); for(int i=1;i<=m;i++) s[i]=read(),bas[s[i]]=1; for(int i=1,x,y;i<n;i++) x=read(),y=read(),add_edge(x,y),add_edge(y,x); c[0][0]=1; for(int i=1;i<=n;i++) c[i][0]=c[i][i]=1; for(int i=1;i<=n;i++) for(int j=1;j<i;j++) c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod; dfs1(1); dfs2(1,1); sort(s+1,s+m+1); for(int i=1;i<=m;i++) for(int j=i+1;j<=m;j++) solve(s[i],s[j]); printf("%lld",ans*power(c[m][k],mod-2)%mod); return 0; }

T3 仙人掌

神仙仙人掌+行列式,咕了

大坑未补#

T4 对弈

解题思路#

又是 老脸买原题 系列

有一个性质: 在最优策略下,如果可以, Alice 只会向右移动, Bob 只会向左移动。

证明:记 ai 表示第 i 个红棋的位置, bi 表示第 i 个蓝棋的位置,我们将一个状态表示为一个 k2 元组 (b1a11,b2a21...bk2ak21) 。记 di=biai1 ,如果 Alice 将 ai 向左移动,那么在它右边的那个蓝棋 bi 一定可以移动相同距离,使得对应的 di 不变,但是显然 ai 的移动范围变小了,不这么做显然不会更劣。

在这个性质的前提下 di 只可能会越来越小,就变成了一个取石子游戏,也就是(下文中的变量名可能与上文没有关系):

n 堆石子,每堆有 ai 个,每次可以选 1m 堆,从这几堆中各取走若干石子,每堆至少取一个。若轮到某人时,所有石子都已经被取完了,这个人就输了。

那么必败局面当且仅当将所有 ai 转成二进制后,每一位 1 的个数 mod(m+1)=0

证明

  1. 全为 0 的局面一定是必败态。

  2. 任何一个 N 状态(必败状态),经过一次操作以后必然会到达 P 状态(必胜状态)。在某一次移动中,至少有一堆被改变,也就是说至少有一个二进制位被改变。由于最多只能改变 m 堆石子,所以对于任何一个二进制位, 1 的个数至多改变 m 。而由于原先的总数为 m+1 的整数倍,所以改变之后必然不可能是 m+1 的整数倍。故在 N 状态下一次操作的结果必然是 P 状态。

  3. 任何 P 状态,总有一种操作使其变化成 N 状态。从高位到低位考虑所有的二进制位。假设用了某种方法,改变了 k 堆,使第 i 位之前的所有位的 1 的个数都变成 m+1 的整数倍。现在要证明总有一种方法让第 i 位也变成 m+1 的整数倍。显然,对于已经改变的那 k 堆,当前位可以自由选择 0 或 1 。设除去已经更改的 k 堆,剩下的堆第 i 位上 1 的总和 mod(m+1)=sum ,分类讨论:

  • summk 此时可以将这些堆上的 1 全部拿掉,然后让那 k 堆的第 i 位全部置成 0 。
  • sum>mk 此时我们在之前改变的 k 堆中选择 m+1sum 堆,将他们的第 i 位置成 i ,剩下的置成 0 。由于 m+1sumk ,故这是可以达到的。

那么就可以 DP 了 f(i,j) 表示考虑了前 i 位,当前总和为 j 的方案数。

于是从 k2 堆里面选出 num×(m+1) 个并且这些石子的第 i 位为 1 。

并且我们还要选出这几堆的位置,于是方程就是:

f(i+1,j+2i×num×(m+1))+=f(i,j)×(num×(m+1)k2)

f(i,j)=f(i,j)×(nik2k2)

最后拿总方案数一减就好了。

code#

Copy
#include<bits/stdc++.h> #define int long long #define ull unsigned long long #define f() cout<<"RP++"<<endl using namespace std; inline int read() { int x=0,f=1;char ch=getchar(); while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} return x*f; } const int N=3e4+10,Lg=70,mod=1e9+7; int ans,n,m,k,lg,fac[N],ifac[N],f[Lg][N]; int power(int x,int y,int p=mod) { int temp=1; while(y) { if(y&1) temp=temp*x%mod; x=x*x%mod; y>>=1; } return temp; } void add(int &x,int y){x+=y;if(x>mod)x-=mod;} int C(int x,int y){if(x<y)return 0;return fac[x]*ifac[y]%mod*ifac[x-y]%mod;} #undef int int main() { #define int long long freopen("chess.in","r",stdin); freopen("chess.out","w",stdout); n=read(); k=read(); m=read(); lg=log2(n)+1; f[0][0]=1; fac[0]=ifac[0]=1; for(int i=1;i<=n;i++) fac[i]=fac[i-1]*i%mod; ifac[n]=power(fac[n],mod-2); for(int i=n-1;i>=1;i--) ifac[i]=ifac[i+1]*(i+1)%mod; for(int i=0;i<=lg;i++) for(int j=0;j<=n-k;j++) for(int p=0;;p++) if((p<<i)*(m+1)>n-k||p*(m+1)>k/2) break; else add(f[i+1][j+(p<<i)*(m+1)],f[i][j]*C(k/2,p*(m+1))%mod); for(int i=0;i<=n-k;i++) add(ans,f[lg][i]*C(n-i-k/2,k/2)%mod); printf("%lld",(C(n,k)-ans+mod)%mod); return 0; }
posted @   Varuxn  阅读(189)  评论(1编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示
目录