10-2国庆节第五场模拟赛题解

T1 seq:

序列2 (seq)

Description

给定个长度为 n 的数列 {a},初始时数列中每个元素 a_i 都不大于 40。你可以在其上进行若干次操作。在一次操作中,你会选出相邻且相等的两个元素,并把他们合并成一个元素,新的元素值为 \((旧元素值+1)\)

请你找出,怎样的一系列操作可以让数列中的最大值变得尽可能地大?这个最大值是多少?

Input

输入文件第一行一个正整数 n,表示数列的长度。

接下来一行 n 个不大于 40 的正整数,表示这个数列 {a }。

Output

输出一个整数,表示经任意次合法操作后所有可能得到的数列中的最大值。

T1是一道DP,洛谷P3147,原题链接https://www.luogu.org/problemnew/show/P3147

考虑怎么设状态,很奇怪的是题目里面给出了初始数值全都不大于40,由数据范围可以知道,最后的答案ans不会超过max+logn,也就是近似最大值为58,那么让我们突破一下思维,将所谓的答案设成状态,也就是设\(f[i][j]\)表示从第i个位置出发,到第\(f[i][j]\)这个位置可以合并出j这个答案。

初始化就是\(f[i][a[i]]\)=i;

得到状态转移方程\(f[i][j]=f[i][f[i][j-1]][j-1]\)

很简单吧,还有一个要注意的地方,可以注意到每一次都是j由j-1推出,所以要先枚举j。

虽然不甚理解eolv的意思,但这应该是一个DP的注意的地方,还是要记住的。

#include<iostream>
#include<cstdio>
using namespace std;
inline int read(){
	int sum=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		sum=(sum<<1)+(sum<<3)+ch-'0';
		ch=getchar();
	}
	return sum*f;
}
const int wx=262144;
int f[wx][59];
int n,ans;
int x;

int main(){
	freopen("seq.in","r",stdin);
	freopen("seq.out","w",stdout);
	n=read();
	for(int i=1;i<=n;i++){
		x=read();
		f[i][x]=i+1;
	}
	for(int j=2;j<=58;j++){
		for(int i=1;i<=n;i++){
			if(!f[i][j])f[i][j]=f[f[i][j-1]][j-1];
			if(f[i][j])ans=max(ans,j);
		}
	}
	printf("%d\n",ans);
	fclose(stdin);
	fclose(stdout);
	return 0;
}

T2: sum

打表好题啊,eolv大佬讲这道题的时候总结了一下规律:

在关于数学的打表题中:首先应该想到几个重要的函数:
1)d(n)表示n的约束个数。

2)phi(n)表示小于等于n的与n互质的数的个数。(欧拉函数)

3)u(n)表示n的因子分布情况。(莫比乌斯函数)

4)id(n)即表示n本身。(废话啊喂,打表当然要输出n了啊)。。

考场上这道题找规律找了一个多小时(太菜了。。。),最后实在找不出针对10000次询问1e9的数据范围的O(1)做法,忐忐忑忑打了个70分做法,没想到居然A了,感谢数据感谢数据。

(OIES大法好啊!!!)

eolv讲过之后忽然感觉这题还是挺简单的,挺好证,那就再来证一遍吧。

对于f(n)

\(f(n)=n^2-\sum_{a=1,a<=n}\sum_{b=1,b<=n}[n|ab]\)

​ =\(n^2-\sum_{a=1,a<=n}\sum_{b=1,b<=n}[n/gcd(a,n)|a*b/gcd(a,n)]\)

​ =\(n^2-\sum_{a=1,a<=n}\sum_{b=1,b<=n}[n/gcd(a,n)|b]\)//因为在这时n/gcd(a,n)与a/gcd(a,n)是互质的,所以 a/gcd(a,n)对答案是没有贡献的

​ =\(n^2-\sum_{a=1,a<=n}gcd(a,n)\)//这是比较难理解的一步,考虑b的取值范围是1到n所以上式的意思就是有多少个数属于1到n并且整除n/gcd(a,n),那么自然就是n/n/gcd(a,n)=gcd(a,n)了

​ =\(n^2-\sum_{d|n}phi(n/d)*d\)//数论证明中非常常见的一步,枚举因子

对于g(n),我们将f(d)带入

\(g(n)=\sum_{d|n}f(d)\)

​ =\(\sum_{d|n}d^2-\sum_{d|n}\sum_{p|d}phi(d/p)*p\)

​ =\(\sum_{d|n}d^2-\sum_{d|n}(phi(d)*id(d))\)//后一个\(\sum\)可以看成是phi函数和id函数的狄利克雷卷积

​ =\(\sum_{d|n}d^2-\sum_{d|n}1(n/d)*((phi(d)*id(d)))\)

​ =\(\sum_{d|n}d^2-1(n)*phi(n)*id(n)\)

​ =\(\sum_{d|n}d^2-id(n)*id(n)\)

