【训练题54:想法 + 权值线段树】Array | HDU7020 | 杭电多校五 09题

杂谈

  • 啊这,这题洛谷居然有原题 Array | HDU7020 | 杭电多校五 09题
    赛内想了很久,各种算奇怪的前缀和之类的
    最后调完了已经赛后半小时了,但是 T L E TLE TLE
    交了洛谷,发现洛谷是 7 s 7s 7s
    然后经过了各种卡常技巧的优化,特别是最后加了 i n l i n e inline inline 直接从 > 8000 M s >8000Ms >8000Ms 卡进 7000 + M s 7000+Ms 7000+Ms
    还有 O 2 O2 O2 加上,变成 6300 M s 6300Ms 6300Ms 了…

题意

  • Array | HDU7020 | 杭电多校五 09题
    给定一个长度为 N N N 的序列 A [ N ] A[N] A[N]
    求有多少个连续子序列 [ L , R ] [L,R] [L,R],满足这个子序列中只有一个众数
    众数:序列中出现次数最多的数
  • 1 ≤ N ≤ 1 0 6 1\le N\le 10^6 1N106
    0 ≤ A [ i ] ≤ 1 0 6 0\le A[i]\le 10^6 0A[i]106

思路

  • 两种思路:
    第一种,固定右端点,求多少个满足要求的左端点。然后我发现我搞不定
    第二种,固定某种数字,不断去处理有哪些(多少)区间满足区间的众数是ta
    第二种貌似是有戏的

初步尝试

  • 假设我们现在考虑数字 a a a ,假设其他非 a a a 的数字,我们记作 b b b ,于是变成了一个 a b ab ab
    因为要求区间的众数,很套路的,目前位置为 i i i ,我们记数字 a a a前缀和 p [ i ] p[i] p[i]
    于是合法区间就变成了如下式子:
    p [ R ] − p [ L − 1 ] > R − L + 1 2 p[R]-p[L-1]>\frac{R-L+1}{2} p[R]p[L1]>2RL+1
    可能我们会去从这个角度出发,转化式子为:
    p [ R ] − R 2 > p [ L − 1 ] − L − 1 2 p[R]-\frac{R}{2}>p[L-1]-\frac{L-1}{2} p[R]2R>p[L1]2L1
    然后我们线段树记录 p [ i ] − i 2 p[i]-\frac{i}{2} p[i]2i ,去区间查询?
    然后发现 这 样 子 没 法 做 \color{red}{这样子没法做}
    这里我们必须固定区间右端点 R R R ,对于数字 a a a 才能算出有多少满足要求的左端点 L L L
    但是我们可能有 n n n 个不同的数字,每个数字的右端点都是无法固定的,都要暴力枚举过去
    也就是时间复杂度变成了 O ( n 2 log ⁡ n ) O(n^2\log n) O(n2logn),难定,比暴力还慢

