T1:简单的区间

Description

NOIP模拟12「简单的区间·简单的玄学·简单的填数」_分治

基本思路:

  区间问题,我最不擅长了
  考虑分治:
  我们首先可以\(O(n)\)用单调栈搞出没一个值作为最大值的区间的左右端点,并且可以处理出第i位前缀和%k的余数并记录,用一个vector,空间复杂度为\(O(n)\),可以接受,由于我们是顺序遍历,所以在每一个vector中下标是升序的。
  然后我们遍历\(1~n\),处理包含\(a[i]\)在内的区间
  我们不必左右两个区间都枚举。只需枚举较短的区间,然后在vector中利用lower_bound与upper_bound\(O(logn)\)查询即可;
  上代码:

#include<bits/stdc++.h>
using namespace std;
namespace STD
{
    #define ll long long 
    #define rr register 
    #define inf INT_MAX
    #define max(x,y) (x>y?x:y)
    const int SIZE=3e5+4;
    const int SIZEK=1e6+4;
    int n,k;
    ll ans;
    ll a[SIZE];
    int f[SIZE];
    int l[SIZE],r[SIZE];//l<r
    vector<int> b[SIZEK];
    int read()
    {
        rr int x_read=0,y_read=1;
        rr char c_read=getchar();
        while(c_read<'0'||c_read>'9')
        {
            if(c_read=='-') y_read=-1;
            c_read=getchar();
        }
        while(c_read<='9'&&c_read>='0')
        {
            x_read=x_read*10+(c_read^48);
            c_read=getchar();
        }
        return x_read*y_read;
    }
    struct pack
    {
        int fi,se;
        pack(){}
        pack(int fi_,int se_){this->fi=fi_;this->se=se_;}
    };
    class Stack
    {
        private:
            pack *Top;
            pack s[SIZE];
        public:
            Stack(){Top=s;}
            void push(pack x){*(++Top)=x;}
            void pop(){Top--;}
            bool empty(){return Top==s;}
            pack top(){return *Top;}
    }s;
};
using namespace STD;
int main()
{
    n=read(),k=read();
    b[0].push_back(0);
	//一定要提前把0压入原因见下
    for(rr int i=1;i<=n;i++)
    {
        a[i]=read();
        f[i]=(f[i-1]+a[i]%k)%k;
        b[f[i]].push_back(i);
    }
	//单调栈
    for(rr int i=1;i<=n;i++)
    {
        if(s.empty()){s.push(pack(a[i],i));continue;}
        if(s.top().fi>=a[i]){s.push(pack(a[i],i));continue;}
        while(!s.empty()&&s.top().fi<a[i])
        {
            r[s.top().se]=i-1;
            s.pop();
        }
        s.push(pack(a[i],i));
    }
    while(!s.empty()){r[s.top().se]=n;s.pop();}
    for(rr int i=n;i;i--)
    {
        if(s.empty()){s.push(pack(a[i],i));continue;}
        if(s.top().fi>a[i]){s.push(pack(a[i],i));continue;}
        while(!s.empty()&&s.top().fi<=a[i])
        {
            l[s.top().se]=i+1;
            s.pop();
        }
        s.push(pack(a[i],i));
    }
    while(!s.empty()){l[s.top().se]=1;s.pop();}
	//
	//这个统计ans的式子是依据%k相同的数相减可以被k整除
	//就有j[r]-j[l-1]-a[i]==0
	//下面num的计算是依据这个式子移项来的
	//注意观察与思考lower_bound第3个参数
    for(rr int i=1;i<=n;i++)
    {
        if(l[i]==r[i]) continue;//l<r
        //以i作为某一端点
        int num,st,en;
        if(i-l[i])
        {
            num=((f[i]-a[i]%k)+k)%k;
            st=lower_bound(b[num].begin(),b[num].end(),l[i]-1)-b[num].begin();
            en=upper_bound(b[num].begin(),b[num].end(),i-2)-b[num].begin();
			//一定要注意b[num]可能为空,如果不判空就调用会RE
			//为因为这个没看出来调了一下午,这下要长记性了QAQ。
            if(b[num].size()&&b[num][st]<=i-2)
                ans+=(en-st);
        }
        if(r[i]-i)
        {
            num=(f[i-1]+a[i]%k)%k;
            st=lower_bound(b[num].begin(),b[num].end(),i+1)-b[num].begin();
            en=upper_bound(b[num].begin(),b[num].end(),r[i])-b[num].begin();
            if(b[num].size()&&b[num][st]<=r[i])
                ans+=(en-st);
        }
        if(!(i-l[i])||!(r[i]-i)) continue;
        if(i-l[i]<r[i]-i)
        {
            for(rr int j=l[i]-1;j<i-1;j++)
            {
                num=(f[j]+a[i]%k)%k;
                st=lower_bound(b[num].begin(),b[num].end(),i+1)-b[num].begin();
                en=upper_bound(b[num].begin(),b[num].end(),r[i])-b[num].begin();
                if(b[num].size()&&b[num][st]<=r[i])
                    ans+=(en-st); 
            }
        }
        else
        {
            for(rr int j=i+1;j<=r[i];j++)
            {
                num=((f[j]-a[i]%k)+k)%k;
                st=lower_bound(b[num].begin(),b[num].end(),l[i]-1)-b[num].begin();
                en=upper_bound(b[num].begin(),b[num].end(),i-2)-b[num].begin();
                if(b[num].size()&&b[num][st]<=i-2)
                    ans+=(en-st);
            }
        }
    }
	printf("%lld",ans);
}
T2:简单的玄学

