神奇的数字串 | 线段树&类滑动窗口

神奇的数字串

**

  • 题目
  • 分析
  • 代码

*题目

在这里插入图片描述

*分析

首先 我们来理解一下题目中所定义的good串的含义:
假设一个数字串长度为n,只有当这个数字串的任意前i(1<=i<=n)项和均大于0,我们才能称这个串为good串。

简单来说就是 一个串所有前缀和的最小值大于0 才能称作good串。(不明白前缀和的小伙伴可以去这里前缀和看看,大佬讲的敲详细)

其次 题目中还有一个重要信息:数字串会循环移动k(0<=k<=n-1)位,换个说法就是数字串会循环移动k次,每次移动一位。

打个比方:
有一个长度为3的数字串1 2 3
第0次移位后就是其本身1 2 3
第1次移位后会变成这样2 3 1(将1移动到队尾)
第2次移位后会变成这样3 1 2(在第一次移位的基础上将2移动到队尾)

我们的任务:求出k次移位后,总共出现了多少次good串。

*代码

#include<cstdio>//循环数字串
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int maxn=1e6+7;
int n,s[maxn];//数字个数、数字串数组
long long sum[maxn];//前缀和

头文件以及对变量的定义:
观察数据发现极端情况下前缀和可能达到1e9,所以开了long long.

struct Node
{
    int l,r;
    long long minn,lazy;//区间修改懒标记
} tr[maxn*4];//一定记得开4倍

定义线段树节点结构体:
l,r代表该节点的左界限与右界限
minn代表该节点区间的最小值
lazy为区间修改的懒标记

void build(int d,int l,int r)//维护前缀和最小值
{
    tr[d].l=l;
    tr[d].r=r;
    tr[d].lazy=0;
    if(l==r)//叶子节点存当前位置数的前缀和
    {
        tr[d].minn=sum[l];
        return ;
    }
    int mid=(l+r)/2,lc=d*2,rc=d*2+1;
    build(lc,l,mid);
    build(rc,mid+1,r);
    tr[d].minn=min(tr[lc].minn,tr[rc].minn);
}

正常建树没说的
注意最后一行回溯更新最小值就ok

什么?不懂什么叫当前位置数的前缀和?
还是打个比方吧:
一个数串1 2 3

1-3:1
1-2:1
3-3:6
1-1:1
2-2:3

第一个叶子节点代表1这个数的前缀和,此时1前面只有它自己,所以第一个叶子节点的值为1
第二个叶子节点代表2这个数的前缀和,此时2前面有1和它自己,所以第二个叶子节点的值为1+2=3
第三个叶子节点同理

当进行第一次移位后,数串会改变为 2 3 1
此时的数据结构应该为:

1-3:2
1-2:2
3-3:5
1-1:6
2-2:2

此时1移位到最后一位,第一个叶子节点代表1这个数的前缀和,此时1前面有2,3两个数,因此第一个叶子节点的值为6
第二个叶子节点代表2这个数的前缀和,此时2前面只有它自己,所以第二个叶子节点前缀和为2
第三个节点同理

既然已经知道了节点意义,那么我们需要知道当数字串循环移位时应该如何对节点进行操作:
对比上面两个图我们可以发现:
当一个数移动到最后时前缀和是这样改变的:
这个数对应位置上的前缀和变成了总前缀和,而其他位置上的前缀和均减少了这个数的值
因此我们需要用一次单点修改将被移动的数对应位置上的前缀和修改为总前缀和,再用两次区间修改将这个数前面以及后面的数对应位置上的前缀和减少一定数值。

