洛谷 P1486 [NOI2004] 郁闷的出纳员(splay)

题目描述

        OIER 公司是一家大型专业化软件公司,有着数以万计的员工。作为一名出纳员,我的任务之一便是统计每位员工的工资。这本来是一份不错的工作,但是令人郁闷的是,我们的老板反复无常,经常调整员工的工资。如果他心情好,就可能把每位员工的工资加上一个相同的量。反之,如果心情不好,就可能把当前在公司的所有员工的工资扣除一个相同的量。我真不知道除了调工资他还做什么其它事情。

        工资的频繁调整很让员工反感,尤其是集体扣除工资的时候,一旦某位员工发现自己的工资已经低于了合同规定的工资下界,他就会立刻气愤地离开公司,并且再也不会回来了。每位员工的工资下界都是统一规定的。每当一个人离开公司,我就要从电脑中把他的工资档案删去,同样,每当公司招聘了一位新员工,我就得为他新建一个工资档案。

        老板经常到我这边来询问工资情况,他并不问具体某位员工的工资情况,而是问现在工资第 𝑘k 多的员工拿多少工资。每当这时,我就不得不对数万个员工进行一次漫长的排序,然后告诉他答案。

        好了,现在你已经对我的工作了解不少了。正如你猜的那样,我想请你编一个工资统计程序。怎么样,不是很困难吧?

        如果某个员工的初始工资低于最低工资标准,那么将不计入最后的答案内。

输入格式

第一行有两个整数 𝑛 和 min⁡。𝑛n 表示下面有多少条命令,min⁡ 表示工资下界。

接下来的 𝑛 行,每行一个字符 𝑥 和一个整数 𝑘,表示一条命令。命令可以是以下四种之一:

  • I k 新建一个工资档案,初始工资为 𝑘。如果某员工的初始工资低于工资下界,他将立刻离开公司。
  • A k 把每位员工的工资加上 𝑘。
  • S k 把每位员工的工资扣除 𝑘。
  • F k 查询第 𝑘 多的工资。

在初始时,可以认为公司里一个员工也没有。

输出格式

对于每条 F 命令,你的程序要输出一行,仅包含一个整数,为当前工资第 𝑘 多的员工所拿的工资数,如果 𝑘大于目前员工的数目,则输出 −1。

输出的最后一行包含一个整数,为离开公司的员工的总数。

请注意,初始工资低于工资下界的员工不算做离开公司的员工。

输入输出样例

输入 #1复制

9 10
I 60
I 70
S 50
F 2
I 30
S 15
A 5
F 1
F 2

输出 #1复制

10
20
-1
2

说明/提示

数据规模与约定

对于全部的测试点,保证:

  • I 命令的条数不超过 1e5;
  • A 和 S 命令的总条数不超过 100;
  • F 命令的条数不超过 1e5;
  • 每次工资调整的调整量不超过 1e3;
  • 新员工的工资不超过 1e5。
  • 0≤𝑛≤3×1e5,0≤min≤1e9,输入的所有数字均在 32 位带符号整形范围内。

本题有以下几种情况:

        (1)招一个员工

        (2)给所有员工同时涨薪(+k)

        (3)给所有员工同时降薪(-k)

        (4)在剩余员工中找到工资第k大数

        对于2,3两种情况,我们设置一个变量delta,每次涨薪,降薪的时候我们都在deltag上操作,那么每个节点的当前价值加上delta就是实际价值,每次降薪之后,我们需要找到>=min-delta的最小值的工资的节点,然后把这个节点之前的所有节点都删除(不包括L节点)。也就是每次删除的时候都是删除前缀。

        与上一个题的最大的区别就是本题每个节点的价值就是节点的价值,上一个题每个节点的价值就是下标。

        代码如下:

#include<bits/stdc++.h>
using namespace std;
const int N=100010,INF=1e9;
int n,m,delta;

struct Node
{
    int s[2],p,v;
    int size;
    
    void init(int _v,int _p)
    {
        v=_v;
        p=_p;
        size=1;
    }
}tr[N];
int root,idx;

void pushup(int x)
{
    tr[x].size=tr[tr[x].s[0]].size+tr[tr[x].s[1]].size+1;
}

void rotate(int x)//左旋右旋操作
{
    int y=tr[x].p;//y是x的父亲
    int z=tr[y].p;//z是y的父亲
    int k=tr[y].s[1]==x;//k==1是x是y的右儿子,否则x是y的左儿子
    
    tr[z].s[tr[z].s[1]==y]=x;
    tr[x].p=z;
    tr[y].s[k]=tr[x].s[k^1];
    tr[tr[x].s[k^1]].p=y;
    tr[x].s[k^1]=y;
    tr[y].p=x;
    
    pushup(y);
    pushup(x);
}