Description:

NOIP模拟12「简单的区间·简单的玄学·简单的填数」_c++_02
  很明显,直接求是不可能的,所以我们要求\(1-P(一个相等的也没有)\)。
  式子就是:

 

\[1-\frac{2^{n}*(2^{n}-1)*(2^{n}-2)*...(2^{n}-m+1)}{2^{mn}} \]

 

 

\[= \]

 

 

\[1-\frac{\prod_{i=2^{n}-m+1}^{2^n-1}}{2^{n(m-1)}} \]

 

  其实式子很好推,但是难计算,考场上就是因为不会高效计算WA了。
  其实可以观察,当m或n比较大时,分子是有可能变成1e6+3的倍数的,所以我们当乘到是模数的倍数时,就跳出,因为还有个“1-”呢,总不可能输出一吧;
  2的幂次直接快速幂即可。
  至于说约分。
  我们可以发现,分母的因子全是2.
  因此统计分子有多少个2因子即可
  有一个性质:

\(2^n-x\)的2因子个数等于\(x\)中的个数,证明:
假设\(x=2^{m}*j\),\(j\)可以是分数

 

\[2^{n}-x= \]

 

 

\[2^{m}*(2^{n-m}-j) \]

 

显然\(j\)没有2因子,因而\(2^{n-m}-j\)没有2因子,因而该结论正确
又由于连乘因子无损失,因此我们直接求\(!(m-1)\)的2因子个数即可

  这有一个\(O(logn)\)求法,详见代码;
  分子约分后值求解直接暴力计算即可。
  上代码:

#include<bits/stdc++.h>
using namespace std;
namespace STD
{
    #define ll long long
    #define rr register 
    #define inf INT_MAX
    const ll mod=1e6+3;
    ll n,m;
    ll read()
    {
        rr ll x_read=0,y_read=1;
        rr char c_read=getchar();
        while(c_read<'0'||c_read>'9')
        {
            if(c_read=='-') y_read=-1;
            c_read=getchar();
        }
        while(c_read<='9'&&c_read>='0')
        {
            x_read=x_read*10+(c_read^48);
            c_read=getchar();
        }
        return x_read*y_read;
    }
    ll qpow(ll base,ll exp)
    {
        ll ret=1;
        while(exp)
        {
            if(exp&1) ret=ret*base%mod;
            base=base*base%mod;
            exp>>=1;
        }
        return ret;
    }
};
using namespace STD;
int main()
{
    n=read(),m=read();
    ll cnt=0;
    for(ll i=1;(1ll<<i)<=(m-1);i++)
        cnt+=((m-1)/(1ll<<i));
    ll b=qpow(2,n);
    ll n2=b;
    ll n3;
    b=qpow(b,m-1);
    n3=b;
    ll x=qpow(2,cnt);
    x=qpow(x,mod-2);
    b=b*x%mod;
    ll a=1ll;
    for(rr ll i=1;i<=m-1;i++)
    {
        a=a*((n2-i%mod)%mod)%mod;
        if(!a) break;
    }
    a=a*x%mod;
    a=(b-a+mod)%mod;//这里一定要注意用b-a再取模,因为b才是约分后的分母
    printf("%lld %lld",a,b);
}
T3:简单的填数

Description:

NOIP模拟12「简单的区间·简单的玄学·简单的填数」_分治_03

