1 条题解

  • 0
    @ 2025-10-16 13:32:39

    题解说明

    问题描述:
    给定两个长度为 nn 的数组 aabb,以及若干区间查询 [l,r][l,r]。要求对每个查询计算区间内的某种统计值(具体为 (a[i]b[i])\sum (a[i]\cdot b[i]) 的某种动态更新结果)。代码通过线段树 ++ 单调栈 ++ 离线处理实现高效查询。

    核心思路:
    1.1. 单调栈预处理:

    • 对数组 aa,计算 lasa[i]lasa[i]:表示 ii 左侧第一个比 a[i]a[i] 大的位置。
    • 对数组 bb,计算 lasb[i]lasb[i]:表示 ii 左侧第一个比 b[i]b[i] 大的位置。
    • 这两个数组用于确定在处理到位置 rr 时,哪些区间需要更新。

    2.2. 线段树维护信息:

    • 每个节点存储四类信息:
      • SS:区间的最终统计值
      • XYXY:区间内 a[i]b[i]a[i]\cdot b[i] 的和
      • XX:区间内 a[i]a[i] 的和
      • YY:区间内 b[i]b[i] 的和
    • 通过懒标记 TAGTAG 支持区间赋值和增量更新,保证更新效率。

    3.3. 更新操作:

    • 当处理到位置 rr 时:
      • 在区间 [lasa[r]+1,r][lasa[r]+1, r] 内,将 a[i]a[i] 统一设为 a[r]a[r]
      • 在区间 [lasb[r]+1,r][lasb[r]+1, r] 内,将 b[i]b[i] 统一设为 b[r]b[r]
      • 在区间 [1,r][1, r] 内,累加 a[i]b[i]a[i]\cdot b[i]
    • 这些操作通过线段树的懒标记实现。

    4.4. 查询处理:

    • 将所有查询按右端点 rr 分组存储。
    • 遍历 r=1..nr=1..n,每次完成上述更新后,回答所有右端点为 rr 的查询。
    • 查询时直接在线段树上取区间 [l,r][l,r]SS 值。

    算法流程:
    1.1. 输入 nn 和数组 a,ba,b
    2.2. 用单调栈分别计算 lasalasalasblasb
    3.3. 输入 qq 个查询,按右端点分组存储。
    4.4. 遍历 r=1..nr=1..n

    • 执行三次区间更新(更新 aa、更新 bb、累加 aba\cdot b)。
    • 回答所有右端点为 rr 的查询。
      5.5. 输出所有查询结果。

    复杂度分析:

    • 单调栈预处理:O(n)O(n)
    • 每个位置 rr 的更新操作:O(logn)O(\log n)(线段树区间更新)
    • 每个查询:O(logn)O(\log n)(线段树区间查询)
    • 总复杂度:O((n+q)logn)O((n+q)\log n),在 n,q5×105n,q \leq 5 \times 10^5 时可行。

    实现细节与注意点:

    • 使用 unsigned long longunsigned\ long\ long 存储结果,避免溢出。
    • 懒标记 TAGTAG 设计较复杂,需正确合并和下传。
    • 离线处理查询,保证每个查询只需一次线段树查询。
    • 单调栈保证 lasa/lasblasa/lasb 的计算为 O(n)O(n)

    总结:
    该算法结合了单调栈和线段树的优势:

    • 单调栈快速确定更新区间
    • 线段树支持复杂的区间赋值与增量操作
    • 离线处理保证查询高效
      整体实现复杂但思路清晰,能在大数据范围内高效回答区间查询。
    #include <bits/stdc++.h>
    using namespace std;
    
    // 类型定义与宏定义
    #define ull unsigned long long  // 无符号长整型,用于存储较大的计算结果
    #define mp make_pair           // 快速创建pair的宏
    #define pb push_back           // 向量尾部插入元素的宏
    #define ls (cur<<1)            // 线段树左孩子节点索引(cur*2)
    #define rs (cur<<1|1)          // 线段树右孩子节点索引(cur*2+1)
    #define mid ((l+r)>>1)         // 区间中点计算((l+r)/2)
    
    /**
     * 快速读入函数
     * 功能:比cin/fscanf更快地读取整数,适用于大量输入场景
     * 原理:直接读取字符流,过滤非数字字符后转换为整数
     */
    inline int read() {
        int x = 0;
        char ch = getchar();       // 从标准输入读入字符
    
        // 跳过非数字字符
        while (!isdigit(ch))
            ch = getchar();
    
        // 读取数字字符并转换为整数
        while (isdigit(ch))
            x = x * 10 + (ch ^ 48), ch = getchar();  // (ch^48)等价于(ch-'0')
    
        return x;
    }
    
    /**
     * 输出函数
     * 功能:将无符号长整型数输出到标准输出
     * 处理:将数字转换为字符串后反转(因为计算时是从低位到高位),再输出
     */
    void print(ull x) {
        if (x == 0) {
            cout << 0 << endl;
            return;
        }
        string s;
        while (x > 0) {
            s += (x % 10) + '0';  // 取出末位数字并转为字符
            x /= 10;
        }
        reverse(s.begin(), s.end());  // 反转得到正确顺序
        cout << s << endl;
    }
    
    // 常量与结构体定义
    const int N = 5e5 + 5;  // 最大数据规模(5*10^5 +5,预留空间)
    
    /**
     * 线段树节点结构体
     * 存储区间的统计信息:
     * S:区间的计算结果总和
     * XY:区间内a[i]*b[i]的总和
     * X:区间内a[i]的总和
     * Y:区间内b[i]的总和
     */
    struct node {
        ull S, XY, X, Y;
    } Tree[N << 2];  // 线段树数组(大小为4*N,满足区间划分需求)
    
    /**
     * 懒标记结构体(延迟更新标记)
     * 用于线段树的区间更新优化,存储需要传递的更新参数:
     * cx:a[i]的固定值(若不为0则表示区间a[i]统一设为cx)
     * cy:b[i]的固定值(若不为0则表示区间b[i]统一设为cy)
     * pxy:a[i]*b[i]的系数
     * px:a[i]的系数
     * py:b[i]的系数
     * p:常数项
     */
    struct TAG {
        ull cx, cy, pxy, px, py, p;
    } tag[N << 2];  // 懒标记数组
    
    // 全局变量
    int a[N], b[N];         // 存储原始数据数组
    int lasa[N], lasb[N];   // lasa[i]:i左侧第一个a值大于a[i]的位置;lasb[i]同理
    int n, q, T, sta[N];    // n:数据长度;q:查询次数;T:测试用例数;sta:单调栈数组
    ull Ans[N];             // 存储查询结果
    vector<pair<int, int>> Q[N];  // 离线查询存储:Q[r]存储所有右边界为r的查询(左边界l,查询id)
    
    /**
     * 线段树上传操作
     * 功能:用左右孩子节点的信息更新父节点信息
     */
    void pushup(int cur) {
        Tree[cur].S = Tree[ls].S + Tree[rs].S;    // 父节点S = 左孩子S + 右孩子S
        Tree[cur].X = Tree[ls].X + Tree[rs].X;    // 父节点X = 左孩子X + 右孩子X
        Tree[cur].Y = Tree[ls].Y + Tree[rs].Y;    // 父节点Y = 左孩子Y + 右孩子Y
        Tree[cur].XY = Tree[ls].XY + Tree[rs].XY; // 父节点XY = 左孩子XY + 右孩子XY
    }
    
    /**
     * 应用懒标记到当前节点
     * @param cur:当前节点索引
     * @param len:当前节点覆盖的区间长度
     * @param t:需要应用的懒标记
     * 功能:根据懒标记更新当前节点的统计信息,并合并到节点自身的懒标记中
     */
    void Add(int cur, int len, TAG t) {
        auto &[cx, cy, pxy, px, py, p] = t;              // 解包新标记
        auto &[Cx, Cy, Pxy, Px, Py, P] = tag[cur];       // 解包当前节点已有标记
    
        // 合并标记:根据当前节点已有标记的状态(是否有固定a/b值)计算新标记的影响
        if (Cx && Cy)  // 已有固定a和b值
            P += Cx * Cy * pxy + Cx * px + Cy * py + p;
        else if (Cx)   // 已有固定a值
            P += Cx * px + p, Py += Cx * pxy + py;
        else if (Cy)   // 已有固定b值
            P += Cy * py + p, Px += Cy * pxy + px;
        else           // 无固定a/b值
            P += p, Px += px, Py += py, Pxy += pxy;
    
        // 更新固定值(若新标记有固定值则覆盖)
        if (cx) Cx = cx;
        if (cy) Cy = cy;
    
        // 更新当前节点的统计信息
        auto &[S, XY, X, Y] = Tree[cur];
        S += XY * pxy + X * px + Y * py + len * p;  // 累加新标记对S的影响
    
        // 根据新标记的固定值更新XY、X、Y
        if (cx && cy)  // 同时固定a和b
            XY = cx * cy * len, X = cx * len, Y = cy * len;
        else if (cx)   // 仅固定a
            XY = cx * Y, X = cx * len;
        else if (cy)   // 仅固定b
            XY = cy * X, Y = cy * len;
    }
    
    /**
     * 线段树下传操作
     * @param cur:当前节点索引
     * @param len1:左孩子覆盖的区间长度
     * @param len2:右孩子覆盖的区间长度
     * 功能:将当前节点的懒标记传递给左右孩子,实现延迟更新
     */
    void pushdown(int cur, int len1, int len2) {
        // 若当前节点有标记,则下传给孩子
        if (tag[cur].cx || tag[cur].cy || tag[cur].pxy || tag[cur].px || tag[cur].py || tag[cur].p) {
            Add(ls, len1, tag[cur]);  // 左孩子应用标记
            Add(rs, len2, tag[cur]);  // 右孩子应用标记
            tag[cur] = {0, 0, 0, 0, 0, 0};  // 清空当前节点标记
        }
    }
    
    /**
     * 线段树区间更新
     * @param cur:当前节点索引
     * @param l:当前节点覆盖的区间左边界
     * @param r:当前节点覆盖的区间右边界
     * @param ql:需要更新的区间左边界
     * @param qr:需要更新的区间右边界
     * @param t:更新使用的懒标记
     * 功能:对[ql, qr]区间应用标记t的更新
     */
    void update(int cur, int l, int r, int ql, int qr, TAG t) {
        // 若当前区间完全在更新区间内,直接应用标记
        if (ql <= l && r <= qr)
            return Add(cur, r - l + 1, t), void();
    
        // 下传标记,确保孩子节点信息正确
        pushdown(cur, mid - l + 1, r - mid);
    
        // 递归更新左/右孩子
        if (qr <= mid)
            update(ls, l, mid, ql, qr, t);
        else if (mid < ql)
            update(rs, mid + 1, r, ql, qr, t);
        else
            update(ls, l, mid, ql, qr, t), update(rs, mid + 1, r, ql, qr, t);
    
        // 上传更新父节点信息
        pushup(cur);
    }
    
    /**
     * 线段树区间查询
     * @param cur:当前节点索引
     * @param l:当前节点覆盖的区间左边界
     * @param r:当前节点覆盖的区间右边界
     * @param ql:查询区间左边界
     * @param qr:查询区间右边界
     * @return:区间[ql, qr]的S值总和
     */
    ull query(int cur, int l, int r, int ql, int qr) {
        // 若当前区间完全在查询区间内,直接返回当前节点的S值
        if (ql <= l && r <= qr)
            return Tree[cur].S;
    
        // 下传标记,确保孩子节点信息正确
        pushdown(cur, mid - l + 1, r - mid);
    
        // 递归查询左/右孩子并返回总和
        if (qr <= mid)
            return query(ls, l, mid, ql, qr);
        else if (mid < ql)
            return query(rs, mid + 1, r, ql, qr);
        else
            return query(ls, l, mid, ql, qr) + query(rs, mid + 1, r, ql, qr);
    }
    
    int main() {
        // 读取输入(测试用例数和数据长度)
        T = read(), n = read();
        int top = 0;  // 单调栈指针
    
        // 计算lasa数组:用单调栈找每个位置左侧第一个大于a[i]的元素位置
        for (int i = 1; i <= n; i++) {
            a[i] = read();
            // 维护单调栈(栈内元素a值递减):弹出所有小于当前a[i]的元素
            while (top && a[sta[top]] < a[i])
                top--;
            lasa[i] = sta[top];  // 栈顶即为左侧第一个更大元素的位置(0表示无)
            sta[++top] = i;      // 当前位置入栈
        }
    
        // 计算lasb数组:同理,找每个位置左侧第一个大于b[i]的元素位置
        top = 0;
        for (int i = 1; i <= n; i++) {
            b[i] = read();
            while (top && b[sta[top]] < b[i])
                top--;
            lasb[i] = sta[top];
            sta[++top] = i;
        }
    
        // 读取查询并离线存储(按右边界r分组)
        q = read();
        for (int i = 1, l, r; i <= q; i++) {
            l = read(), r = read();
            Q[r].pb(mp(l, i));  // 右边界为r的查询存到Q[r],记录左边界l和查询id
        }
    
        // 按右边界r递增处理,同时更新线段树并回答查询
        for (int r = 1; r <= n; r++) {
            // 更新区间[lasb[r]+1, r]:设置a[i] = a[r]
            update(1, 1, n, lasa[r] + 1, r, (TAG){a[r], 0, 0, 0, 0, 0});
            // 更新区间[lasb[r]+1, r]:设置b[i] = b[r]
            update(1, 1, n, lasb[r] + 1, r, (TAG){0, b[r], 0, 0, 0, 0});
            // 更新区间[1, r]:累加a[i]*b[i](pxy=1表示系数为1)
            update(1, 1, n, 1, r, (TAG){0, 0, 1, 0, 0, 0});
    
            // 回答所有右边界为r的查询
            for (auto [l, id] : Q[r])
                Ans[id] = query(1, 1, n, l, r);
        }
    
        // 输出所有查询结果
        for (int i = 1; i <= q; i++)
            print(Ans[i]);
    
        return 0;
    }
        
    
    • 1

    信息

    ID
    3169
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    2
    已通过
    1
    上传者