所有区间问题的套路

改题改题改题!!!做完题一定要好好改真的会继续两次NoNoNo继续n次遇见它!!!

C. 完美子图

线段树做法:考虑把一个区间向右拓展,新增加的一个位置对以前所有处理过的区间的影响,当然对自己这一个长度的影响是1,第一层循环枚举R,新加入的数能对过去的区间会造成影响只有两种情况:加入的新数成为了某段以i为右边界的区间的最大值或最小值,这个可以用单调栈来维护。

把图转成二维的之后问题就变成了max-min=r-l,但因为这是一个排列(代表没有重复),对于所有区间来说max-min>=r-l一定成立,所以合法的区间就是l+max-min的最小值=r,线段树上维护了l+max-min的最小值以及满足这个最小值的区间个数,建树的时候把 l 先放进去,长度只有1的时候max-min=0,正好是初始状态,更新时,(以最大值为例)新的最大值是a[i],被他影响到的上一个最大值是a[mx[t_mx]],虽然还可能有更多的被影响,但是要在每次出栈是更新因为“过去的最大值”变化了,而最大值的增量就是a[i]-a[mx[t_mx]],因为lazy放的是对最小值的更新所以当然不用*区间长度。

因为对于一个区间来说,max-min=r-l是它的最小情况,所以左右子树向上合并时,min相同才可能同时合法,min大的那个绝对不合法(已有比它小的它就不可能最小了),同样query的时候也不能把左右直接相加,分类讨论一下就好了。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 5e4 + 2;
const ll mod = 998244353;
const int INF = 2147483647;

int n, a[maxn], mx[maxn], mi[maxn], t_mx, t_mi;
ll ans;

