寒假模你赛题解
1.13
平凡的函数
发现长得很像欧拉筛,我们考虑怎么筛
在我们第一次找到某个质数p时显然有 \(F[p]= p \otimes 1\)
在筛的过程中考虑如何计算\(F[i*prime[j]]\)的值
因为有
即互质积性
那么当\(prime[j] \nmid i\)时直接相乘更新
当$prime[j] \mid i \(时,\)prime[j]\(是\)i\(的最小质因数,设将\)i\(的\)prime[j]\(因数全部拿掉后为\)a\(,求出\)i\(的质因数分解下,\)prime[j]\(的指数再加上一为\)b$,
那么此时\(a\)与\(prime[j]\)显然是互质的,可以积性求了,即
考场上卡了一会儿
code
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#define ll long long
using namespace std;
const int maxn=5e7+10,maxm=6e6+10;
bool vis[maxn];
int prime[maxm],cnt;
int cs[maxn];
int n;
ll ans;
void get(int n){
ans=cs[1]=1;
for(int i=2;i<=n;i++){
if(!vis[i]){
prime[++cnt]=i;
cs[i]=i^1;
}
for(int j=1;j<=cnt && i*prime[j]<=n;j++){
vis[i*prime[j]]=true;
if(i%prime[j]==0){
int res=i,bit=1;
while(res%prime[j]==0){res/=prime[j];bit++;}
cs[i*prime[j]]=(prime[j]^bit)*cs[res];
break;
}else cs[i*prime[j]]=cs[i]*cs[prime[j]];
}
ans+=cs[i];
}
}
int main (){
freopen("func.in","r",stdin);
freopen("func.out","w",stdout);
scanf("%d",&n);
get(n);
printf("%lld\n",ans);
}
那一天她离我而去
考场上没什么思路确实,只有极其暴力的想法,题解里提到的暴力也没想到.考场上判了个一号点所在的环,在环上暴力dfs骗了点分
正解:
首先有一个暴力的想法
我们发现这条回路可以分为三部分
1.从1到达x点的距离
2.从x点到达y点的距离
3.从y点回到x点的距离
当选出一组\(x\)和\(y\)时,1和3就是确定的,我们要求的就是\(x\)和\(y\)之间的最短路
暴力跑的话要算\(n\)次最短路,我们考虑如何优化这个算法
首先剥离出一个点集为直接与一号点相连的点,一个边集为与一号点相连的边
我们暴力的思路实际上就是每次选出一组起点和终点求最短路,那如果我们一次求多组呢?
形象化的来说,我们每次从点集中选一些点作为\(x\)点,一些点作为\(y\)点,我们将边集中\(1\)连向\(x\)点的边加入图中(单向),再建立一个超级汇点,将\(y\)点连向\(1\)的边接到超级远点上,再以\(1\)为原点就最短路,实际上我们就是求出来从\(1\)经过某\(x\)点再经过某\(y\)点回到一的最短路,也就是题目要求的最小环.
暴力中我们要选出任意两点作为起点和终点才能求出最终答案,那么我们如何分组才能同时保证时间复杂度和正确性呢?
就挺神的,我们考虑每个点编号的二进制表示,任意两点的编号不同,二进制下任意两点编号至少有一位不同,那我们枚举二进制下的每一位,为\(1\)的分一组,为\(0\)的分一组,这样就保证了任意两点至少在某一次被分为了起点和终点,我们也只需要跑\(log(n)\)次最短路
分组实在是太巧妙了
点击查看代码
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
const int maxn = 100005;
struct edge{
int to, next, dis;
}e[maxn*2];
struct node{
int dis,pt;
node(){}
node(int a,int b):dis(a),pt(b){}
bool operator<(const node &rhs)const{
return dis>rhs.dis;
}
};
int head[maxn*2], len;
void lqx(int from, int to, int dis){
e[++len].to = to;
e[len].next = head[from];
e[len].dis = dis;
head[from] = len;
}
struct nodes{
int to, val;
}a[maxn];
queue<int> q;
bool vis[maxn];
int dis[maxn];
void dij(int u){
memset(dis,0x3f,sizeof(dis));
priority_queue < node >q;
memset(vis,0,sizeof(vis));
dis[u]=0;
q.push(node(0,u));
while(!q.empty()){
u=q.top().pt;
q.pop();
if(vis[u])continue;
vis[u]=true;
for(int i=head[u];i;i=e[i].next){
int v=e[i].to,d=e[i].dis;
if(dis[v]>dis[u]+d){
dis[v]=dis[u]+d;
//printf("%d %d %d %d\n",v,dis[v],u,dis[u]+d);
q.push(node(dis[v],v));
}
}
}
}
int cnt;
int tmp[maxn*2];
int T,n,m;
void solve(){
memset(head, 0, sizeof head);
memset(vis, 0, sizeof vis);
memset(dis, 0x3f, sizeof dis);
len=cnt=0;
scanf("%d%d",&n,&m);
for(int i = 1; i <= m; i++){
int u,v,w;
scanf("%d%d%d",&u,&v,&w);
if(u > v) swap(u, v);
if(u == 1) a[++cnt].to = v, a[cnt].val = w;
else lqx(u, v, w), lqx(v, u, w);
}
int ans = 0x3f3f3f3f;
memcpy(tmp, head, sizeof head);
for(int i = 0; (1 << i) <= cnt; i++){
memcpy(head, tmp, sizeof tmp);
for(int j = 1; j <= cnt; j++){
if(j & (1 << i)) lqx(1, a[j].to, a[j].val);
else lqx(a[j].to, n + 1, a[j].val);
}
dij(1);
ans = min(ans, dis[n + 1]);
}
if(ans == 0x3f3f3f3f) ans = -1;
printf("%d\n", ans);
}
int main(){
freopen("leave.in", "r", stdin);
freopen("leave.out", "w", stdout);
scanf("%d",&T);
while(T--)solve();
return 0;
}
熟练剖分
期望神题,半个小时题没读懂样例没明白
我们定义\(F[i][j]\)表示节点\(i\)的子树中,最坏时间复杂度不超过j的概率
考虑如何转移
先不转移了
1.15
匹配
暴力\(KMP\)即可
但是注意到\(KMP\)求出的最长boader是不考虑自己是自己的boader的
所以要特判!!!!!!!!!!!
如果加入的字符和原位置一样直接输出答案即可
所以特判有八十分
所以垃圾数据毁我青春
回家
看到这个题第一反应就是找个割点就了了
但是眉头一皱发现不对劲
怎么可能这么水
我们发现必经点一定是割点(这很显然),但是割点不一定是必经点
因为割点虽然会把图分为两个连通块,但是如果\(1\)和\(n\)仍在一个连通块呢
还得求割点,但是在求割点的过程中,我们判断一下某个割点的儿子是否能搜到\(n\)点,如果可以那么这个割点属于答案
寿司
考虑断环成链
我们发现合法解中,一定有一个边界,这个边界的左右不发生交换,这也就是断环成链,选取的左右边界就是这个边界
同时发现我们只需要考虑一种颜色的移动,另一种颜色会同时完成的.
那么就考虑\(R\)的移动步数
现在\([1,2*n]\)中选出断环成链的左右边界\(L,R\),我们只需要将\(R\)全都移到左右边界就行
发现存在一个分界点,这个分界点左侧的\(R\)都向左移动,右侧都向右移动,并且随着\(L,R\)向右移动,这个分界点也只向右移动
那么我们就可以预处理出这个分界点的一些值,进行求答,考虑每次移到\(L\)和\(R\)的开销即可
时间复杂度\(O(n)\),常数略大
也可以考虑把所有\(R\)都移到中间(就相当于把所有\(B\)移到两边)
合理的预处理可以减小常数(大概就是\(1000ms\)与\(2000ms\)的差距)
1.16
斐波那契
原题
某个数的父亲就是他减去离他最近的斐波那契数,二分暴力跳父亲即可
数颜色
原题
考虑到只交换相邻两个兔子,而且兔子除了颜色没有其他区别,也就是交换两个颜色相同的兔子相当于没交换,同种颜色的兔子的相对位置不变
那么我们就可以考虑用vector保存每种颜色的下标集合,每次在vector里二分查找修改即可
复杂度\(O(nlogn)\)
注意有些颜色可能是不存在的,特判一下,要不然不知道二分出来什么鬼东西,奇奇怪怪的RE
分组
\(K=1\) 时暴力判合法即可
\(K=2\) 时用扩展域并查集维护关系,同时需要特判 \(2*a[i]=x^2\)的情况
1.16夜间版
中国象棋
\(50%\)的数据可以三进制状态压缩求
然后我们考虑如何优化这个状压
发现行与行之间,列与列之间时不相互影响的,并且在转移过程中,一些转移本质上,数值上也是相同的
按套路我们依旧状压一行一行的放,显然一行只能放小于等于\(2\)个炮,炮可以放在还没有放炮的某列或者之前只放了一个炮的某列,在同一性质的列上,我们放在他们中的哪一个都是一样的,直接组合意义计算
那么就比较清晰了
我们定义\(F[i][j][k]\)为当前放完了第\(i\)行,有\(j\)列没放跑,有\(k\)列只放了一个炮的方案数
枚举放置的情况转移即可
记得限制边界
奇妙的Fibonacci
太奇妙了
这又是个欧拉筛
通过大量打表发现
当\(F_j \mid F_i\) 有\(j \mid i\)
1.18
入阵曲
丹青千秋酿,一醉解愁肠。
无悔少年枉,只愿壮志狂。
有显然的\(n^4\)做法,直接二维前缀和再枚举子矩阵
我们考虑如何优化这个过程
发现我们求某个矩阵的和时,是用一个大矩阵减去几个小矩阵,试图变换柿子突破
假如只有矩阵只有一行,那么我们\(n^2\)枚举矩阵的左右边界\(L,R\)
\(ans+= (pre[R]-pre[L-1] \% K==0)\)
也就是有 \(K \mid (pre[R]-pre[L-1])\)
这不是同余的充要条件吗
我们考虑扩展到多行,实际上我们就可以把多行视为一行,每次枚举当前要考虑多少行,行的上下边界在哪,再把列扫一遍,记录前边的矩阵模K余数即可
相同余数的个数就是贡献
可以做到\(O(n^3)\)
将军令
历史/落在/赢家/之手
至少/我们/拥有/传说
谁说/败者/无法/不朽
拳头/只能/让人/低头
念头/却能/让人/抬头
抬头/去看/去爱/去追
你心中的梦
.
又想起了四月。
如果不是省选,大家大概不会这么轻易地分道扬镳吧?
只见一个又一个昔日的队友离开了机房。
凭君莫话封侯事,一将功成万骨枯。
发现我们在任何一个点放的开销时完全相同的,那么我们可以试图贪心让每个开销的贡献更大
显然我在某个更靠近中央的点放小队,他能扩散的点比边缘点更多,但是我又必须扩散到边缘点,那么我每次都考虑放在一个尽量靠近中央的,且能控制到当前离根最远的点 的店
也就是放在当前深度最大的点的\(K\)级祖先,再从这个点向外暴力扩展\(K\)层打标记即可
显然第一眼看题就觉得是像套路的树形dp,但是细想好像不太好写
然后就思考如何偏分
星空
命运偷走如果只留下结果,
时间偷走初衷只留下了苦衷。
你来过, 然后你走后, 只留下星空。
.
逃不掉的那一天还是来了,小 F 看着夜空发呆。
天上空荡荡的,没有一颗星星?D?D大概是因为天上吹不散的乌云吧。
心里吹不散的乌云,就让它在那里吧,反正也没有机会去改变什么了。
我们发现这题长得有点像分手,如何我们就考虑如何优雅分手
当\(m=1\)的时就是分手,直接\(O(n)\)扫一遍,暴力反转就行
如何我们开始审视测试点
发现我们可以大力骗分
要求最小步数,第一发写的二分check,二分确实没太大问题但是我写挂了,就思考如何优雅bdfs
那么就迭代加深,每次限定最小答案,接着分手,只不过要多枚举一下用哪个长度的操作点亮当前点,暴力剪枝搜
取得了72分好成绩
好了开正解
挺离谱的想不到
我们思考如何转化这个问题,发现我们用某一个长度操作摁两次,两次区间差一,可以做到将某一段的两端翻转
好像没用?
考虑差分,对一段的反转操作就是反转两个边界,
不会写了
1.19 理科综合
math
想不到什么方向只能骗分
考虑一个数的若干倍模k可以出现若干个余数,我们把这些余数都求出来,再加上之前的
1.24 HNOI (雾
队长快跑
显然一眼DP,我们如何设计状态和转移
发现某一个水晶是否能摧毁,与当前已经摧毁的\(A_i\)最小值有关,也与他的\(B\)值有关
我们设\(F[i][j]\)为前\(i\)个水晶,摧毁的水晶最小值为\(j\)时,能够摧毁的最大数量
考虑暴力转移,
当\(A_i \leq B_i\)时,为使得能够摧毁,之前摧毁的最小值取值区间为\([B_i+1,MAX]\),在前\(i-1\)中找最大的转移
当\(A_i > B_i\)时,最小值取值区间为\([B_i+1,MAX]\),有两种转移
暴力枚举为\(O(n^3)\),转移时继承上一个位置可以做到\(O(n^2)\)
但是我们发现转移就是求一个之前的最大值,再对某一个区间进行区间加,所以我们可以把第二维丢到线段树上,第一维丢掉,从头到尾一棵线段树即可,就相当于继承上一阶段的状态
注意转移的先后顺序
影魔
一个神奇的转化
如果不考虑深度的限制,那么就是求某棵子树内颜色的个数
第一反应是线段树合并,但是好像并不太ok太ex了
然后就伪了一个树上莫队,然后就拿到了和dfs一样的分数
下面说正解
主席树
不考虑深度限制,每个点能够产生贡献在它到根的路径上,在树上做类似差分的做法
u+1;
lca(pre,u)-1
lca(u,suf)-1
lca(pre,suf)+1
pre和suf为u点在相同颜色dfn序上的前驱和后继
容斥以消除重复贡献,将问题转化为求子树和,p
由于有深度限制,那么按深度建主席树,限制深度求即可
本题不强制在线,所以还有一种线段树合并加树状数组的离线做法,但是没写也没胡
抛硬币
可能是比较水的题
设\(F[i][j]\)为前i位长度为j的本质不同子序列个数
考虑每个字符新加进串时产生的贡献
为\(F[i-1][j-1]\),同时它还可以直接继承之前的串\(F[i-1][j]\)
本质不同也就是子序列至少有一位不同,考虑新加进来的这个字符的重复贡献,假设之前没有出现过这个字符,那么它新产生的贡献一定不重,如果之前已经出现过,记上一次出现的位置为\(last\),那么会重复产生\(F[last-1][j-1]\)的贡献,我们将这部分减去即可
1.25
Reverse
这很像星空的一个子问题,但是星空那题的步长只有64种,而本题的步长可以有\(n\)种,所以直接暴力bfs的做法显然会炸,我们考虑如何优化这个算法
可以线段树优化建图但是不会写也不会调,我们尝试换一种方法乱搞
发现在bfs过程中,每个状态第一次抵达的步数就是最小值,那么我们之后再枚举更新它是无意义的,我们也只需要从他出发更新其他状态一次.
两种做法,枚举过程中,我们是两个连个跳的,所以我们可以对可达点按奇偶分组,分别放在两个set里,每次从set里lower_bound当前点能够翻到的区间[l,r],找到一个更新一个,erase一个,时间复杂度为\(O(nlogn)\)
另一种做法是维护链表,复杂度为\(O(n)\)但是不如set省事
silhouette
大概就是组合数学容斥二项式反演,然而数学都不会暂时先鸽了
Seat
神仙概率DP
模拟一下选座过程,我们可以发现第\(i\)个人选座时题目定义的距离时一定的,不管前面的人怎么坐,同时选了一些人后的区间局面本质也相同
我们考虑按照题目中定义的距离分层处理,我们只需要知道每个人有多少概率坐在一个奇区间/偶区间,考虑DP
设\(dp[i][j]\)表示这一层已经做下\(i
\)个人后,还有\(j\)个偶区间的概率,转移当前人坐在了那种区间,转移后算出这个人的答案
通过注释std的方式具体讲
//注意
//std中odd为偶数,even为奇数
//接下来所说的最小距离为题目中定义的距离
#include<bits/stdc++.h>
using namespace std;
const int N=1030;
int n,mod;
int qpow(int x,int k,int ans=1){
while(k){
if(k&1) ans=ans*x%mod;
x=x*x%mod;
k>>=1;
}
return ans;
}
//dp为最终答案矩阵
//f为题解中定义的dp数组(已经坐了i人,还剩j个偶区间),g为一个中间临时数组
//cnt[i]为最小距离为i的人的数量,也就是对距离分层
//odd[i]为最小深度为i的偶区间的数量
//pos[i]为第i个人坐的位置
int dp[N][N],f[N][N],g[N][N],vis[N],inv[N],cnt[N],odd[N],pos[N];
int main(){
scanf("%d%d",&n,&mod);
for(int i=1;i<=n;++i) inv[i]=qpow(i,mod-2);
vis[0]=vis[n+1]=true;
for(int i=1;i<=n;++i){
int pl=0,pr=0,mx;
for(int j=0;j<=n;++j){
int r=j+1;
while(!vis[r]) ++r;
if(r-j>pr-pl) pl=j,pr=r;
j=r-1;
}
++cnt[mx=pr-pl>>1]; odd[mx]+=pr-pl&1;
pos[i]=pl+mx; vis[pl+mx]=true;
}
/*------------
本段预处理可以理解为找出一种可行的座位安排,同时求得各层的人数,偶区间数量
---------------*/
int sum=n;//sum是当前还没有坐的人数
for(int i=1;i<=n;++i){//最小距离从小到大枚举,也就是逆推
if(!cnt[i]) continue;
int l=sum-cnt[i]+1,r=sum;//最小距离为i的人是我们预处理的一组解中的[l,r]
if(i==1) for(int j=l;j<=r;++j) for(int k=l;k<=r;++k) dp[j][pos[k]]
=inv[cnt[i]];
//显然最小距离为一时,剩下的人到每个位置的概率时相同的,直接均分概率
else{
for(int j=0;j<=cnt[i];++j) for(int k=0;k<=odd[i];++k) f[j][k]=0;//清空数组
f[0][odd[i]]=1; //还没有坐人,那么该层剩下odd[i]个偶数区间的概率为1
int p=l+odd[i]-1;
//偶数区间长度的最后一个人(接下来会用到),为什么?
//预处理时,对于某一个mx,它对应的偶区间的(pr-pl)显然要大于奇区间
//也就是说这一层的前若干人都在偶区间,剩下的在奇区间
for(int j=1;j<=cnt[i];++j){//当前坐了几个人
int oddw=0,evenw=0;//odd为偶数 even为奇数
for(int k=0;k<=odd[i];++k){//还剩多少偶区间
if(!f[j-1][k]) continue;
//dp转移 所以f值为0的时候可以剪枝 k表示剩余多少个偶数区间
int frac=(cnt[i]-(j-1))+k,w=0;
//括号内表示剩余的区间个数 +k表示剩余多少个转移点
if(k){//还有偶区间剩余
w=f[j-1][k]*k*2%mod*inv[frac]%mod;
//占一个偶区间位置 那么概率为k*2/转移点
oddw=(oddw+w*inv[odd[i]*2])%mod;
//方便累加答案 对于这一次的转移 可能作用在不同的转移点
(f[j][k-1]+=w)%=mod;
}
if(cnt[i]-odd[i]){
//可以向奇区间转移
//这个判断条件也可以写为判断剩下没有奇区间
//还可以不写
w=f[j-1][k]*(frac-2*k)%mod*inv[frac]%mod;
//向奇数区间转移的概率
evenw=(evenw+w*inv[(cnt[i]+odd[i])-odd[i]*2])%mod;//向不同奇数区间转移
(f[j][k]+=w)%=mod;
}
}
for(int u=l;u<=p;++u) (dp[l+j-1][pos[u]]+=oddw)%=mod,(dp[l+j-1][pos[u]+1]+=oddw)%=mod;
for(int u=p+1;u<=r;++u) (dp[l+j-1][pos[u]]+=evenw)%=mod;
//累加答案,前面已经说明过p的意义
}
for(int j=l;j<=p;++j){//坐在偶区间的人
int L=pos[j]-i+1,R=pos[j]+i;//当前的偶区间左右端点
for(int v=L;v<=R;++v){
if(v==pos[j]) continue;//不能是选择的点
for(int u=r+1;u<=n;++u){//后面每一个人
int s=v<pos[j]?v+i+1:v-i,w=dp[u][v]*inv[2]%mod;
//后一个人在位置v的概率除2
(g[u][v]+=w)%=mod; (g[u][s]+=w)%=mod;
//这实际上就是题解中提到的利用对称性推答案
}
}
for(int v=L;v<=R;++v) for(int u=r+1;u<=n;++u) dp[u][v]=g[u][v],g[u][v]=0;
}
}
sum-=cnt[i];//考虑下一层 剩余人数减少
}
for(int i=1;i<=n;i++,puts("")) for(int j=1;j<=n;j++) printf("%d ",dp[i][j]);
return 0;
}