9 16 模拟赛&关于线段树上二分总结

1 考试时又犯了一个致命的错误,没有去思考T2的正解而是去简单的推了一下式子开始了漫漫找规律之路,不应该这样做的 为了得到规律虽然也打了暴力 但是还是打了一些不必要的程序 例如求组合数什么的比较浪费时间了。

考试后在我看来这都是浪费时间了 浪费了我足足1h30min 其实是可以避免的 得不到正解先去看下一道题 都做完了再回来重新审视这道题。

2 T3的正解推得的时间虽然不长但是没有仔细思考自己的做法是否优秀尽管应该是正确的 但是不充足的思考让我的代码变得非常的冗长。

例如 一些区间求和的操作我完全可以直接拿处理好的前缀和后缀和数组求但是我还是写了两个线段树函数求和。明明可以不用线段树O(n)来写代码复杂度会低的很但是我没有充分的思考直接码上了线段树...

当然还是有优点的 T1写完后直接开始对拍了 这一点很好(拍了2万组数据 所以我就没怎么检查 就直接交了。

再论述题解的时候 我希望这套题不仅仅是对考试的思考 还有对线段树的总结我想我应该总结线段树的某些功能是怎么写的了不能每次写的时候都再推一遍。

线段树上二分是一个技巧且比较有用因为其利用本身就是区间二分的形式建的树和本身就支持查询区间最值的特点致使我们寻找某个值比较方便。

这里先给出题目背景 给定一个序列对于询问 x 求整个序列小于x的第一个位置是谁?

这个我们暴力扫其实都可以 但是多次询问呢?把询问离线由大到小的搞,强制在线呢?一个比较经典的做法是 ST+二分 看似序列并不是单调的 但是我们二分这个答案的长度 加以ST表寻找最值即可解决。

带上单点修改呢?ST表可是不支持修改的,那么此时线段树上二分就可以轻易解决这个问题了。

考虑我们先进入线段树 观察总体是否都大于x 不是的话那就 再观察左边 然后 去左边还是去右边递归的搞即可。

限定区间l r 怎么搞 这就是思维和代码难度的升级了 其实我们发现这个东西并不好写 但是 这也是对线段树的 利用 尽管非常的困。

其实对于这一个询问来说 我们可以定点l 寻找 一个rr 属于r即可。但是显然的是限定这个r是没有什么意义的我们完全可以向左找到在和r比 所以这里r 是没有用的。这里放上代码 不断还是在区间二分即可。

具体的还是找到了两个端点相当于一个区间 然后复杂度还是logn;

code:

inline int ask(int p,int l,int r,int L,int x)//求 L R里第一个<=x的数字
{
    if(sum(p)<=x)return r;
    if(l==r)return l-1;
    int mid=(l+r)>>1;
    if(l>=L)
    {
        if(sum(l(p))<=x)return ask(r(p),mid+1,r,L,x);
        return ask(l(p),l,mid,L,x);
    }
    if(L>mid)return ask(r(p),mid+1,r,L,x);
    int w=ask(l(p),l,mid,L,x);
    if(w==mid)
    {
        if(sum(r(p))<=x)return r;
        return ask(r(p),mid+1,r,L,x);
    }
    return w;
}

这就是常用的板子 写出来要对线段树有一定的理解 复杂度 logn 可以发现我们其实是先找到了左端点 再寻找到右端点了 所以相当于找到了一个区间。

不算很复杂 当然找全局的答案就更显得轻松了。值得注意的是由于查找的是<=x 所以我们sum记录的是区间最大值。
9 16 模拟赛&关于线段树上二分总结_第1张图片

当然我调了一天的T3 到了晚上最后的时刻才 发现了点什么思路是错误的 T3的伪思路 。

必胜点 的左部点的确只能吃一个方向的点 但是 注意到如果这个点事输的点那么就还会存在左边去完去右边 所以导致答案的错误。

所以我没有全局考虑还浪费近乎一天的时间 今天有点迷 头脑不太清醒 以后需要多思考正确性看看能不能把自己的做法卡掉这是关键。

对拍不一定能够发现问题 小数据2万组AC 大数据照样崩也是正常的事情。所以不要仗着对拍的成功就说明自己代码没有什么问题。

当一个代码调的时间过长的时候是不能再调了 因为可能思路错了 等到冷静下来再思考一下看看是否还有bug。浪费时间是可耻的。

当考试时间剩下的不多的时候不能再写自己刚想出来且还没有证明是正确的思路 因为这个时候可能存在漏洞还没有发现。可能当快写完的时候才会发现自己想错了 或者有一个细节什么的处理不了 或者代码复杂度过高根本是写不了的地步。

当代码难度较高 且正确性还没有证明 那么就不要去实现它 因为可能暴力分比其好写 好得分。

比赛中尽量拿分才是关键而不是AC一道全场都不能AC的题目。

综上所述 我是沙比。

进入正文:先放一波 我的伪代码毕竟调了很久里面的线段树向左二分和向右二分也很值得复习。

//#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ll long long
#define db double
#define min(x,y) ((x)>(y)?(y):(x))
#define max(x,y) ((x)>(y)?(x):(y))
#define INF 1000000010
#define cnt(p) s[p].cnt
#define sum(p) s[p].sum
#define l(p) s[p].l
#define r(p) s[p].r
using namespace std;
char *ft,*fs,buf[1<<15];
inline char getc()
{
    return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++;
}
inline ll read()
{
    ll x=0,f=1;char ch=getc();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();}
    return x*f;
}
const ll MAXN=1000010;
ll n,m,root,id,maxx;
ll t,h,flag;
ll sum[MAXN],cnt[MAXN];
ll a[MAXN],mark[MAXN],vis[MAXN];
ll q[MAXN],w[MAXN],f[MAXN];
struct wy
{
    ll l,r;
    ll sum;
    ll cnt;
}s[MAXN<<1];
inline void insert(ll &p,ll l,ll r,ll x,ll val,ll op)
{
    if(!p)p=++id;
    if(l==r){if(op)sum(p)=val;else cnt(p)=val;return;}
    ll mid=(l+r)>>1;
    if(x<=mid)insert(l(p),l,mid,x,val,op);
    else insert(r(p),mid+1,r,x,val,op);
    sum(p)=max(sum(l(p)),sum(r(p)));
    cnt(p)=max(cnt(l(p)),cnt(r(p)));
}
inline ll ask(ll p,ll l,ll r,ll x,ll val)
{
    if(x>n)return n;
    if(sum(p)<=val)return r;
    if(l==r)return l-1;
    ll mid=(l+r)>>1;
    if(l>=x)
    {
        if(sum(l(p))<=val)return ask(r(p),mid+1,r,x,val);
        return ask(l(p),l,mid,x,val);
    }
    if(mid>1;
    if(r<=x)
    {
        if(cnt(r(p))<=val)return query(l(p),l,mid,x,val);
        return query(r(p),mid+1,r,x,val);
    }
    if(x<=mid)return query(l(p),l,mid,x,val);
    ll w=query(r(p),mid+1,r,x,val);
    if(w==mid+1)
    {
        if(cnt(l(p))<=val)return l;
        return query(l(p),l,mid,x,val);
    }
    return w;
}
inline void judge(ll x)
{
    if(!vis[x-1]&&x>=2)
    {
        q[++t]=x-1;
        vis[x-1]=1;
        f[t]=0;
        w[t]=a[x]-m;
    }
    if(!vis[x+1]&&x<=n-1)
    {
        q[++t]=x+1;
        w[t]=a[x]-m;
        f[t]=1;
        vis[x+1]=1;
    }
}
inline void unjudge(ll x,ll val)
{
    if(!vis[x-1]&&x>=2)
    {
        q[++t]=x-1;
        vis[x-1]=1;
        f[t]=0;
        w[t]=max(val-a[x],a[x]-m);
        //w[t]=val-a[x];
    }
    if(!vis[x+1]&&x+1<=n)
    {
        q[++t]=x+1;
        w[t]=max(val-a[x],a[x]-m);
        //w[t]=val-a[x];
        f[t]=1;
        vis[x+1]=1;
    }
}
inline void bfs()
{
    while(h++=w[h])
            {
                mark[x]=1;
                judge(x);
            }
            else unjudge(x,w[h]);
        }
        else
        {
            pos=query(1,1,n,x-1,m-cnt[x+1]);
            s=sum[x]-sum[pos-1];
            //if(x==188)cout<=w[h])
            {
                mark[x]=1;
                judge(x);
            }
            else unjudge(x,w[h]);
        }
    }
}
int main()
{
    freopen("1.in","r",stdin);
    n=read();m=read();
    for(ll i=1;i<=n;++i)
    {
        a[i]=read();
        sum[i]=sum[i-1]+a[i];
        maxx=max(maxx,a[i]);
    }
    for(ll i=n;i>=1;--i)cnt[i]=cnt[i+1]+a[i];
    for(ll i=1;i<=n;++i)
        insert(root,1,n,i,a[i]-sum[i-1],1);//向右吃
    for(ll i=1;i<=n;++i)
        insert(root,1,n,i,a[i]-cnt[i+1],0);//向左吃
    for(ll i=1;i<=n;++i)
        if(a[i]==maxx&&!flag)
        {
            mark[i]=1;
            vis[i]=1;flag=1;
            cout<=2)
            {
                q[++t]=i-1;
                vis[i-1]=1;
                f[t]=0;
                w[t]=maxx-m;
            }
            if(!vis[i+1]&&i<=n-1)
            {
                q[++t]=i+1;
                w[t]=maxx-m;
                f[t]=1;
                vis[i+1]=1;
            }
        }
    bfs();
    //int s=188;
    //int s1=ask(1,1,n,s+1,m-sum[s-1]);
    //int s2=query(1,1,n,s-1,m-cnt[s+1]);
    //cout<

