noip模拟12[简单的区间·简单的玄学·简单的填数]

noip模拟12 solutions

这次考试靠的还是比较好的,但是还是有不好的地方,

为啥嘞??因为我觉得我排列组合好像白学了诶,文化课都忘记了

    正难则反!!!!!!!!

害没关系啦,一共拿到了\(120pts\),其实距离我的理想分数还差那么\(100pts\)

具体是这样的,第一题AC,第二题10,第三题10

下次要把知识都回忆一下,比如这次用到的欧拉定理,差一点就忘记了

noip模拟13!!!200分!!

·

·

·

T1 简单的区间

哈哈哈这个题是我这几次考试中最成功的一道了,所以我一定要好好讲一讲我的STL算法

(因为正解好像是主席树 逃)(我现在对这树那树的非常恶心)

首先我第一眼看到这个题面的时候,我直接就想到了昨天\(noip11\)那场的\(T3\)

同样是求有关于最大值的结果,还是在区间上,那必须是单调栈走起啊,好像有人还不会诶

priority_stack
这里是利用数组来实现的单调栈,相比普通的STL栈来说要好理解
sta为栈数组,tot为栈顶

每一次向栈中加入值的时候,都把栈中小于等于它的数弹出,并将这些弹出的元素右边界赋值为i-1
注意判断栈中是否还有元素!!1
弹完之后将现在插入的值的左边界赋值为sta[tot]+1;就是栈顶元素的右边一位
最后,将栈中元素全部弹出,右边界复制为n
for(re i=1;i<=n;i++){
		while(tot&&a[i]>=a[sta[tot]])r[sta[tot]]=i-1,tot--;
        //弹出+右边界
		l[i]=sta[tot]+1;
        //左边界
		sta[++tot]=i;
        //加入栈中
	}
	while(tot)r[sta[tot]]=n,tot--;
    //给右边界赋值

然后我们找到左右边界之后,再回来看这个题,

求的是区间和减去最大值\(\mod k==0\)所以,既然涉及到区间求和,那前缀和是必不可少的

设前缀和数组为\(fro[]\),左边界为\(i\),右边界为\(j\),此时区间最大值的坐标为\(x\)

那么我们有一个结论\(fro[j]-fro[i-1]-a[x]=0\)(此处省略一堆+k、%k)

既然这样我们为了好处理这个式子,我们把所有的\(fro[i]\)\(a[i]\)\(k\)取模

接下来我们就可以利用一个\(vector\)数组来存储每一个状态

vec[tmp],是一个\(vector\)类型表示余数为tmp的坐标都有哪些,

在插入值的时候,我们只需要从1~n遍历一遍,然后vec[tmp].push_back(i)就好了

(因为是顺序插入,所以每一个\(vector\)中都是排好序的坐标)

然后我们在左右区间内选取较小的那一段来进行枚举(假设选取左边较小)

那么右边界的前缀和就可以由\(fro[i-1]+a[x]\)来求出,那么我们就可以直接在\(vector\)中查找在x~r这个区间内的数有多少

这个操作可以通过STL内置函数lower_bound和upper_bound来实现

如果是右区间较短,也同理,有些细节在代码里展示

AC_code