深入尝试

  • 然后发现,如果我们去记录每个位置的值 是一定做不了的,我们就只能记录 每个值出现的次数 去做
    启发摩尔投票法
    我们希望算出 [ 1 , n ] [1,n] [1,n] 的众数,我们去记录一个
    如果 A [ i ] A[i] A[i] 等于栈顶的元素,或者栈为空,那么入栈一个 A [ i ] A[i] A[i]
    否则,出栈一个
    这样,最后的栈顶元素就是区间的众数
  • 考虑类似的做法。我们现在考虑数字 a a a
    如果 A [ i ] = a A[i]=a A[i]=a ,那么我们记 p r e [ i ] = p r e [ i − 1 ] + 1 pre[i]=pre[i-1]+1 pre[i]=pre[i1]+1
    否则, p r e [ i ] = p r e [ i − 1 ] − 1 pre[i]=pre[i-1]-1 pre[i]=pre[i1]1
    (注意,这里 p r e pre pre 数组并不是前缀和)
    举例:
    A [ ] = a , b , a , b , b , b , a , a , b p r e [ ] = 1 , 0 , 1 , 0 , − 1 , − 2 , − 1 , 0 , − 1 \begin{aligned} A[]=&a,b,a,b,b,b,a,a,b\\ pre[]=&1,0,1,0,-1,-2,-1,0,-1 \end{aligned} A[]=pre[]=a,b,a,b,b,b,a,a,b1,0,1,0,1,2,1,0,1
    考虑到,我们固定右端点为 R R R
    那么所有满足要求的左端点 L L L,都满足:
    p r e [ R ] − p r e [ L − 1 ] > 0 pre[R]-pre[L-1]>0 pre[R]pre[L1]>0
    等价于:
    p r e [ R ] ≥ p r e [ L − 1 ] − 1 pre[R]\ge pre[L-1]-1 pre[R]pre[L1]1
  • 但是刚刚说了,我们不能记录位置,而是去记录值的数量,所以我们改成权值线段树:
    p r e [ R ] ≥ p r e [ − I N F ] ∼ p r e [ R − 1 ] pre\Big[R\Big]\ge pre\Big[-INF\Big] \sim pre\Big[R-1\Big] pre[R]pre[INF]pre[R1]
    可以用线段树区间查询去获取有多少满足的数量 ∑ ( p r e [ − I N F ] ∼ p r e [ R − 1 ] ) \sum\Big( pre\Big[-INF\Big] \sim pre\Big[R-1\Big] \Big) (pre[INF]pre[R1])
  • 考虑更新:
    对于新的位置,我们只有 p r e [ R ] pre\Big[R\Big] pre[R] 增加 1 1 1,其他都不变
    所以我们可以单点更新,十分简单。
  • 但是刚刚也说了,我们如果对于所有的 R R R 全去做一遍,就变成了 O ( N 2 log ⁡ N ) O(N^2\log N) O(N2logN) 了,考虑优化
    首先,如果当前位置 A [ i ] = a A[i]=a A[i]=a,我们当然要固定右端点 R = i R=i R=i 然后去做
    但是如果当前位置 A [ i ] = b A[i]=b A[i]=b,我们可能也要去固定右端点 R = i R=i R=i 然后去做
    比如 A [ ] = a a b b A[]=aabb A[]=aabb
    R = 3 R=3 R=3,当然有合法的: [ a a b ] [aab] [aab]
    但是 R = 4 R=4 R=4 就没有合法的区间了
    这个时候问题来了:哪些位置我们需要去固定当前位置作为右端点去查询?
    一个很简单的想法就是:当当前位置作为右端点,且有满足的左端点,满足区间的众数为 a a a 的嘛!
    也就是满足
    p r e [ R ] ≥ p r e [ − I N F ] ∼ p r e [ R − 1 ] , ∃ p r e [ k ] ≠ 0 pre\Big[R\Big]\ge pre\Big[-INF\Big] \sim pre\Big[R-1\Big]\quad ,\exist pre\Big[k\Big]\ne 0 pre[R]pre[INF]pre[R1],pre[k]=0
    我们只需要记录最小的 p r e [ k ] = m i n pre\Big[k\Big]=min pre[k]=min ,看是否满足 p r e [ R ] ≥ m i n pre\Big[R\Big]\ge min pre[R]min 即可
    举例:
    A [ ] = a , a , b , b , b , b , b , b , a p r e [ ] = 1 , 2 , 1 , 0 , − 1 , − 2 , − 3 , − 4 , − 3 A[]=a,a,b,b,b,b,b,b,a\\ pre[]=1,2,1,0,-1,-2,-3,-4,-3 A[]=a,a,b,b,b,b,b,b,apre[]=1,2,1,0,1,2,3,4,3
    注意到当某个 b b b 的位置 i i i 满足 p r e [ i ] < 0 pre[i]<0 pre[i]<0 时,就没有满足要求的了,这些点都不用作为右端点去获取值
  • 还有我们注意到,我们不能对于每个值 a a a,对于每个位置 i i i 我们都去单点更新 p r e [ ] pre[] pre[] 数组
    考虑到上面中间一连串 b b b 的位置,我们的 p r e [ ] pre[] pre[] 数组中,数字从 2 2 2 降到 − 4 -4 4
    所以我们只需要区间更新 u p d a t e [ − 4 , 2 ] update[-4,2] update[4,2] 区间,赋值 + 1 +1 +1 即可快速记录所有的 p r e [ A [ ] ] pre[A[]] pre[A[]] 的值
  • 根据摊还分析 a a a 使势能提高 1 1 1 b b b 使势能降低 1 1 1 ,我们 u p d a t e update update 的次数自然是 2 × c n t a 2\times cnt_a 2×cnta
    因为只有当 势能 ≥ m i n \ge min min 的时候我们才去更新

卡常小技巧

  • 思路差不多到这里,但是写着时间复杂度仍然会非常爆炸
    首先,最多有 n n n 个不同的数字,因为我们记录的是 p r e [ R ] pre\Big[R\Big] pre[R] ,自然会有负数,我们增加一个 O R I ORI ORI 偏移常量
    对于每一个不同的数字,我们的 p r e [ ] pre[] pre[] 数字都要清空,这样貌似清空的总体复杂度就是 O ( n 2 ) O(n^2) O(n2) 了?
  • 简单优化:我们使用 o p [ i ] op[i] op[i] 去记录第 i i i 个更新的操作区间为 [ L i , R i ] [L_i,R_i] [Li,Ri],操作完某个数之后我们对于每一个更新的区间全都反向赋值,原来 + 1 +1 +1 赋值变成 − 1 -1 1
    这样清空复杂度: O ( n 2 ) → O ( n k log ⁡ n ) O(n^2)\rightarrow O(nk\log n) O(n2)O(nklogn)
    但是仍然很慢
  • 深度优化: 我们使用乘法线段树,即多做一个清空标记,即若想清空复杂度,就区间 ∗ 0 *0 0 ,只需要一次 u p d a t e update update 就好了,快很多: O ( n 2 ) → O ( n log ⁡ n ) O(n^2)\rightarrow O(n\log n) O(n2)O(nlogn)
  • 致命优化: 线段树我们加上 i n l i n e inline inline ,直接洛谷快 1 s 1s 1s H D U HDU HDU T L E → A C \color{red}{TLE}\rightarrow \color{green}{AC} TLEAC
    或者加上 #pragma GCC optimize(2) ,也可以快很多
    对了,应该没有人不加快读的吧