void splay(int x,int k)
{
    while(tr[x].p!=k)
    {
        int y=tr[x].p,z=tr[y].p;
        if(z!=k)
            if((tr[y].s[1]==x)^(tr[z].s[1]==y))//折线
                rotate(x);
            else
                rotate(y);
        rotate(x);
    }
    if(!k)
        root=x;
}

int insert(int v)
{
    int u=root,p=0;
    while(u)
    {
        p=u;
        u=tr[u].s[v>tr[u].v];
    }
    u=++idx;
    if(p)
        tr[p].s[v>tr[p].v]=u;
        tr[u].init(v,p);
        splay(u,0);

    return u;
}

int get(int v)//大于等于某一个数的最小的节点编号
{
    int u=root,res;
    while(u)
    {
        if(tr[u].v>=v)
        {
            res=u;
            u=tr[u].s[0];
        }
        else
            u=tr[u].s[1];
    }
    return res;
}

int get_k(int k)//返回第k小的数
{
    int u=root;
    while(u)
    {
        if(tr[tr[u].s[0]].size>=k)
            u=tr[u].s[0];
        else if(tr[tr[u].s[0]].size+1==k)
            return tr[u].v;
        else 
        {
            k-=tr[tr[u].s[0]].size+1;
            u=tr[u].s[1];
        }
    }
    return -1;
}

int main()
{
    scanf("%d%d",&n,&m);
    int L=insert(-INF),R=insert(INF);//左右端点
    
    int tot=0;//表示总人数
    while(n--)
    {
        char op[2];
        int k;
        scanf("%s%d",op,&k);
        if(op[0]=='I')//来了一个新员工
        {
            if(k>=m)
            {
                k-=delta;//k+delta表示真实工资,所以这里k减去delta
                insert(k);
                tot++;
            }
        }
        else if(op[0]=='A')//所有人工资加上k
        delta+=k;
        else if(op[0]=='S')//所有人工资减去k
        {
            delta-=k;
            R=get(m-delta);//找到大于等于m-delta的最小的节点编号
            splay(R,0);
            splay(L,R);
            tr[L].s[1]=0;//删除(将L的右子树清空)
            
            pushup(L);
            pushup(R);
        }
        else//输出第k多的工资
        {
            if(tr[root].size-2<k)//k>当前员工数量
                puts("-1");
            else//第k+1大的数是第tr[root].size-k小的数(包含哨兵节点)
                printf("%d\n",get_k(tr[root].size-k)+delta);
        }
        
    }
    printf("%d\n",tot-(tr[root].size-2));//离开公司的员工总数
    
    
    return 0;
}