基本思路:

  贪心法
  气死,又是这种小点,像分治,贪心,堆栈,队列这些最基本的小点总是会忽视,气死了。
  说思路:我们定义两个数组\(up、down\)记录在i位置我们期望可以得到的最大值最小值及其长度,这个正序\(O(n)\)扫一遍就可以
  由于他们维护的是最值,那么我们肯定是希望他们越大(小)越好了,那么当\(up\)的长度满2时\(val\)就加一,\(down\)满五时\(val\)就加一,这样就是所有可能值里的最大(小)值了。
  然后倒序\(O(n)\)扫一遍,就可以推出真正的序列了。注意真正的序列不是\(up\)的\(val\)因为假如\(len\)为一,那么这个\(up\)的\(val\)只出现了一次,很明显是不合法的。
  上代码:

#include<bits/stdc++.h>
using namespace std;
namespace STD
{
    #define ll long long
    #define rr register 
    #define min(x,y) x<y?x:y
    #define max(x,y) x>y?x:y
    const int SIZE=2e5+4;
    const int RANGE=1e5+4;
    int n;
    int a[SIZE];
    int cnt[RANGE];
    struct pack{int val,len;}up[SIZE],down[SIZE];
    int read()
    {
        rr int x_read=0,y_read=1;
        rr char c_read=getchar();
        while(c_read<'0'||c_read>'9')
        {
            if(c_read=='-') y_read=-1;
            c_read=getchar();
        }
        while(c_read<='9'&&c_read>='0')
        {
            x_read=(x_read*10)+(c_read^48);
            c_read=getchar();
        }
        return x_read*y_read;
    }
};
using namespace STD;
int main()
{
    n=read();
    for(rr int i=1;i<=n;i++) a[i]=read();
    up[1].val=up[1].len=1;
    down[1].val=1,down[1].len=1;
    for(rr int i=2;i<=n;i++)
    {
        up[i].len=up[i-1].len+1;
        up[i].val=up[i-1].val;
        down[i].val=down[i-1].val;
        down[i].len=down[i-1].len+1;
        if(up[i].len>2)
        {
            up[i].val++;
            up[i].len=1;
        }
        if(down[i].len>5)
        {
            down[i].val++;
            down[i].len=1;
        }
        if(!a[i]) continue;
        if(a[i]<up[i].val) up[i].val=a[i],up[i].len=2;
        if(down[i].val<a[i]) down[i].val=a[i],down[i].len=1;
		//要注意的是假如说我a已经有值了就要将它与up、down取最值
		//因为这时a已经有值的话a之后的值要以他为基础加一加一地上升
		//为了兼顾实际情况,我也要与a取最值,更何况我要依靠up搞出an的最大值
		//最后的up一定是要可取的,就这一点来说我也要在之前取一下最值
		//因为最后an的取值还是要受之前值得影响的,毕竟有2到5的限制
        if(a[i]>up[i].val||a[i]<down[i].val){cout<<-1<<endl;return 0;}
		//由于我维护的是可能的最小(大)值假如说现在的a比up还大或比down还小,就证明我之前无论怎么努力都升(降)不到a显然是不合法的
    }
    if(up[n].len<2) up[n].val=up[n-1].val,up[n].len=up[n-1].len+1;
    a[n]=up[n].val;
    printf("%d\n",a[n]);
    for(rr int i=n;i>=1;i--)
    {
        if(a[i]){cnt[a[i]]++;continue;}
        a[i]=min(a[i+1],up[i].val);
        if(cnt[a[i]]==5)
            a[i]--;
        cnt[a[i]]++;
    }
    for(rr int i=1;i<=n;i++)
        printf("%d ",a[i]);
}
后记

关于T1:

  考场上那个区间的处理方法我想出来了,与上一次模拟题的T3是一样的;
  但是我想的是用桶而不是vector记录再二分来查个数,结果考场上没做出来这道题
  而且时间复杂度还算错了,人家实际上是\(O(nlogn)\)我算成了\(O(n^{2})\)QAQ
  难受,得学着证明了。
  气死。
  这道题主要还是学习分治法。
  而且要活泛,我一想到用桶,就一直想怎么用桶,其实有想过这种方法是不对的。
  但是我以为能干,就一直想着怎么把桶改改了,就没有想用别的了。

关于T2:

  式子很好推,然而很难计算。因此我考场上直接跳了这道题。
  约分用到的性质我不会,这次要记住了。
  总归还是想象力不足,也不是很敢想。
  这点要改改了。

关于T3

  一开始我想的是模拟,不过挂了。
  连判无解都写错了。
  贪心法不熟,气死。
2021.7.13 现役