​ =\(\sum_{d|n}d^2-\sum_{d|n}n*(n/d)\)

​ =\(\sum_{d|n}d^2-\sum_{d|n}n\)

很容易得出答案就是n的因数的平方和减去n*n的约数个数。

#include<iostream>
#include<cstdio>
#define ll long long
using namespace std;
ll g[2005];
int t;
ll n,ans;
ll work(ll now){
	ll re=0;//记录因子的平方和 
	ll tmp=0;//记录因子个数 
	for(int i=1;i*i<=now;i++){
		if((now%i==0)&&(i*i!=now)){
			re+=i*i;re+=(now/i)*(now/i);
			tmp+=2;
		}
		else if(i*i==now){
			re+=i*i;
			tmp++;
		}
	}
	return re-now*tmp;
}
int main(){
	freopen("sum.in","r",stdin);
	freopen("sum.out","w",stdout);
	scanf("%d",&t);
	while(t--){
		scanf("%lld",&n);
		ans=work(n);
		printf("%lld\n",ans);
	}
	fclose(stdin);
	fclose(stdout);
	return 0;
}

T3:frame

Description

你想送给 Makik 一个情人节礼物,但是手中只有一块方格纸。这张方格纸可以看作是一个由 H 行 W 列格子组成的长方形,不幸的是上面甚至还有一些格子已经损坏了。为了让这张破破烂烂的方格纸变得像个礼物的样子,你要从中剪出一个边长不小于 L 的方框,并且损坏的格子都不能被包含在这个方框中。这里,一个边长为 s(s≥3) 的方框指的是大小为 s×s 的正方形最外层的 4(s-1)) 个格子所构成的形状。

在动手剪方格纸之前,请你算一算,一共有可能剪出多少种不同的方框?

Input

输入文件第一行包含四个整数 H,W,L,P分别表示格子的总行数、列数、方框边长的最小限制和损坏格子的数量。

接下来 P 行,其中第 i 行包含两个整数 x_i, y_i,表示方格纸上第 x_i 行第 y_i 列处的格子已经损坏。

Output

输出一行一个整数,表示剪出不同的方框的方案数。

数据规模与约定

对于 20%的数据,H \(\leq\) 500, W$\leq$500

对于另 20% 的数据,P = 0。

对于另 20% 的数据,1 \(\leq\) P\(\leq\) 10。

对于 100%的数据,1 \(\leq\) H$\leq$4000, 1 \(\leq\) W \(\leq\) 4000, 3 \(\leq\) L \(\leq\) min{W, H}, 0 \(\leq\) P \(\leq\) 100000 1≤H≤4000,1≤W≤4000,3≤L≤min{W,H},0≤P≤100000。

其中很简单的是P=0的部分分,直接输出不解释

\[\sum_{i=l}^{i<=min(n,m)}(n-i+1)*(m-i+1) \]

还有前面数据范围比较小的20分直接暴力,我写的\(n^4\),大佬们都用前缀和优化到了\(n^3\),好吧,比他们低十分不亏。

下面来讲一下正解吧,鸣谢GMPotlc大佬,细心的讲解。

因为题目中问的是正方形的个数,那么考虑正方形的性质,很容易想到正方形的左上角的点和右下角的点是在同一条主对角线上的,那么就可以将主对角线当做我们的中间变量去枚举,然后找到每条主对角线上有多少合法的正方形,总的取一个和就行。

枚举对角线的操作:

	for(int x=n,y=1;y<=m;x==1?++y:--x){//可以说是非常秀了
		int len=0;
		memset(sum,0,sizeof sum);
		memset(edge,0,sizeof edge);
		memset(head,0,sizeof head);//每次拉出一条主对角线都是一次新的区间XJB操作,所以每次都要初始化
		num=0;
		for(int i=x,j=y;i<=n&&j<=m;i++,j++)len++;//当前是拉出一条主对角线
        。。。
        。。。
    }

那么考虑怎么统计每条对角线上合法的正方形,暴力枚举是一定不可以的,一定会T的。

又因为是求和,考虑用我们熟悉的数据结构来维护,又因为对于每个点,让它作为一个正方形的左上角点,那么对应合法的右下角点是在一个范围内的,所以很容易想到用树状数组维护和,因为要差分一下。

考虑当前枚举到这条对角线的第j个点,主要是三个操作:

一、我们枚举到的这个点,可以是作为一个正方形的右下角位置的,所以要在这里统计一下答案。

1)首先我们要更新这个点作为右下角会有多少个左上角与之对应,那么我们就需要提前记录一下这个点作为右下角合法的左上角。又因为左上角一定是先被枚举到的,所以我们在枚举到的点作为左上角操作时,就可以找到它所对应的合法右下角的区间,通过差分将这个区间进行修改。不过这里是需要延迟标记的,也就是只有当我们枚举到它对应的右下角开始的位置时,这个点才有答案,所以这时才更新。

可能上述的话不够清晰,但是还是要尽量理解。