<think>嗯,用户想了解如何用分块方法解决P2042这个维护数列的问题。首先,我需要回忆一下分块算法的基本概念。分块通常是将数据分成多个块,每个块单独处理,平衡查询和修改的时间复杂度。不过,题目是NOI2005的,可能比较复杂,因为维护数列通常涉及插入、删除、区间修改、翻转、求和、最大子段和等操作。分块处理这些操作可能比较麻烦,尤其是动态的插入和删除,会导致块的大小变化,影响效率。 首先,我得确认分块是否适合这个问题。一般来说,分块的时间复杂度是O(n√n),对于大数可能还行,但像这种NOI题目可能数据量较大,分块可能不够高效,不过用户明确问分块方法,所以需要找到如何结构设计。 维护数列的典型操作包括:在某个位置插入一段数列,删除某个区间,区间覆盖,区间翻转,区间求和,求最大子段和。分块的话,每个块需要维护这些操作的信息。比如每个块需要记录翻转标记、覆盖标记,以及各种区间和、最大子段和等。 接下来,分块的结构设计。每个块可以存储一个数组,同时维护块内的总和、最大前缀和、最大后缀和、最大子段和,以及翻转和覆盖的懒标记。插入和删除操作需要考虑块的分裂和合并,这可能比较复杂,因为频繁的插入删除会导致块的大小不均,影响效率。所以可能需要设置块的大小阈值,比如当块的大小超过2√n时分裂,小于√n/2时合并相邻块。 处理翻转操作时,每个块维护一个翻转标记,当需要翻转区间时,对覆盖的完整块标记翻转,并处理部分块。同样,覆盖操作需要懒标记,当块被完全覆盖时,直接更新块内的值,并记录覆盖值。 最大子段和的计算需要每个块维护内部的最大子段和、最大前缀和、最大后缀和,这样在查询时,可以将多个块的信息合并起来得到整体的最大子段和。这可能比较复杂,需要考虑块之间的前后缀如何组合。 插入和删除操作需要找到对应的位置所在的块,如果插入导致块过大,就分裂成两个块。删除时如果块变得过小,可能需要合并相邻块。这些操作需要维护块之间的链表结构,方便快速插入和删除块。 举个例子,当要在位置pos插入一段数列,首先找到pos所在的块,如果块的大小加上插入元素后超过阈值,就分裂该块为两个,然后将插入的元素作为新块插入到链表中。这样插入的时间复杂度是O(√n),因为分裂和插入块的时间与块的大小有关。 对于翻转和覆盖操作,应用懒标记是关键,这样可以延迟实际的操作,直到需要访问块的具体内容时才进行实际的处理。例如,当需要访问一个带有翻转标记的块时,先执行翻转操作,并清除标记。 不过分块在处理这些动态操作时,尤其是频繁的插入和删除,可能效率不如平衡树结构(如Splay或Treap),但分块实现起来可能相对简单一些。需要权衡实现复杂度和效率。 总结一下,分块实现的关键点包括: 1. 分块的结构设计,每个块维护必要的信息和懒标记。 2. 插入和删除时的块分裂与合并策略。 3. 懒标记的处理,如翻转和覆盖,何时下传。 4. 查询操作时如何合并各个块的信息,如最大子段和。 可能遇到的问题包括块的大小管理不当导致性能下降,懒标记处理错误导致数据不一致,以及在合并块信息时逻辑错误。需要详细设计每个操作的步骤,并处理各种边界情况,比如操作区间跨多个块,或者在块的开头或结尾的部分操作。 最后,参考用户提供的引用,尤其是引用[1]提到递归图的理解,可能分块的递归处理不太相关,但引用[2]提到题目,可能需要参考类似题解中的分块实现方法。不过用户提供的引用可能没有直接相关的内容,所以需要依靠已有的分块知识来构建解决方案。</think>### 分块方法实现 P2042 维护数列问题 #### 分块结构设计 1. **块内信息**: - 数据存储:每个块存储$size \leq 2\sqrt{n}$个元素 - 维护以下字段: ```text sum: 区间和 lmax: 最大前缀和 rmax: 最大后缀和 tmax: 最大子段和 rev: 翻转标记(0/1) cov: 覆盖标记(特殊值表示未覆盖) ``` 2. **分块逻辑**: - 块间通过链表或数组连接 - 插入/删除时动态调整块大小,触发分裂或合并(例如块大小超过$2\sqrt{n}$时分裂,相邻块总大小低于$\sqrt{n}$时合并) #### 核心操作实现 **1. 区间翻转(REVERSE)**: - 对完整覆盖的块:翻转标记`rev ^= 1`,交换`lmax`与`rmax` - 对部分覆盖的块:下传标记后暴力翻转元素 时间复杂度:$O(\sqrt{n})$ **2. 区间覆盖(MAKE-SAME)**: - 对完整覆盖的块:更新`cov`标记,直接计算`sum`,`lmax`,`rmax`,`tmax` - 对部分覆盖的块:下传标记后暴力修改 时间复杂度:$O(\sqrt{n})$ **3. 最大子段和(MAX-SUM)**: - 合并相邻块的`rmax`和`lmax`,遍历所有块维护全局最大值 时间复杂度:$O(\sqrt{n})$ **4. 插入(INSERT)**: - 定位插入位置所在块,若插入后块大小超过阈值则分裂 时间复杂度:$O(\sqrt{n} + k/\sqrt{n})$ **5. 删除(DELETE)**: - 移除目标区间涉及的所有块,合并相邻小块 时间复杂度:$O(\sqrt{n} + k/\sqrt{n})$ #### 代码框架示例(关键部分) ```python class Block: def __init__(self): self.data = [] self.sum = 0 self.lmax = self.rmax = self.tmax = -inf self.rev = 0 self.cov = None def push_down(self): # 下传覆盖/翻转标记到数据层 if self.cov is not None: self.data = [self.cov] * len(self.data) if self.rev: self.data.reverse() self.rev = 0 self.cov = None def update_info(self): # 重新计算sum/lmax/rmax/tmax pass class BlockList: def __init__(self): self.blocks = [] def split(self, idx): # 分裂过大的块 pass def merge(self): # 合并过小的相邻块 pass ``` #### 复杂度分析 | 操作类型 | 时间复杂度 | |----------------|---------------------| | 插入/删除 | $O(\sqrt{n} + k)$ | | 翻转/覆盖/求和 | $O(\sqrt{n})$ | | 最大子段和 | $O(\sqrt{n})$ | #### 注意事项 1. **标记下传策略**:在访问块内数据前必须下传所有标记 2. **块大小平衡**:通过动态分裂/合并保证$size \in [\sqrt{n}/2, 2\sqrt{n}]$ 3. **边界处理**:特别注意区间跨多个块时的部分覆盖情况
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值