#include<bits/stdc++.h>
using namespace std;
#define re register int
#define ll long long
const int N=3e5+5;
const int M=1e6+5;
int n,k;
int a[N],fro[N];
int ys[M],ji[M],cnt;
int l[N],r[N],sta[N],tot;
vector<int> vec[M];
ll ans;
void sol(int x,int l,int r){
	if(x-l<r-x){
		for(re i=l;i<=x;i++){
			int tmp=(fro[i-1]+a[x])%k;//注意此处fro[i-1];
			int lh=lower_bound(vec[tmp].begin(),vec[tmp].end(),x)-vec[tmp].begin();
			int rh=upper_bound(vec[tmp].begin(),vec[tmp].end(),r)-vec[tmp].begin();
			ans+=rh-lh;
		}
	}
	else{
		for(re i=x;i<=r;i++){
			int tmp=(fro[i]+k-a[x]%k)%k;//a[x]%k别忘记
			int lh=lower_bound(vec[tmp].begin(),vec[tmp].end(),l-1)-vec[tmp].begin();//此处l-1
			int rh=upper_bound(vec[tmp].begin(),vec[tmp].end(),x-1)-vec[tmp].begin();//x-1
			ans+=rh-lh;
		}
	}
}
signed main(){	
	scanf("%d%d",&n,&k);
	for(re i=1;i<=n;i++){
		scanf("%d",&a[i]);
		fro[i]=(fro[i-1]+a[i])%k;
		ys[fro[i]]++;
		if(ys[fro[i]]==1)ji[++cnt]=fro[i];//记录出现过的余数
	}
	for(re i=1;i<=cnt;i++){
		vec[ji[i]].reserve(ys[ji[i]]+5);//限制空间
	}
	vec[0].push_back(0);
	for(re i=1;i<=n;i++)
		vec[fro[i]].push_back(i);
	for(re i=1;i<=n;i++){
		while(tot&&a[i]>=a[sta[tot]])r[sta[tot]]=i-1,tot--;
		l[i]=sta[tot]+1;
		sta[++tot]=i;
	}
	while(tot)r[sta[tot]]=n,tot--;
	for(re i=1;i<=n;i++)sol(i,l[i],r[i]);
	printf("%lld",ans-n);//最后方案数减去n,因为左边界不能等于右边界
}

你是不是看到我的代码里好像有一些并没有什么用的vector操作

那些操作可能在这个题中没啥用,但是这个很重要。看看这篇博客

T2 简单的玄学

这个题就是排列组合嘛,数学课上我就一直瞎嚷嚷:正难则反!!正难则反!!!

可是到了真正用到这个做法的时候,我倒是看不出来了,真是的

就这个题统计至少两个数相同的概率,我在考场上苦思冥想,想出来了正着做的办法,然后10pts

考完一看题解,正难则反,直接1减去两两都不相同的概率不就好了???

看的我人都傻了,咋考场上就想不出来啊,要是想出来了,起码也得有50pts啊

两两不相同的概率是

\(\dfrac{A_{2^n}^{m} }{2^{nm}}=\dfrac{\frac{2^n!}{(2^n-m)!}}{2^{nm}}=\dfrac{\prod_{i=2^n-m+1}^{2^n}}{2^{nm}}=\dfrac{\prod_{i=2^n-m+1}^{2^n-1}}{2^{n(m-1)}}\)

这样,目前在这个式子上来看,是没有啥可以约掉的啦

题目要求我们对这分子分母进行约分,仔细观察发现,这个分母的因子只有2

所以我们只需要找到分母中有多少个\(2\)\(OK\)

现在有一个结论:\(2^n-a\)\(a\)中,因子2的个数是一致的,

    因为它们两个加起来是2的幂,所以我们可以知道,这两个数同时除以相同个数的2后,
    一定相加是偶数,所以只能是都是奇数或者都是偶数,都是奇数那就符合这个结论
    都是偶数,那就继续除,除到是奇数为止

所以我们要寻找\(2^n-m+1\)~~~~~\(2^n-1\)这些数相乘的结果中的2的个数,

等价于找\(1~m-1\)中的2的个数,这里确实有一个\(O(logm)\)的算法,并且极其好想

O(logm) get sum of 2
这里主要是利用2的性质
m个数中,有m/2个数可以整除2,那么就有这么多个2
同理,有m/4个数可以整除4,那么就再有这么多2
........
然后就这样一直除下去,得到fz,

for(ll i=2ll;i<m;i*=2ll)
	fz+=(m-1)/i,fz%=(mod-1);

那么此时我们发现如果\(m>mod\)的话,那就一定有一个数是mod的倍数,那分子就变成0咯

但是此时的答案并不是1,因为%mod之后为0,并不代表原来为0

所以这样的话我们就计算分母就好啦,别忘记除掉约去的2

那要是m<mod的话,暴力计算分子,输出答案

答案是1减去你算出来的值啊啊啊啊啊啊啊啊啊啊