考虑正解 其实 还算是比较难以思考出来的每次想我都会不知不觉转移到暴力的身上。 首先对于每一个数字 求出左边第一个比其大的数字 右边第一个比其大的数字。

必胜点已知 且吞噬掉必胜点的点必然也是必胜点 我们进行继续推下去 对于一个数字我们知道了它当前能吞噬的一个区间 如果这个区间+m比左边和右边都小那么就是必败点了。

吞噬掉必败点一定必败 但是如何阻止暴力的产生?其实这也是不可避免的但是我们可以利用只暴力一次的想法把所有点的合法情况都给求出来。

记搜优化这个过程即可。当实现的时候就发现了是否存在环?这显然是不可能的。

//#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ll long long
#define db double
#define min(x,y) ((x)>(y)?(y):(x))
#define max(x,y) ((x)>(y)?(x):(y))
#define INF 8000000000000010ll
using namespace std;
char *ft,*fs,buf[1<<15];
inline char getc()
{
    return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++;
}
inline ll read()
{
    ll x=0,f=1;char ch=getc();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();}
    return x*f;
}
const ll MAXN=8000010;
ll n,m,maxx,top;
ll sum[MAXN];
ll l[MAXN],r[MAXN];
ll a[MAXN],vis[MAXN],s[MAXN];
inline void dfs(ll x)
{
    vis[x]=-1;
    ll cnt=sum[r[x]-1]-sum[l[x]];
    if(cnt+m>=a[r[x]])
    {
        if(!vis[r[x]])dfs(r[x]);
        if(vis[r[x]]==1)vis[x]=1;
    }
    if(cnt+m>=a[l[x]])
    {
        if(!vis[l[x]])dfs(l[x]);
        if(vis[l[x]]==1)vis[x]=1;
    }
    return;
}
signed main()
{
    freopen("1.in","r",stdin);
    n=read();m=read();
    a[0]=INF;a[n+1]=INF;
    for(ll i=1;i<=n;++i)
    {
        a[i]=read();
        maxx=max(maxx,a[i]);
        sum[i]=sum[i-1]+a[i];
    }
    s[++top]=0;
    for(ll i=1;i<=n;++i)
    {   
        if(a[i]==maxx)vis[i]=1;
        while(top&&a[s[top]]<=a[i])--top;
        l[i]=s[top];s[++top]=i;
    }
    s[top=1]=n+1;
    for(ll i=n;i>=1;--i)
    {
        while(top&&a[s[top]]<=a[i])--top;
        r[i]=s[top];s[++top]=i;
    }
    vis[0]=vis[n+1]=1;
    for(ll i=1;i<=n;++i)if(!vis[i])dfs(i);
    for(ll i=1;i<=n;++i)if(vis[i]==1)printf("%d ",i);
    return 0;
}

