Hetao P10484 [XRCOI Round 4] 01 transform 题解 [ 蓝 ] [ 奇偶位反转 ] [ 线段树 ] [ 栈 ]

01 transform:蒟蒻的第四道公开赛题 qwq。

Subtask 1

暴搜剪枝即可。

Subtask 2

因为 \(p\) 全是 \(\texttt{1}\),所以相当于只要对 \(s\) 考虑即可。

观察发现,长度为偶数的一段相同字符一定可以变成相反的一段相同字符,然后将两端的字符连接到一起,比如 \(\texttt{011110}\) 可以变成 \(\texttt{000000}\)。因此,我们可以将这个过程看作把长度为偶数的一段相同字符删除。具体实现上,因为偶数长度可以用一些长度为 \(2\) 的相同字符拼起来,所以我们不断将相邻且相同的两个字符删除,最后剩下的就一定是类似 \(\texttt{010101}\) 的交替结构。

显然,当 \(p\) 全是 \(\texttt{1}\) 时,\(s\) 最后只剩下一个 \(\texttt{1}\) 或者什么都不剩才是满足条件的状态。因此,我们用一个栈来维护,每次将栈顶的两个相同的元素删去即可。

时间复杂度 \(O(n^2)\)

Subtask 3

考虑如何加快 Subtask 2 的过程,因为我们删除的是两个相邻且相等的字符,所以每次删除的时候这两个字符之间原本可能隔着的字符一定是偶数个,也就是他们在原序列中的下标的奇偶性不同

因此,我们考虑将 \(\texttt{0}\) 赋值为 \(1\),将 \(\texttt{1}\) 赋值为 \(-1\),然后将偶数位的值全部乘 \(-1\)。一段区间能全被消完,当且仅当这段区间的和为 \(0\)

这样做很容易证明是对的,因为一个 \(+1,-1\) 能被抵消,只有在下标奇偶性不同且字符相同,或者下标奇偶性相同且字符不同的时候。第一种显然能被消掉,而第二种时这两种字符间一定隔着奇数个字符,则一定可以转化为若干个第一种相消,最后留下单个字符的情况。

比如 \(\texttt{010010}\),原来赋的值为 \({1,-1,1,1,-1,1}\),偶数位乘 \(-1\) 后变为 \({1,1,1,-1,-1,-1}\),整个数组的和为 \(0\),那么就是可以消除的了。

因此,我们按这样用线段树维护这样的一个序列,最终满足条件的就是和为 \(1\) 或者为 \(0\) 的情况了。对于区间翻转,直接将区间里的数乘 \(-1\) 即可。

注意特判,如果两个区间的开头奇偶性不同,就要将一个区间的和乘上 \(-1\) 再来判断。

时间复杂度 \(O(n\log n)\)

Subtask 4

进一步在 Subtask 2 的基础上观察满足要求的条件,不难发现只要最终两个字符串消成的栈相同即可满足要求。

因为最终的形态一定是 \(\texttt{01}\) 交替的,而最终相邻的两个字符之间原本一定隔着偶数个相同字符,所以由于可以执行 \(\texttt{001}\to \texttt{100}\) 之类左右移动某个字符两个位置的操作,这些 \(\texttt{01}\) 相当于是可以左右移动偶数个位置的,并且不会改变左右两边同色字符个数的奇偶性。

由此用栈模拟即可,时间复杂度 \(O(n^2)\)

#include <bits/stdc++.h>
#define fi first
#define se second
#define lc (p<<1)
#define rc ((p<<1)|1)
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi=pair<int,int>;
const int N=1000005;
int n,q,a[N],b[N],stk1[N],tp1=0,stk2[N],tp2=0;
bool check(int x,int y,int z)
{
    tp1=tp2=0;
    for(int i=x;i<=x+z-1;i++)
    {
        if(tp1>0&&a[i]==stk1[tp1])tp1--;
        else stk1[++tp1]=a[i];
    }
    for(int i=y;i<=y+z-1;i++)
    {
        if(tp2>0&&b[i]==stk2[tp2])tp2--;
        else stk2[++tp2]=b[i];
    }
    if(tp1!=tp2)return 0;
    for(int i=1;i<=tp1;i++)if(stk1[i]!=stk2[i])return 0;
    return 1;
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>q;
    for(int i=1;i<=n;i++)
    {
        char c;
        cin>>c;
        a[i]=c-'0';
    }
    for(int i=1;i<=n;i++)
    {
        char c;
        cin>>c;
        b[i]=c-'0';
    }
    while(q--)
    {
        int op,x,y,z;
        cin>>op;
        if(op==1)
        {
            cin>>x>>y;
            for(int i=x;i<=y;i++)a[i]^=1;
        }
        else if(op==2)
        {
            cin>>x>>y;
            for(int i=x;i<=y;i++)b[i]^=1;
        }
        else
        {
            cin>>x>>y>>z;
            cout<<check(x,y,z);
        }
    }
    return 0;
}

