寒假模你赛题解

寒假模你赛题解


1.13

平凡的函数

发现长得很像欧拉筛,我们考虑怎么筛

在我们第一次找到某个质数p时显然有 \(F[p]= p \otimes 1\)

在筛的过程中考虑如何计算\(F[i*prime[j]]\)的值

因为有

\[F(a*b)=F(a)*F(b),(a与b互质) \]

即互质积性

那么当\(prime[j] \nmid i\)时直接相乘更新

当$prime[j] \mid i \(时,\)prime[j]\(是\)i\(的最小质因数,设将\)i\(的\)prime[j]\(因数全部拿掉后为\)a\(,求出\)i\(的质因数分解下,\)prime[j]\(的指数再加上一为\)b$,

那么此时\(a\)\(prime[j]\)显然是互质的,可以积性求了,即

\[F[i*prime[j]]=F[a]*(p \otimes b) \]

考场上卡了一会儿

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]\),有两种转移

\[F[i][A_i]=max\{F[i-1][j]\}+1,A_i \leq j \leq MAX \]

\[F[i][j]=F[i-1][j]+1,B_i<j<A_i \]

暴力枚举为\(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;
}
posted @ 2022-01-24 21:21  Delov  阅读(55)  评论(0编辑  收藏  举报