9 16 模拟赛&关于线段树上二分总结_第2张图片

关于T2 比较难思考 我 连最基础的都证明不出来 更别说列出来递推式了。

突然想到自己为什么要看题解...突然自己推了一波 感觉是正确的。

首先 对于构造这个长度为n的字符串我们显然有\(n^m\)\(n^2\)这个东西 当然manacher显然可以加快这个过程。不过复杂度少乘一个n。

这样显然是不科学的 因为n和m都过大不能想着构造 对于一个长度为n的字符串字符集为m 那么显然这个字符串的个数为 sum=\(m^ \frac{n}{2}\)

减去不合法的即可。那么就显然有ans=sum-\(\sum_{i=2}^{n-1}\)\(f_i\)\(\frac{(n-2*i+1)}{2}\) 其中\(f_i\)表示长度为i的字符串的是回文串的个数且前缀不含回文的数量。

我们只要求出f这个数组即可 但是还没有那么简单..\(f_i\)这个数组中可能有重。

关于一个命题 :一个长度大于\(\frac{n}{2}\)的回文字符串 必然存在一个小于\(\frac{n}{2}\)的回文串。

证明 :分两种情况 一种显然 一种定量计算即可。(本人找刘神证明过了

那么上式中i的范围就可以被我们从n-1缩小到\(\frac{n}{2}\)的范围之内了 那么现在我们就可以写了。

然后我们发现答案其实就是f[n]了 也就是这个式子就是用来计算f数组的我们直接赋好初值即可,n^2递推

const ll MAXN=1000010;
ll n,m;
ll f[MAXN],p[MAXN];
signed main()
{
    freopen("1.in","r",stdin);
    n=read();m=read();
    p[0]=1;f[1]=m;f[2]=m;
    for(ll i=1;i<=n;++i)p[i]=p[i-1]*m%mod;
    for(ll i=3;i<=n;++i)
    {
        for(ll j=2;j<=(i+1)/2;++j)
        {
            f[i]=((f[i]-((f[j]%mod)*p[((i-2*j+1)/2)])%mod)%mod+mod)%mod;
        }
        f[i]=(f[i]+p[(i+1)/2])%mod;
    }
    printf("%lld\n",f[n]%mod);
    return 0;
}

显然这个式子是可以优化的我们发现每次j的集合最多增大1 所以前缀和优化一下就行了 优化细节开个数组记录一下即可。

ll n,m;
ll f[MAXN],p[MAXN],g[MAXN];
signed main()
{
    freopen("1.in","r",stdin);
    n=read();m=read();
    p[0]=1;f[1]=m;f[2]=m;
    for(ll i=1;i<=n;++i)p[i]=p[i-1]*m%mod;
    for(ll i=3;i<=n;++i)
    {
        if(i&1)g[i]=(g[i-1]*m)%mod;
        else g[i]=g[i-1]%mod;
        if(i&1)g[i]=(g[i]+(-(f[(i+1)/2]%mod)*p[(i-2*((i+1)/2)+1)/2])%mod)%mod;
        f[i]=(f[i]+g[i])%mod;
        f[i]=(f[i]+p[(i+1)/2])%mod;
    }
    printf("%lld\n",(f[n]%mod+mod)%mod);
    return 0;
}

真看不懂就别看了 很自闭研究了好久这道题。

9 16 模拟赛&关于线段树上二分总结_第3张图片

这可能是这两天最水的题目了 简单的树形dp 看完题就秒了。

dp一下 然后换根操作也显得非常的简单。

//#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define ll long long
#define db double
#define R register
#define min(x,y) ((x)>(y)?(y):(x))
#define max(x,y) ((x)>(y)?(x):(y))
#define INF 1000000000
using namespace std;
char *ft,*fs,buf[1<<15];
inline char getc()
{
    return (fs==ft&&(ft=(fs=buf)+fread(buf,1,1<<15,stdin),fs==ft))?0:*fs++;
}
inline int read()
{
    int x=0,f=1;char ch=getc();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getc();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getc();}
    return x*f;
}
const int MAXN=1000010;
int n,flag,ans,len;
int w[MAXN],f[MAXN],g[MAXN],mark[MAXN];
int lin[MAXN],nex[MAXN<<1],ver[MAXN<<1];
inline void add(int x,int y)
{
    ver[++len]=y;
    nex[len]=lin[x];
    lin[x]=len;
}
inline void dfs(int x,int father)
{
    f[x]=w[x];
    for(int i=lin[x];i;i=nex[i])
    {
        int tn=ver[i];
        if(tn==father)continue;
        dfs(tn,x);
        if(f[tn]>0)
        {
            f[x]+=f[tn];
            mark[tn]=1;
        }
    }
    return;
}
inline void dp(int x,int father)
{
    for(int i=lin[x];i;i=nex[i])
    {
        int tn=ver[i];
        if(tn==father)continue;
        if(mark[tn])g[tn]=max(f[tn],g[x]-f[tn]);
        else g[tn]=max(0,g[x])+f[tn];
        dp(tn,x);
    }
}
int main()
{
    //freopen("1.in","r",stdin);
    //freopen("trip.in","r",stdin);
    //freopen("trip.out","w",stdout);
    n=read();
    for(int i=1;i<=n;++i)
    {
        w[i]=read();
        if(i!=1)if(w[i]!=w[i-1])flag=1;
        if(!w[i])w[i]=-1;
    }
    if(!flag)
    {
        printf("%d\n",n);
        return 0;
    }
    for(int i=1;i

这个T2 真令人自闭 这个T3 我的线段树也令人自闭。

你可能感兴趣的:(9 16 模拟赛&关于线段树上二分总结)