inline int read()
{
    int x = 0, f = 1;
    char ch = getchar();
    while(ch > '9' || ch < '0')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

struct node 
{
    int ls, rs, cnt, min, lazy;
}tree[maxn<<2];
int tot;

#define lson tree[rt].ls
#define rson tree[rt].rs 

void pushup(int rt)
{
    if(tree[lson].min == tree[rson].min)
    {
        tree[rt].min = tree[lson].min; tree[rt].cnt = tree[lson].cnt+tree[rson].cnt;
        return;
    }
    if(tree[lson].min > tree[rson].min)
    {
        tree[rt].min = tree[rson].min; tree[rt].cnt = tree[rson].cnt;
        return;
    }
    tree[rt].min = tree[lson].min; tree[rt].cnt = tree[lson].cnt;
}

void pushdown(int rt)
{
    if(tree[rt].lazy)
    {
        int lz = tree[rt].lazy;
        tree[rt].lazy = 0;
        tree[lson].lazy += lz;
        tree[rson].lazy += lz;
        tree[lson].min += lz;
        tree[rson].min += lz;
    }
}

int build(int rt, int l, int r)
{
    if(!rt) rt = ++tot;
    if(l == r)
    {
        tree[rt].cnt = 1; tree[rt].min = l;
        return rt;
    }
    int mid = (l + r) >> 1;
    tree[rt].ls = build(lson, l, mid);
    tree[rt].rs = build(rson, mid+1, r);
    pushup(rt);
    return rt;
}

void update(int rt, int l, int r, int L, int R, int val)
{
    if(L <= l && r <= R)
    {
        tree[rt].lazy += val; tree[rt].min += val;
        return;
    }
    pushdown(rt);
    int mid = (l + r) >> 1;
    if(L <= mid) update(lson, l, mid, L, R, val);
    if(R > mid) update(rson, mid+1, r, L, R, val);
    pushup(rt);
}

struct node2 
{
    int id, v;
    node2(){}
    node2(int x, int y) {id = x, v = y;}
};

node2 query(int rt, int l, int r, int L, int R)
{
    if(L <= l && r <= R)
    {
        return node2(tree[rt].cnt, tree[rt].min);
    }
    pushdown(rt);
    int mid = (l + r) >> 1;
    node2 left, right;
    left.v = right.v = 1e9;
    if(L <= mid) left = query(lson, l, mid, L, R);
    if(R > mid) right = query(rson, mid+1, r, L, R);
    if(left.v < right.v) return left;
    if(right.v < left.v) return right;
    left.id += right.id;
    return left;
}

int main()
{
    n = read();
    for(int i=1; i<=n; i++)
    {
        int x = read(), y = read();
        a[x] = y;
    }
    tot = 1;
    int root = build(1, 1, n);
    for(int i=1; i<=n; i++)
    {
        while(t_mx > 0 && a[mx[t_mx]] < a[i])
        {
            update(root, 1, n, mx[t_mx-1]+1, mx[t_mx], a[i]-a[mx[t_mx]]);
            t_mx--;
        }
        mx[++t_mx] = i;
        while(t_mi > 0 && a[mi[t_mi]] > a[i])
        {
            update(root, 1, n, mi[t_mi-1]+1, mi[t_mi], a[mi[t_mi]]-a[i]);
            t_mi--;
        }
        mi[++t_mi] = i;
        node2 cat = query(1, 1, n, 1, i);
        if(cat.v == i) ans += (ll)cat.id;
    }

    printf("%lld\n", ans);
    
    return 0;
}
View Code

——感谢caorong

还可以用cdq分治:在合并的时候还是把区间想像成左右两部分,分类讨论一下:两个最值都在左; 都在右; min在左,max在右;min在右,max在左4种情况,也就是每次考虑跨过中点的区间答案,细节注释在了代码里。

#include <bits/stdc++.h>

using namespace std;

typedef unsigned long long ll;
const int maxn = 50003;
const ll mod = 998244353;
const int INF = 0x7ffffff;

int a[maxn], Min[maxn], Max[maxn], cnt[maxn<<1];
int ans, n;

inline int read()
{
    int x = 0, f = 1;
    char ch = getchar();
    while(ch > '9' || ch < '0')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

void solve(int l, int r)
{
    if(l == r) 
    {
        ans++; return;
    }
    int mid = (l + r) >> 1;
    solve(l, mid); solve(mid+1, r);
    Min[mid] = Max[mid] = a[mid];
    Min[mid+1] = Max[mid+1] = a[mid+1];

    for(int i=mid-1; i>=l; i--)
    {
        Min[i] = min(Min[i+1], a[i]);
        Max[i] = max(Max[i+1], a[i]);
    }
    for(int i=mid+2; i<=r; i++)
    {
        Min[i] = min(Min[i-1], a[i]);
        Max[i] = max(Max[i-1], a[i]);
    }
    //两个最值都在左区间,
    for(int i=mid; i>=l; i--)
    {
        int j=i+Max[i]-Min[i];//如果通过最值找到的右端点合法
        if(j<=r && j>mid && Max[i]>Max[j] && Min[i]<Min[j]) ans++;
    }
    for(int j=mid+1; j<=r; j++)
    {
        int i=j-Max[j]+Min[j];
        if(i>=l && i<=mid && Max[i]<Max[j] && Min[i]>Min[j]) ans++;
    }
    int j = mid+1, k = mid+1;
    for(int i=mid; i>=l; i--)//左小右大
    {
        while(j<=r && Min[j]>Min[i])//j满足左小,j是mid+1到右边界的区间,而不是右端点
        {
            cnt[Max[j]-j+n]++;//在干嘛?所以j是一个符合条件的区间,Max[j]-j==Min[i]-i
            j++;
        }
        //Max是单调的,越接近越可能会合法,和上面的Min不同,它反向了
        //j不变,因为Min[i]是区间最小值,只有可能变得更小或不变,而变得更小就使得j的范围更大
        while(k<j && Max[i]>Max[k])//为了避免重复,一定要满足右大,可是为什么要比j小?而且,k是右区间,
        //这是在清理重复的情况,把左大的删掉
        {
            cnt[Max[k]-k+n]--;
            k++;
        }
        //同上,k不需要清空
        ans += cnt[Min[i]-i+n];//为了满足区间长度的条件,上面存的只是可能合法的(左小右大的)
    }
    while(k<j)
    {
        cnt[Max[k]-k+n]--;//其实就是清空数组的意思,memset会TLE
        k++;
    }
    j = mid, k = mid;
    for(int i=mid+1; i<=r; i++)
    {
        while(j>=l && Min[i]<Min[j])
        {
            cnt[Max[j]+j]++;
            j--;
        }
        while(k>j && Max[k]<Max[i])
        {
            cnt[Max[k]+k]--;
            k--;
        }
        ans += cnt[Min[i]+i];//Max[i]-Min[j]==j-i,移项
    }
    while(k>j)
    {
        cnt[Max[k]+k]--;
        k--;
    }
}

int main()
{
    n = read();
    for(int i=1; i<=n; i++)
    {
        int x = read(), y = read();
        a[x] = y;
    }
    solve(1, n);
    printf("%d", ans);
    
    return 0;
}
View Code

 

B. 任意模数快速插值

未完待续……

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 5e5 + 2;
const ll mod = 998244353;
const int INF = 2147483647;
const int lim = 1e4 + 1;

int n, a[maxn], Max[maxn], Min[maxn], p[maxn];
ll ans, sum[maxn];

inline int read()
{
    int x = 0, f = 1;
    char ch = getchar();
    while(ch > '9' || ch < '0')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

void solve(int l, int r)
{
    if(l == r) 
    {
        ans = (ans+(ll)a[l]*a[l]%mod)%mod; return;
    }
    int mid = (l + r) >> 1;
    solve(l, mid); solve(mid+1, r);
    Max[mid] = Min[mid] = a[mid];
    Max[mid+1] = Min[mid+1] = a[mid+1];
    for(int i=mid-1; i>=l; i--)
    {
        Max[i] = max(Max[i+1], a[i]);
        Min[i] = min(Min[i+1], a[i]);
    }
    for(int i=mid+2; i<=r; i++)
    {
        Max[i] = max(Max[i-1], a[i]);
        Min[i] = min(Min[i-1], a[i]);
    }
    for(int i=mid,j=mid+1; i>=l; i--)
    {
        while(j<=r && Max[i]>=Max[j] && Min[i]<=Min[j]) j++;
        ans = (ans+(ll)Max[i]*Min[i]%mod*(j-mid-1)%mod)%mod;
    }
    for(int j=mid+1,i=mid; j<=r; j++)
    {
        while(i>=l && Max[i]<Max[j] && Min[i]>Min[j]) i--;
        ans = (ans+(ll)Max[j]*Min[j]%mod*(mid-i)%mod)%mod;
    }
    for(int i=mid,j=mid+1; i>=l; i--)
    {
        while(j<=r && Min[i]<=Min[j]) j++;
        p[i] = j;
    }
    sum[mid] = 0;
    for(int i=mid,j=mid+1; i>=l; i--)
    {
        while(j<=r && Max[i]>=Max[j]) 
        {
            sum[j] = (sum[j-1]+Min[j])%mod; j++;
        }
        if(j-1 >= p[i])//j-1是因为j在最后多减了一个
        {
            ans = (ans+(ll)Max[i]*(sum[j-1]-sum[p[i]-1])%mod)%mod;//sum[p[i]-1]是为了保留区间左端点
        }
    }
    for(int j=mid+1,i=mid; j<=r; j++)
    {
        while(i>=l && Min[i]>Min[j]) i--;
        p[j] = i;
    }
    sum[mid+1] = 0;
    for(int j=mid+1,i=mid; j<=r; j++)
    {
        while(i>=l && Max[i]<Max[j]) 
        {
            sum[i] = (sum[i+1]+Min[i])%mod; i--;
        }
        if(i+1 <= p[j])
        {
            ans = (ans+(ll)Max[j]*(sum[i+1]-sum[p[j]+1])%mod)%mod;
        }
    }
}

int main()
{
    n = read();
    for(int i=1; i<=n; i++)
    {
        a[i] = read();
    }
    solve(1, n);
    printf("%lld\n", (ans%mod+mod)%mod);
    
    return 0;
}
View Code

 

C. 有趣的区间问题

关于代码里指针的移动条件,由于mn1和mx1是由上一步的若干个a[p1]得到的,它起不到限制作用,所以可以删掉。但是下面这个不能删,因为用了一个"||"上一步满足的条件并不确定,就有必要取一下min和max了。

eq数组是现算的不需要用memset来更新,但是不要忘了eq[mid] = 0。

如果左边最小值小于右边最小值,以左边的最小值为标准找到右边最大值与它相等的个数。[mid+1, p1]是闭区间,p1-(mid+1)+1 = p1-mid. (p1, p2]左开右闭,左开的实现就是cnt相减的时候减没了左端点,右闭在指针移动时形成。(p2, r]左开右闭,把p2的前缀和去掉了。

//鹤的
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 1e6 + 5;
const int logV = 65;
const ll inf = 0x3f3f3f3f3f3f3f3f;

int n, cnt1[logV], cnt2[logV], cnt3[logV], cnt4[logV], eq[maxn];
ll a[maxn], ans;

#define Clear(a) memset(a, 0, sizeof(a))
#define popc __builtin_popcountll
//我知道我赛时为什么没分了!因为我用的是__builtin_popcount没开ll!!!

inline ll read()
{
    ll x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch ^ 48);
        ch = getchar();
    }
    return x * f;
}

void solve(int l, int r)
{
    if(l > r) return;
    if(l == r) {ans++; return;}
    int mid = (l + r) >> 1;
    Clear(cnt1), Clear(cnt2), Clear(cnt3), Clear(cnt4);
    eq[mid] = 0;
    //预处理中点必选,右区间的合法个数
    for(ll i=mid+1,mn=a[mid+1],mx=a[mid+1]; i<=r; i++,mn=min(mn,a[i]),mx=max(mx,a[i]))
    {
        eq[i] = eq[i-1] + (popc(mn) == popc(mx));
    }
    ll mn = inf, mx = -inf, mn1 = inf, mx1 = -inf, mn2 = inf, mx2 = -inf;
    //从大到小枚举左端点i,对于一个固定的i,在[m+1,r]之间一定会存在两个分界点p1,p2
    //把右区间分为3部分,分类讨论max和min都在那里取到
    //随着i递减,max和min都在左端/右端取到都会变难,p1,p2都是递增的
    //第1种和第3种的贡献:右端点用预处理的,左端点直接距离*合法性
    for(int i=mid,p1=mid,p2=mid; i>=l; i--)
    {
        mn = min(mn, a[i]); mx = max(mx, a[i]);
        while(p1<r && mn<=min(mn1,a[p1+1]) && mx>=max(mx1,a[p1+1]))
        {
            p1++; mn1 = min(mn1, a[p1]); mx1 = max(mx1, a[p1]);
            cnt1[popc(mn1)]++; cnt2[popc(mx1)]++;
        }
        while(p2<r && (mn<=min(mn2,a[p2+1])||mx>=max(mx2,a[p2+1])))//包含了第一种情况
        {
            p2++; mn2 = min(mn2, a[p2]); mx2 = max(mx2, a[p2]);
            cnt3[popc(mn2)]++; cnt4[popc(mx2)]++;
        }
        ans += (popc(mn)==popc(mx))*(p1-mid)+eq[r]-eq[p2];
        //讨论是max还是min在左区间取到
        if(mn<=mn2) ans += cnt4[popc(mn)]-cnt2[popc(mn)];//左端最小值小于右端最小值
        //最小值在左边取,最大值在右边取,用全部减掉最大值也在左边取的个数,就是最大值和最小值pop相等的数目
        else ans += cnt3[popc(mx)]-cnt1[popc(mx)];
        //关于我本来打算开一棵可持久化权值线段树来找个数这件事……
    }
    solve(l, mid); solve(mid+1, r);
}

int main()
{
    n = read();
    for(int i=1; i<=n; i++) a[i] = read();
    solve(1, n);
    printf("%lld\n", ans);
    
    return 0;
}
View Code

 

C. english

和上面的题大概类型不太一样,虽然也是所有区间,但主要是问题的转化和异或的应用,主要算法是01Trie

qwq

/*
好吧,下文的注释都是我自己的胡思乱想,一问题解的作者就发现全都想错了
*/
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 1e5 + 100;
const int mod = 1e9 + 7;
const int INF = 2147483647;

int L[maxn], R[maxn], st[maxn], top, trie[maxn*22][2], tot=1;
int rat[maxn*22], n_1[maxn][22], n_0[maxn][22], n, opt, A[maxn], root[maxn];

inline int read()
{
    int x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}
//对每一个区间维护一棵01Trie树 = 来一个可持久化Trie
inline void Insert(int a)
{
    int pre = root[a-1];
    root[a] = ++tot; int now = tot;
    for(int i=20; i>=0; i--)
    {
        if(pre) 
        {
            trie[now][0] = trie[pre][0]; trie[now][1] = trie[pre][1];
        }
        bool op = ((A[a]&(1<<i))>0);
        rat[now] = rat[pre] + 1;//rat[]'s use?一定会多1吗?
        //一定会多1,因为now是新建的节点,多加了一个数??大小关系怎么确定?
        //rat代表如果更大的话,即“子树的大小”,但这里的子树不是节点,而是now的左子树上有几个“边缘”
        //也就是从根到now这个节点代表的这一位都相同,在now的后一位更小了的数的数目
        //不过这大概是一种错误的理解,这样的话ans显然加多了???

        //只是一个点怎么会有大小之分??所以ret[now]存的是经过now这个点的数的个数
        trie[now][op] = ++tot;
        //同时转向子树:为了省空间,只开必要的节点,而不是新建一条链
        now = tot;
        if(pre) pre = trie[pre][op];
    }
    rat[now] = rat[pre] + 1;//那这里为什么要重复?
}

inline int Query(int now, int a, int b)//A[a]^? > A[b]满足条件的?有几个
{
    int ans = 0;
    for(int i=20; i>=0; i--)
    {
        bool x = ((A[a]&(1<<i))>0), y = ((A[b]&(1<<i))>0);
        if(!y)
        {
            ans = ans+rat[trie[now][x^1]]; now = trie[now][x];
        }
        else //在同一位上,y=1代表a已经没有了比b大的机会?但这只是拆开的单独一位啊
        {
            now = trie[now][x^1];
        }
        if(!now) break;
    }
    return ans;
}

int main()
{
    n = read(); opt = read(); root[0] = 1;
    for(int i=1; i<=n; i++)
    {
        A[i] = read();
        while(top > 0 && A[i]>=A[st[top]])
        {
            R[st[top]] = i-1; top--;
        }
        L[i] = st[top] + 1;
        st[++top] = i;
        if(opt == 1 || opt == 3)
        {
            for(int j=0; j<=20; j++)
            {
                //分别记录第j位为1和第j位为0的前缀和
                n_1[i][j] = n_1[i-1][j]; n_0[i][j] = n_0[i-1][j];
                if((1<<j)&A[i]) n_1[i][j]++;
                else n_0[i][j]++;
            }
        }
        if(opt == 2 || opt == 3)
        {
            Insert(i);
        }
    }
    A[n+1] = INF;
    while(top > 0 && A[n+1]>=A[st[top]])
    {
        R[st[top]] = n; top--;
    }
    ll ans1 = 0, ans2 = 0;
    for(int i=1; i<=n; i++)
    {
        if(i-L[i] < R[i]-i)
        {
            if(opt == 1 || opt == 3)
            {
                for(int j=L[i]; j<=i; j++)//不对啊,为什么两边可以同时取等?
                {
                    for(int k=0; k<=20; k++)
                    {
                        if((1<<k)&A[j]) //因为没有影响,i已经是相反的了
                        {
                            ans1 = (ans1+1ll*(n_0[R[i]][k]-n_0[i-1][k])*(1<<k)%mod*A[i]%mod)%mod;
                        }
                        else //它也取等一样解释??
                        {
                            ans1 = (ans1+1ll*(n_1[R[i]][k]-n_1[i-1][k])*(1<<k)%mod*A[i]%mod)%mod;
                        }
                    }
                }
            }
            if(opt == 2 || opt == 3)
            {
                for(int j=L[i]; j<=i; j++)
                {
                    //什么意思?这就是题解上说的启发式合并??
                    //右区间有多少个数满足 v^a[i] > a[x],x是最大值,i是循环的右区间
                    ans2 = (ans2+(ll)A[i]*(Query(root[R[i]], j, i)-Query(root[i-1], j, i)))%mod;
                }
            }
        }
        else 
        {
            if(opt == 1 || opt == 3)
            {
                for(int j=i; j<=R[i]; j++)
                {
                    for(int k=0; k<=20; k++)
                    {
                        if((1<<k)&A[j])
                        {
                            ans1 = (ans1+1ll*(n_0[i][k]-n_0[L[i]-1][k])*(1<<k)%mod*A[i]%mod)%mod;
                        }
                        else 
                        {
                            ans1 = (ans1+1ll*(n_1[i][k]-n_1[L[i]-1][k])*(1<<k)%mod*A[i]%mod)%mod;
                        }
                    }
                }
            }
            if(opt == 2 || opt == 3)
            {
                for(int j=i; j<=R[i]; j++)
                {
                    ans2 = (ans2+(ll)A[i]*(Query(root[i], j, i)-Query(root[L[i]-1], j, i)))%mod;
                }
            }
        }
    }
    if(opt == 1 || opt == 3) printf("%lld\n", ans1);
    if(opt == 2 || opt == 3) printf("%lld\n", ans2);
    
    return 0;
}
View Code

 

posted @ 2022-09-28 08:30  Catherine_leah  阅读(36)  评论(0编辑  收藏  举报
/* */