Subtask 5

结合 Subtask 3 和 Subtask 4 的做法,把栈用这个序列代替即可。

因为没有修改操作,所以这个子任务无需线段树,用前缀和即可实现。

时间复杂度 \(O(n)\)

Subtask 6 & Subtask 7

把 Subtask 5 的前缀和换成线段树维护即可过 Subtask 6 和 Subtask 7,时间复杂度 \(O(n \log n)\)

Subtask 6 留给一些奇怪复杂度做法和常数较大的正解。

代码如下:

#include <bits/stdc++.h>
#define fi first
#define se second
#define lc (p<<1)
#define rc ((p<<1)|1)
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi=pair<int,int>;
const int N=1000005;
int n,q,a[2][N];
struct Node{
    int l,r,v,tag;
};
struct Segtree{
    Node tr[4*N];
    void pushup(int p)
    {
        tr[p].v=tr[lc].v+tr[rc].v;
    }
    void pushdown(int p)
    {
        if(tr[p].tag)
        {
            tr[lc].tag^=1;
            tr[rc].tag^=1;
            tr[lc].v=-tr[lc].v;
            tr[rc].v=-tr[rc].v;
        }
        tr[p].tag=0;
    }
    void build(int p,int ln,int rn,int id)
    {
        tr[p]={ln,rn,a[id][ln],0};
        if(ln==rn)return;
        int mid=(ln+rn)>>1;
        build(lc,ln,mid,id);
        build(rc,mid+1,rn,id);
        pushup(p);
    }
    void update(int p,int ln,int rn)
    {
        if(ln<=tr[p].l&&tr[p].r<=rn)
        {
            tr[p].tag^=1;
            tr[p].v=-tr[p].v;
            return;
        }
        pushdown(p);
        int mid=(tr[p].l+tr[p].r)>>1;
        if(ln<=mid)update(lc,ln,rn);
        if(rn>=mid+1)update(rc,ln,rn);
        pushup(p);
    }
    int query(int p,int ln,int rn)
    {
        if(ln<=tr[p].l&&tr[p].r<=rn)return tr[p].v;
        pushdown(p);
        int mid=(tr[p].l+tr[p].r)>>1;
        if(rn<=mid)return query(lc,ln,rn);
        if(ln>=mid+1)return query(rc,ln,rn);
        return query(lc,ln,rn)+query(rc,ln,rn);
    }
}tr1,tr2;
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);
    cout.tie(0);
    cin>>n>>q;
    for(int i=1;i<=n;i++)
    {
        char c;
        cin>>c;
        if((c-'0')^(i&1))a[0][i]=1;
        else a[0][i]=-1;
    }
    for(int i=1;i<=n;i++)
    {
        char c;
        cin>>c;
        if((c-'0')^(i&1))a[1][i]=1;
        else a[1][i]=-1;
    }
    tr1.build(1,1,n,0);
    tr2.build(1,1,n,1);
    while(q--)
    {
        int op,x,y,z;
        cin>>op;
        if(op==1)
        {
            cin>>x>>y;
            tr1.update(1,x,y);
        }
        else if(op==2)
        {
            cin>>x>>y;
            tr2.update(1,x,y);
        }
        else
        {
            cin>>x>>y>>z;
            int tmp=1;
            if((x&1)!=(y&1))tmp=-1;
            if(tr1.query(1,x,x+z-1)==tr2.query(1,y,y+z-1)*tmp)cout<<1;
            else cout<<0;
        }
    }
    return 0;
}

感觉这题主要的难点在于想到用栈维护这样的操作,然后再运用奇偶位反转这个 trick 去加速这个过程,线段树是不难的。最后给这题被卡常的人切个腹,没想到核桃最近的机子跑这么慢/kk。

posted @ 2025-08-11 00:21  KS_Fszha  阅读(34)  评论(0)    收藏  举报