void push_down(int d)//懒标记下推
{
    if(tr[d].lazy)
    {
        tr[d<<1].lazy=tr[d<<1|1].lazy=tr[d].lazy;//将父节点的懒标记传给子节点
        tr[d<<1].minn-=tr[d].lazy;//子节点最小值减去需要修改的值
        tr[d<<1|1].minn-=tr[d].lazy;
        tr[d].lazy=0;//一定注意要清除下推后的懒标记
    }
}
void updated(int d,int l,int r,int x)//单点修改
{
    int mid=(tr[d].l+tr[d].r)/2,lc=d*2,rc=d*2+1;
    if(tr[d].l==l&&tr[d].r==r)//找到该节点后直接修改
    {
        tr[d].minn=x;
        return ;
    }
    push_down(d);//这里可能有小伙伴要问了:为什么单点修改还要下推懒标记?因为可能存在区间修改后未下推的懒标记,所以记住修改完就下推懒标记
    if(l>mid)
    {
        updated(rc,l,r,x);
    }
    else if(r<=mid)
    {
        updated(lc,l,r,x);
    }
    tr[d].minn=min(tr[lc].minn,tr[rc].minn);//回溯更新不能忘
}
void updateq(int d,int l,int r,int x)//区间修改
{
    if(tr[d].l>=l&&tr[d].r<=r)//找到区间直接修改
    {
        tr[d].minn-=x;
        tr[d].lazy+=x;//添加懒标记
        return ;
    }
    push_down(d);
    int mid=(tr[d].l+tr[d].r)/2,lc=d*2,rc=d*2+1;
    if(l>mid)
    {
        updateq(rc,l,r,x);
    }
    else if(r<=mid)
    {
        updateq(lc,l,r,x);
    }
    else
    {
        updateq(lc,l,mid,x);
        updateq(rc,mid+1,r,x);
    }
    tr[d].minn=min(tr[lc].minn,tr[rc].minn);
}

这里就是标准的单点修改与区间修改(话说裸单点修改不需要懒标记以及下推操作来着)

int main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&s[i]);//输入数字串
    }
    sum[0]=0;
    for(int i=1; i<=n; i++)
    {
        sum[i]=sum[i-1]+s[i];//求数字串前缀和
    }
    build(1,1,n);//建树
    int ans=0;
    for(int i=1; i<=n; i++)
    {
        if(tr[1].minn>=0)//如果根节点(该数字串前缀和最小值)不小于0,ans++;
        {
            ans++;
        }
        updated(1,i,i,sum[n]);//将移位数字处的叶子节点更新为总前缀和
        if(i>=2)
        {
            updateq(1,1,i-1,s[i]);//更新前面的叶子
        }
        if(i+1<=n)
        {
            updateq(1,i+1,n,s[i]);//更新后面的叶子
        }
    }
    printf("%d",ans);
    return 0;
}

整体代码

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int maxn=1e6+7;
int n,s[maxn];
long long sum[maxn];
struct Node
{
    int l,r;
    long long minn,lazy;
} tr[maxn*4];
void build(int d,int l,int r)
{
    tr[d].l=l;
    tr[d].r=r;
    tr[d].lazy=0;
    if(l==r)
    {
        tr[d].minn=sum[l];
        return ;
    }
    int mid=(l+r)/2,lc=d*2,rc=d*2+1;
    build(lc,l,mid);
    build(rc,mid+1,r);
    tr[d].minn=min(tr[lc].minn,tr[rc].minn);
}
void push_down(int d)
{
    if(tr[d].lazy)
    {
        tr[d<<1].lazy=tr[d<<1|1].lazy=tr[d].lazy;
        tr[d<<1].minn-=tr[d].lazy;
        tr[d<<1|1].minn-=tr[d].lazy;
        tr[d].lazy=0;
    }
}
void updated(int d,int l,int r,int x)
{
    int mid=(tr[d].l+tr[d].r)/2,lc=d*2,rc=d*2+1;
    if(tr[d].l==l&&tr[d].r==r)
    {
        tr[d].minn=x;
        return ;
    }
    push_down(d);
    if(l>mid)
    {
        updated(rc,l,r,x);
    }
    else if(r<=mid)
    {
        updated(lc,l,r,x);
    }
    tr[d].minn=min(tr[lc].minn,tr[rc].minn);
}
void updateq(int d,int l,int r,int x)
{
    if(tr[d].l>=l&&tr[d].r<=r)
    {
        tr[d].minn-=x;
        tr[d].lazy+=x;
        return ;
    }
    push_down(d);
    int mid=(tr[d].l+tr[d].r)/2,lc=d*2,rc=d*2+1;
    if(l>mid)
    {
        updateq(rc,l,r,x);
    }
    else if(r<=mid)
    {
        updateq(lc,l,r,x);
    }
    else
    {
        updateq(lc,l,mid,x);
        updateq(rc,mid+1,r,x);
    }
    tr[d].minn=min(tr[lc].minn,tr[rc].minn);
}
int main()
{
    scanf("%d",&n);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&s[i]);
    }
    sum[0]=0;
    for(int i=1; i<=n; i++)
    {
        sum[i]=sum[i-1]+s[i];
    }
    build(1,1,n);
    int ans=0;
    for(int i=1; i<=n; i++)
    {
        if(tr[1].minn>=0)
        {
            ans++;
        }
        updated(1,i,i,sum[n]);
        if(i>=2)
        {
            updateq(1,1,i-1,s[i]);
        }
        if(i+1<=n)
        {
            updateq(1,i+1,n,s[i]);
        }
    }
    printf("%d",ans);
    return 0;
}

