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]);
}
完事啦