具体的操作就是我们提前记录这个点作为右下角对应的左上角的点,即从右下角连向一个左上角,当没见到右下角时,返回去从左上角的位置开始加1。

又因为这个点无论是作为左上角还是右下角,都会有一个对应的合法区间,所以完全可以通过树状数组差分来进行区间修改。

for(int j=head[i];j;j=edge[j].nxt){
				int v=edge[j].to;
				update(v,edge[j].dis);
			}

2)已经更新了当前的树状数组前缀和,那么我们有知道当前这个点作为右下角对应的左上角合法的区间,所以直接区间查询即可。

if(f2[i+x-1][i+y-1] >= l)ans+=query(i-l+1)-query(i-f2[i+x-1][i+y-1]);

3)当前枚举到的点当然不能只作为右下角进行操作。那么现在将它作为左上角应该做什么呢?

前面在1)操作是已经提到了,我们需要处理出当前点作为左上角对应合法的右下角区间,标记好这个区间的开头和结尾,那么但我们枚举到这两个位置时,就可以返回去更改这个点对答案的贡献,也就是一次差分。

所以找到这两个点,通过从这两个点想当前点建边完成信息的处理(也可以成为标记的实现)。

eolv大佬把这个成为在每个点维护一个链表,不过经过GMPotlc大佬的教诲,我在这个部分是通过链式前向星来实现的。

上面的难点讲完,就还剩下一个细节,我们需要提前预处理出每个点最多可以向左上或者向右下最多延伸的长度。那么每一个点在这个对角线上所对应的另一个点的合法区间就有了,一端是由题中给出的l决定,一端就由刚刚所说的极限长度决定。

那就还要判断一下每个点对应的区间的合法性。

也就是如果一个区间的左端点大于等于右端点了,那么肯定是不行的。

code:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int wx=4001;
inline int read(){
	int sum=0,f=1;
	char ch=getchar();
	while(ch<'0'||ch>'9'){
		if(ch=='-')f=-1;
		ch=getchar();
	}
	while(ch>='0'&&ch<='9'){
		sum=(sum<<1)+(sum<<3)+ch-'0';
		ch=getchar();
	}
	return sum*f;
}
struct node{
	int x,y;
}a[wx];
struct e{
	int nxt,to,dis;
}edge[wx*2];
long long ans;
int head[wx],sum[wx],vis[wx][wx],Left[wx][wx],Right[wx][wx],up[wx][wx],down[wx][wx];
int f1[wx][wx],f2[wx][wx];
int n,m,p,l,x,y,z,num,tot;
void add(int from,int to,int dis){
	edge[++num].nxt=head[from];
	edge[num].to=to;
	edge[num].dis=dis;
	head[from]=num;
}
void update(int pos,ll k){
	for(int i=pos;i<=n;i+=(i&-i)){
		sum[i]+=k;
	}
}
ll query(int x){
	ll re=0;
	for(int i=x;i>=1;i-=(i&-i)){
		re+=sum[i];
	}
	return re;
}
void pre(){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(!vis[i][j]){
				Left[i][j]=Left[i][j-1]+1;
				up[i][j]=up[i-1][j]+1;
			}
		}
	}
	for(int i=n;i>=1;i--){
		for(int j=m;j>=1;j--){
			if(!vis[i][j]){
				Right[i][j]=Right[i][j+1]+1;
				down[i][j]=down[i+1][j]+1;
			}
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			f1[i][j]=min(Right[i][j],down[i][j]);//能够向右下方扩展的长度 
			f2[i][j]=min(Left[i][j],up[i][j]);//能够向左上方扩展的长度 
		}
	}
}
int main(){
	freopen("frame.in","r",stdin);
	freopen("frame.out","w",stdout);
	n=read();m=read();l=read();p=read();
	for(int i=1;i<=p;i++){
		x=read();y=read();vis[x][y]=1;
	}
	pre();
	for(int x=n,y=1;y<=m;x==1?++y:--x){
		int len=0;
		memset(sum,0,sizeof sum);
		memset(edge,0,sizeof edge);
		memset(head,0,sizeof head);
		num=0;
		for(int i=x,j=y;i<=n&&j<=m;i++,j++)len++;//当前是拉出一条主对角线 
		for(int i=1;i<=len;i++){
			for(int j=head[i];j;j=edge[j].nxt){
				int v=edge[j].to;
				update(v,edge[j].dis);
			}
			if(f2[i+x-1][i+y-1] >= l)ans+=query(i-l+1)-query(i-f2[i+x-1][i+y-1]);
			if(f1[i+x-1][i+y-1] >= l)add(i+l-1,i,1);
			if(f1[i+x-1][i+y-1] >= l)add(i+f1[i+x-1][i+y-1],i,-1);
		}
	} 
	printf("%lld\n",ans);
	return 0;
}

具体细节还是不少的,要好好理解。

posted @ 2018-10-03 20:35  _王小呆  阅读(245)  评论(0编辑  收藏  举报