That’s All

不对吧不对吧(说好的滑动窗口呢???)

书接上一回

*分析

先来讲讲为什么能用滑动窗口吧:

题目里所说的循环移位k次,如果我们以之前的思路写代码,很容易能发现我们不得不写两个修改函数:单点修改和区间修改,同时还要注意单点修改中也需要进行懒标记下推操作,如果这些细节处理不到位的话我们是很难AC的。

那么有没有一个更好的办法避免修改呢?

首先要知道修改的操作是怎么来的:
当操作空间一定(线段树有效节点与数组有效长度相同)时,我们不得不通过覆盖一部分值来达到更新的目的。
打个比方说:当数组元素为1 2 3时,线段树有效节点有3个,每一个存的就是当前位置数的前缀和,当前缀和改变时我们就需要对线段树节点进行修改。

而当我们将数组元素复制一倍存于此数组后时(比如说1 2 3 1 2 3),我们就能够将循环移位的操作简化为:设置一个长度为n的窗口,在长度为2*n的数组上以一次1个长度的方式移位(n为数字串的长度)。

于是这样就将一道需要修改的线段树转化成了一道简单的滑动窗口问题。

和上一种方法思路相同,只要用线段树维护前缀和的最小值即可,有一点不同的是,我们将修改的操作简化为滑动窗口。
只要注意在查询区间前缀和最小值后要减去当前区间的第一个叶子节点所存的前缀和,这个操作对应了第一种思路中区间修改的操作。

*代码

(注释全在代码里了)
主要还是因为懒

#include<cstdio>//循环数字串之滑动窗口
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int maxn=1e6+7;
int n;
long long s[maxn*2];//由于是求前缀和还是记得要开long long 
struct Node
{
    int l,r;
    long long minn;
}tr[maxn*8];//由于数组变成两倍了所以线段树记得开8倍
void build(int d,int l,int r)//标准建树模板
{
    tr[d].l=l;
    tr[d].r=r;
    if(l==r)
    {
        tr[d].minn=s[l];
        return ;
    }
    int mid=(l+r)/2,lc=d*2,rc=d*2+1;
    build(lc,l,mid);
    build(rc,mid+1,r);
    tr[d].minn=min(tr[lc].minn,tr[rc].minn);
}
long long query(int d,int l,int r)//标准查询模板
{
    if(tr[d].l==l&&tr[d].r==r)
    {
        return tr[d].minn;
    }
    int mid=(tr[d].l+tr[d].r)/2,lc=d*2,rc=d*2+1;
    if(l>mid)
    {
        return query(rc,l,r);
    }
    else if(r<=mid)
    {
        return query(lc,l,r);
    }
    else
    {
        return min(query(lc,l,mid),query(rc,mid+1,r));
    }
}
int main()
{
    scanf("%d",&n);
    s[0]=0;//求前缀和的前缀0
    for(int i=1;i<=n;i++)
    {
        scanf("%lld",&s[i]);
        s[i+n]=s[i];//将s[]数组复制一遍
    }
    for(int i=1;i<=n*2;i++)
    {
        s[i]+=s[i-1];//求前缀和
    }
    build(1,1,2*n);
    int ans=0;
    for(int i=1;i<=n;i++)
    {
        if(query(1,i,i+n-1)-s[i]>=0)//具体看上面的思路
        {
            ans++;
        }
    }
    printf("%d",ans);
    return 0;
}//轻松结束

That’s All

这次是真的

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值