<题面链接> 提取码:7puf
Part1. 游记
果不其然还是做模拟赛……习惯就好……
莫名感觉今天是数学专场,进门就一道期望题,还好知道套路,最后忘了写线性求逆元,被卡了;
有一些奇怪的数学结论忘了,像中国剩余定理之类的?估计回去要恶补数论了……(为什么我记得之前也说过要补数论,还学了一下中国剩余定理和扩展中国剩余定理?)
最后有一道 DAG 最长反链 等于 二分图最大匹配 的题,反正我是没有见过这个套路,还被一个 dalao 喷了“这不是基本常识吗?”
得出结论“My vegetable has explosed…”
Tab. 这篇博客也是 Day7 补的,之前的一篇因为计算机 D盘蜜汁清空了……
又及,用Typora写博客的时候换了一个主题,莫名快乐
Part2. Solution
T1. 锻造(forging)
- Experience
你看这是一道期望题,显然我们要 Dp……
于是非常轻松(或许吧)地推导出了转移方程,最后不知道直接求逆元会被卡,就没有预处理逆元,结果就出锅了……
- Solution
可以得到一个显然的 Dp 状态:f[i]
表示锻造出 i 级剑的期望花费。
我们考虑从 f[i-1],f[i-2]
推导出 f[i]
,设 i 级剑锻造成功的概率为 p[i]
:
我们需要一把 i-1 级剑和一把 i-2 级剑锻造出 i 级剑,即 f[i]=f[i-1]+f[i-2]
;
但是假如失败了呢,有 (1-p[i])
的概率?我们会得到一把 i-2 级剑,要再锻造 i 级剑,我们需要锻造一把 i-1 级剑,即 f[i]=f[i-1]+f[i-2]+(1-p[i])f[i-1]
;
假如又失败了?我们仍然需要一把 i-1 级剑,即 f[i]=f[i-1]+f[i-2]+(1-p)(f[i-1]+(1-p)f[i-1])
……
假如一直失败,则有 f[i]=f[i-1]+f[i-2]+(1-p)(f[i-1]+(1-p)(f[i-1]....))
;
定义 g[i]=f[i-1]+(1-p)g[i]
,解一下方程,可得 g[i]=f[i-1]/p
;由于 f[i]=g[i]+f[i-2]
,则 f[i]=f[i-1]/p+f[i-2]
;
于是我们就得到了一个状态转移方程式。
显然这里的除以 $p$ 我们需要逆元,不过这道题直接算逆元是会 TLE 的……于是我们需要预处理逆元,线性筛即可。(因为 inv(x) 是积性函数)
Q:这里的 $p$ 可能很大啊,怎么储存它的逆元呢?
A: 根据题意我们可以知道,$p=\frac {\min(c_{i-1},b_{i-2})}{c_{i-1}}$ ,把这个式子代回转移方程式:
所以我们并不是真正预处理 $p$ 的逆元,而是预处理 $\min(c_{i-1},b_{i-2})$ 的逆元,实际规模只有 $10^7$,所以是可以储存的。
- Source
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include<set> #include<queue> #include<stack> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std;
typedef long long ll; const ll MOD=998244353; const int LIMIT=int(1e7);
int inv[LIMIT+3],prn[LIMIT/2+3]; bool vis[LIMIT+3]; int n,m,Tbx,Tby,Tcx,Tcy,Tp,nprn; int QPow(int A,int B){ int ret=1; while(B){ if(B&1) ret=int(1ll*ret*A%MOD); A=(int)(1ll*A*A%MOD); B>>=1; } return ret; } void Process(){ inv[1]=1; for(int i=2;i<=LIMIT;i++){ if(!vis[i]){ prn[++nprn]=i; inv[i]=QPow(i,MOD-2); } for(int j=1;j<=nprn && 1ll*i*prn[j]<=LIMIT;j++){ vis[i*prn[j]]=true; inv[i*prn[j]]=(int)(1ll*inv[i]*inv[prn[j]]%MOD); if(i%prn[j]==0) break; } } } int f[10],b[10],c[10]; int main(){ freopen("forging.in","r",stdin); freopen("forging.out","w",stdout); Process(); scanf("%d%d%d%d%d%d%d",&n,&m,&Tbx,&Tby,&Tcx,&Tcy,&Tp); f[0]=m; b[0]=Tby+1;c[0]=Tcy+1; for(int i=1;i<=n;i++){ int I=i%3,J=(i-1)%3,K=(i-2)%3; b[I]=int((1ll*b[J]*Tbx+Tby)%Tp+1); c[I]=int((1ll*c[J]*Tcx+Tcy)%Tp+1); if(i==1) f[1]=int(f[0]+1ll*f[0]*c[0]%MOD*inv[min(c[0],b[0])]%MOD)%MOD; else f[I]=int(f[K]+1ll*f[J]*c[J]%MOD*inv[min(c[J],b[K])]%MOD)%MOD; } printf("%d\n",f[n%3]); return 0; }
|
T2. 整除(division)
- Experience
说起来也只怪自己的数学太菜了……
推出来了前大半的结论,得到了一个非常优秀的 $O(ct)\approx10^6$ 的预处理;结果不知道最后怎么统计答案(其实把那个结论推出来,就可以得到最后的答案就是把我预处理出来的所有答案乘起来),只能把算法复杂度退化到 $O(cn)$ ,相当于一个暴力……
ei……
- Solution
= P1. 第一个结论
显然,对于一个质数 $p$ :
若 $x\equiv r\pmod p$;
则有 $(x+p)\equiv r\pmod p$;
再推广一下,若 $x^m\equiv r\pmod p$;
则有 $(x+p)^m\equiv r\pmod p$;
= P2. 第二个结论
大概是不需要证明的……
= P3. 求解
知道上面两个结论我们能干什么呢?
先分析题目,发现题目其实是要让我们求解同余方程:
由于 $p_1,p_2,…,p_c$ 互不相同,所以可以改为:
即同余方程组的求解,可以用中国剩余定理。
但是还可以简化一下:
对于第一个结论,概括一下就是方程 $x^m\equiv r\pmod p$ 的解以 $p$ 为循环节(即如果 $x$ 是解,则 $(x+p)$ 也是解)。所以我们只要枚举 $x\in[1,p]$ ,判断是否是同余方程的解即可。
因为 $p_1,p_2,…p_c$ 都是互不相同的质数,所以它们两两互质。故设 $x^m\equiv x\pmod p_i$ 在 $[1,p_i]$ 内的解的个数为 $h_i$,则根据第二个结论,可以知道最后的答案即 $h_1h_2…h_c$ 。
处理出 $h_i$ 只需要枚举 $1$ 到 $p_i$ ,暴力判断是否是同余方程的解即可。但是注意要预处理 $x^m\bmod p$ 。
- Source
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| #pragma GCC optimize(2) #include<map> #include<set> #include<queue> #include<stack> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; const int MOD=998244353,T=1e4; int tex,cas,n,m,nprn; int p[55],cnt[55],prn[T+5],inv[T+5]; bool vis[T+5]; int QPow(int A,int B,int C){ int ret=1; while(B){ if(B&1) ret=ret*A%C; A=A*A%C; B>>=1; } return ret; } void Process(){ for(int i=2;i<=T;i==2? 3:i+2){ if(!vis[i]) prn[++nprn]=i; for(int j=1;j<=nprn && 1ll*i*prn[j]<=T;j++){ vis[i*prn[j]]=true; if(i%prn[j]==0) break; } } } int QRead(){ int ret=0;char ch=getchar(); while(ch<'0' || '9'<ch) ch=getchar(); while('0'<=ch && ch<='9') ret=(ret<<1)+(ret<<3)+ch-'0',ch=getchar(); return ret; } int main(){ freopen("division.in","r",stdin); freopen("division.out","w",stdout); Process(); tex=QRead();cas=QRead(); while(cas--){ n=QRead();m=QRead(); int ans=0; for(int i=1;i<=n;i++){ p[i]=QRead(); for(int j=1;j<=nprn && prn[j]<p[i];j++){ inv[prn[j]]=QPow(prn[j],m,p[i]); } inv[1]=1;cnt[i]=2; for(int j=2;j<p[i];j==2? 3:j+2){ if(!vis[j]){ if(inv[j]==j) cnt[i]++; } for(int k=1;prn[k]<=j && k<=nprn && 1ll*j*prn[k]<p[i];k++){ inv[j*prn[k]]=inv[j]*inv[prn[k]]%p[i]; if(inv[j*prn[k]]==j*prn[k]) cnt[i]++; if(j%prn[k]==0) break; } } } ans=cnt[1]; for(int i=2;i<=n;i++) ans=int(1ll*ans*cnt[i]%MOD); printf("%d\n",ans); } return 0; }
|
T3. 欠钱(money)
- Experience
emm……这道题考试的时候看漏了一句话:“每只企鹅只会借一次钱”,错以为这道题是动态维护网络流……
最后回宿舍跟室友们交流才发现,这原来是一棵树 QwQ ,所以就从网络流简化为求链上最小值了~
听神犇讲题、看题解,然后发现,这是一道 LCT 的板子题。(板子又怎样,LCT 我没学啊)
然后 Day4 这天就没改出来这道题,Day5 因为听到讲课要讲 LCT,心血来潮就把 LCT 自学了一下,打了一道版题。于是来看这道题,但还是没有写出来(可能我对 LCT 理解不够深刻)。
最后看了题解,发现另一种解法,才终于 AC 了这道题。
(你们猜我是什么时候改的这道题?)
- Solution
=P1. 一些初始思路
动态倍增!!!
其实这是我考试的时候的想法……但是我不知道怎么更新倍增数组,然后就爆零了 TAT
思路很简单:
- 修改(0操作)就看作连一条从儿子a到父亲b(这里不能换方向)的边;不难发现,只有当 b 是 a 的祖先时,a 获得钱,b 才会获得钱,且数量为 a 到 b 的链上的最小值;
- 询问(1操作)首先判断 b 是否是 a 的祖先,再倍增求 a 到 b 的链上的最小值即可。
=P2. 倍增
根据这个思路,我们可以定义出倍增数组 pre[u][i]
表示 $u$ 的第 $2^i$ 个祖先,mn[u][i]
表示 $u$ 到 pre[u][i]
的链上的最小边权。
当我们连一条从 儿子u 到 父亲v 的边权为 c 的边时,我们需要更新:
这个时候我们不更新其他的倍增数组,因为可能有很多更新的信息用不到,就浪费了时间。当我们要用到 pre[u][i]
或 mn[u][i]
时,我们用函数 Update(u,i)
更新:
1 2 3 4 5 6 7
| void Update(int u,int x){ for(int i=1;i<=x;i++){ Update(pre[u][i-1],i-1); pre[u][i]=pre[pre[u][i-1]][i-1]; mn[u][i]=min(mn[u][i-1],mn[pre[u][i-1]][i-1]); } }
|
但是我们发现,这样还是太浪费了,因为倍增数组只可能扩展出新的值,而不会修改原来的值(即如果我们计算过 pre[u][i]
或 mn[u][i]
,这两个值在之后就不会被修改了)。
我们对于每个 u
,储存 upd[u]
表示我们已经计算过 pre[u][0~upd[u]]
和 mn[u][0~upd[u]]
;于是 Update(u,i)
变为了:
1 2 3 4 5 6 7 8
| void Update(int u,int x){ for(int i=upd[u]+1;i<=x;i++){ Update(pre[u][i-1],i-1); pre[u][i]=pre[pre[u][i-1]][i-1]; mn[u][i]=min(mn[u][i-1],mn[pre[u][i-1]][i-1]); } upd[u]=x; }
|
现在我们解决了动态倍增及其更新的问题了。
=P3. 并查集
但是还有一个问题——维护每个节点的深度(以便求 LCA 并进一步判断出 b 是否是 a 的祖先),于是我们想到可以用并查集!还可以顺带维护每个节点所在树的根节点。
但是需要注意并查集合并时是有顺序的,比如我们连了一条从 儿子u 到 父亲v 的边,就只能在并查集中把 u 合并到 v,道理非常显然(毕竟这是一棵有根树)。
维护深度……只能具体看代码了……(咕)
- Source
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| #include<cstdio> #include<cstring> #include<algorithm> using namespace std;
const int N=1e5;
int n,m,ans; int pre[N+3][20],mn[N+3][20],upd[N+3]; int mfa[N+3],mdep[N+3];
int MFa(int x){ if(mfa[x]==x) return x; int res=MFa(mfa[x]); mdep[x]+=mdep[mfa[x]]; return mfa[x]=res; } void Update(int u,int x){ for(int i=upd[u]+1;i<=x;i++){ Update(pre[u][i-1],i-1); pre[u][i]=pre[pre[u][i-1]][i-1]; mn[u][i]=min(mn[u][i-1],mn[pre[u][i-1]][i-1]); } upd[u]=x; } int LCAMin(int u,int v){ int del=mdep[u]-mdep[v],res=(1<<29); for(int i=19;i>=0;i--) if(del&(1<<i)) del^=(1<<i), Update(u,i), res=min(res,mn[u][i]), u=pre[u][i]; if(u==v) return res; return 0; } int main(){ freopen("money.in","r",stdin); freopen("money.out","w",stdout); scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) mfa[i]=i; for(int h=1;h<=m;h++){ int tmp,u,v; scanf("%d%d%d",&tmp,&u,&v); u=(u+ans)%n+1;v=(v+ans)%n+1; if(tmp){ if(MFa(u)!=MFa(v) || mdep[u]<=mdep[v]) ans=0; else ans=LCAMin(u,v); printf("%d\n",ans); } else{ int val;scanf("%d",&val); val=(val+ans)%n+1; pre[u][0]=v;mn[u][0]=val; mfa[u]=v;mdep[u]=1; } } return 0; }
|
The End
Thanks for reading!
Email: lucky_glass@foxmail.com ,欢迎提问!