代码

  • 时间复杂度: O ( n log ⁡ n ) O(n\log n) O(nlogn)
#pragma GCC optimize(2)
#include<bits/stdc++.h>
using namespace std;
void show(){std::cerr << endl;}template<typename T,typename... Args>void show(T x,Args... args){std::cerr << "[ " << x <<  " ] , ";show(args...);}
#define ls (p<<1)
#define rs (p<<1|1)
#define md ((l+r)>>1)
#define ll long long
const int MAX = 2e6+50;

int tree[MAX*4];
int tadd[MAX*4];
bool tmul[MAX*4];
inline void push_up(int p){
    tree[p]=tree[ls]+tree[rs];
}
inline void add(int p,int l,int r,int add,bool mul){
    if(mul){
        tree[p] = tree[p] + add * (r - l + 1);
        tmul[p] = tmul[p];
        tadd[p] = tadd[p]+ add;
    }else{
        tree[p] = add * (r - l + 1);
        tmul[p] = 0;
        tadd[p] = add;
    }
}
inline void push_down(int p,int l,int r){
    add(ls,l,md,tadd[p],tmul[p]);
    add(rs,md+1,r,tadd[p],tmul[p]);
    tadd[p] = 0;
    tmul[p] = 1;
}
inline void updateMul(int p,int l,int r,int ux,int uy,bool k){
    if(ux <= l && uy >= r){
        add(p,l,r,0,k);
        return;
    }
    push_down(p,l,r);
    if(ux <= md)updateMul(ls,l,md,ux,uy,k);
    if(uy >  md)updateMul(rs,md+1,r,ux,uy,k);
    push_up(p);
}
inline void updateAdd(int p,int l,int r,int ux,int uy,int k){
    if(ux <= l && uy >= r){
        add(p,l,r,k,1);
        return;
    }
    push_down(p,l,r);
    if(ux <= md)updateAdd(ls,l,md,ux,uy,k);
    if(uy >  md)updateAdd(rs,md+1,r,ux,uy,k);
    push_up(p);
}
inline int query(int p,int l,int r,int qx,int qy){
    int res = 0;
    if(qx <= l && r <= qy)return tree[p];
    push_down(p,l,r);
    if(qx <= md)res += query(ls,l,md,qx,qy);
    if(qy >  md)res += query(rs,md+1,r,qx,qy);
    return res;
}

int nxt[MAX];
int las[MAX];
bool M[MAX];
int hd[MAX];

const int YOU = 2e6+10;
const int ORI = 1e6+5;
int val[MAX];
int main()
{
    int T;T = read();
    //T = 1;
    updateAdd(1,0,YOU,ORI,ORI,1);
    while(T--){
        int n;n = read();
        //int tra;tra = read();
        int cnt = 0;
        for(int i = 1;i <= n;++i){
            val[i] = read();
            if(!M[val[i]]){
                hd[++cnt] = i;
                M[val[i]] = 1;
                las[val[i]] = 0;
            }
            nxt[las[val[i]]] = i;
            las[val[i]] = i;
            nxt[i] = n + 1;
        }
        ll ans = 0;
        for(int i = 1;i <= cnt;++i){
            int pos = 1;
            int shu = 0;
            if(pos < hd[i]){
                int x = hd[i] - 1;
                updateAdd(1,0,YOU,-x+ORI,-1+ORI,1);
                shu -= x;
            }
            pos = hd[i];
            int mn = min(shu,0);
            while(pos <= n){
                shu++;
                updateAdd(1,0,YOU,shu+ORI,shu+ORI,1);
                ans = ans + (ll)query(1,0,YOU,mn,shu+ORI-1);

                if(pos == n)break;
                if(nxt[pos] == pos+1){
                    pos ++;
                    continue;
                }
                int x = nxt[pos] - pos - 1;
                int xia = nxt[pos];
                int tmp = shu;
                for(int j = pos + 1;j <= xia - 1;++j){
                    shu--;
                    if(shu > mn){
                        ans = ans + (ll)query(1,0,YOU,mn,shu+ORI-1);
                    }else{
                        break;
                    }
                }
                updateAdd(1,0,YOU,tmp-x+ORI,tmp-1+ORI,1);
                pos = nxt[pos];
                shu = tmp - x;
                mn = min(mn,shu);
            }
            updateMul(1,0,YOU,0,YOU - 1,0);
            updateAdd(1,0,YOU,ORI,ORI,1);
        }
        for(int i = 1;i <= n;++i)
            M[val[i]] = 0;
        cout << ans << endl;
    }
    return 0;
}
  • 7
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值