AC_code
#include<bits/stdc++.h>
using namespace std;
#define re register int
#define ll long long
const int mod=1e6+3;
ll n,m;
ll fz;
ll a,b;
ll ksm(ll x,ll y){
	ll ret=1;
	while(y){
		if(y&1)ret=ret*x%mod;
		x=x*x%mod;
		y>>=1;
	}
	return ret;
}
signed main(){
	scanf("%lld%lld",&n,&m);
	for(ll i=2ll;i<m;i*=2ll)
		fz+=(m-1)/i,fz%=(mod-1);
	if(m>mod){
		ll tmp=(n%(mod-1))*((m-1)%(mod-1))%(mod-1);
		tmp=(tmp-fz+mod-1)%(mod-1);
		ll bas=ksm(2ll,tmp);
		printf("%lld %lld",bas,bas);
		return 0;
	}
	a=1;
	for(ll i=1;i<m;i++)a=a*(ksm(2ll,n%(mod-1))+mod-i)%mod;
	a=a*ksm(ksm(2ll,fz),mod-2)%mod;
	b=ksm(2ll,((n%(mod-1)*(m-1)%(mod-1))%(mod-1)+mod-1-fz)%(mod-1));
	printf("%lld %lld",(b+mod-a)%mod,b);
}

T3 简单的填数

这个题考场上是真没有思路好吧,真的惨暴了

但是我在考场上确实想到了要正着走一遍,倒着走一遍,只是这个过程有点小小的复杂

然后我在考场上直接放弃了,对着我那第一题的对拍程序高兴去了

这个题主要是维护一个up和dw二元组(x,len),存储这个位置目前最大的值及其个数,最小值及其个数

所以我们就有了一个转移,

    当len==2时,up更新
    当len==5时,dw更新

但是注意,在原序列上如果a[i]上有值的话:

如果当前值和a[i]相同的话,那就不需要做任何更改

如果不相等的话,那就直接赋值成a[i],

    up.len=2(因为我要让up越大越好,所以把他赋值成要更改的边界)
    dw.len=1(要最小嘛)

然后就剩下倒序走一遍然后得到答案序列啦,这是最难的啦

我是记录了一个now表示从上一个标记的a[i]开始5个一组的数,目前是多少,num表示现在的now有几个

然后我们的\(ans[i]=\min({now},{up[i].x})\)

如果遇到下一个标记好的a[i],就\(now=a[i],num=0\)就行了

最后倒序输出一下这个序列就好啦

记得判断的时候有两种情况,一个是up<a[i],dw>a[i];

还有一个就是,最后一个长度不够2,输出-1;

AC_code
#include<bits/stdc++.h>
using namespace std;
#define re register int
const int N=2e5+5;
int n,a[N];
struct node{
	int x,len;
}up[N],dw[N];
int ans[N];
signed main(){
	scanf("%d",&n);
	for(re i=1;i<=n;i++)scanf("%d",&a[i]);
	up[1].x=1;up[1].len=1;
	dw[1].x=1;up[1].len=1;
	for(re i=2;i<=n;i++){
		up[i].x=up[i-1].x;up[i].len=up[i-1].len+1;
		if(up[i].len>2)up[i].x++,up[i].len=1;
		dw[i].x=dw[i-1].x;dw[i].len=dw[i-1].len+1;
		if(dw[i].len>5)dw[i].x++,dw[i].len=1;
		if(a[i]){
			if(up[i].x<a[i]||dw[i].x>a[i]){
				printf("-1");return 0;
			}
			if(up[i].x>a[i])up[i].x=a[i],up[i].len=2;
			if(dw[i].x<a[i])dw[i].x=a[i],dw[i].len=1;
		}
	}
	if(up[n].len==1)up[n].x--,up[n].len=up[n-1].len+1;
	if(up[n].x<dw[n].x){
		printf("-1");return 0;
	}
	printf("%d\n",ans[n]=up[n].x);
	int now=ans[n],num=1;
	for(re i=n-1;i>=1;i--){
		if(a[i]){
			if(now>a[i])now=a[i],num=0;
		}
		if(num==5)now--,num=0;
		num++;
		ans[i]=min(now,up[i].x);
	}
	for(re i=1;i<=n;i++)printf("%d ",ans[i]);
}

完事啦

posted @ 2021-07-12 18:58  fengwu2005  阅读(64)  评论(1编辑  收藏  举报