NKOJ-3776 工资管理

P3776工资管理
时间限制 : - MS 空间限制 : 165536 KB
评测说明 : 1000ms
问题描述

何老板的公司有n名员工,编号1到n。一开始所有员工的工资都是0。根据何老板的心情好坏,可能出现下列两种针对员工工资的操作:
1.U x y 改工资操作:何老板将第x号员工的工资改成了y; 
2.Z x y 减工资操作:何老板生气了,他想选出x个员工,并将他们的工资全都减去1。何老板想知道,他能否一口气进行y次这样的减工资操作。能输出TAK,否则输出NIE。注意,员工的工资不能为负。

对于每个减工资的操作,何老板只是在心里想想,口头上说说,吓唬吓唬大家,解解闷气,他并不会真正执行。即不会对任何人的工资进行修改。

输入格式

第一行包含两个正整数n,m,分别表示员工的人数和操作次数。 
接下来m行,每行一个操作,形式如题面所述。

输出格式

包含若干行,对于每个减工资操作,若可行,输出TAK,否则输出NIE。

样例输入 1

3 8
U 1 5
U 2 7
Z 2 6
U 3 1
Z 2 6
U 2 2
Z 2 6
Z 2 1 

样例输出 1

NIE
TAK
NIE
TAK

样例输入 2

13 17
U 1 12
Z 1 9
Z 1 5
Z 4 7
U 7 18
Z 1 1
Z 1 8
U 6 4
U 1 9
U 3 13
Z 5 2
U 7 8
U 4 20
U 7 14
Z 6 1
Z 3 2
Z 8 7

样例输出 2

TAK
TAK
NIE
TAK
TAK
NIE
NIE
TAK
NIE

提示

对于30%的数据:1<=n,m<=1000
对于100%的数据:1<=n,m<=200000  1<=x<=n,0<=y<=10^9,1<=y<=10^9。

来源 改编自POI2015 Logistyka

没想到老板的公司竟然有高达20万个员工

题解

思路

分析下题意

对于这道题目,我们大概可以分析出

当扣钱次数为y次时,一个人最多被扣掉y块钱
所以对于工资>y的人,他最多也就只能被扣y块钱(最多被扣y次)
对于工资<=y时,他最多就被扣完

而且对于一个扣款y,因为它是分y次扣的,所以可以被扣在多个人头上(这个结论应该是很容易想到的,不作证明)

所以我们就可以分析出结果

对于工资大于y的人,我们记录他被扣的钱为y元
而工资小于y的人,我们就把他的钱扣完

举例

假设修改后工资为
1 2 5 7 8 6 3 2 1

如果老板的要求是,5个人,扣4次
所以一共是扣20块钱
但由于5 7 8 6 大于4,所以这四个都算作4==>和为16
而剩下的,可以由1,2,3(工资额)一起分担
所以,根据上面的结果来看,这个情况就是
    4*4(工资大于4的人)+1+2+3+2+1>4*5
所以可行

换个情况
现在的工资总和为35
所以我们分两种情况看

①6个人,扣5次==>和为30
    那么一共有四个人可以扣5次
    这四个人的工资为(5 7 8 6)==>和为20
    而剩下的人工资为(1 2 3 2 1)==>和为9
    4*5+1+2+3+2+1<5*6
 这种情况不行

②4个人,扣8次==>和为32
    那么可以扣8次的人只有一个
    而其余的人,工资就要全部被扣光
    所以
        8*1+1+2+5+7+6+3+2+1>32
    所以这是可以的

为什么下面这个扣款之和比上面的大,为什么下面的可以上面的却不可以呢??
请结合上面的分析进行考虑。

解法

这道题目有两种解法,一种是线段树,一种是树状数组

先说线段树

这道题目一开始我是用线段树做的
线段树就很简单了,基本上就是一个模板题

先把线段树画出来,状态定义就是
对于线段[l,r],表示的就是l号员工到r号员工的状态
我们记录[l,r]的工资最大值和最小值

那么对于扣款次数y,如果[l,r]的最小值大于等于y,说明[l,r]内所有的人的工资不少于y
所以这个区间可以被扣(r-l+1)*y块钱

而对于[l,r]的最小值小于y,且最大值也小于y,就说明[l,r]内所有的人都不能够被扣y次
换句话说,这些人的钱要全部被扣完
所以对这个区间我们选择求和

对于[l,r]的最小值小于y,最大值大于y
我们选择进行进一步的细分讨论(再把区间二分)

上面的过程其实就是一个简单的求和,唯一需要注意的是工资大于y的人在求和时只能+y

但是由于数据太过庞大,所以怎么求都要超时,这种做法我们不提倡

树状数组

那么树状数组我们就不能简单地学习上面的做法了
因为如上述做法所述
对于工资大于y的人,我们只能够在求和时+y
所以树状数组的简单求和显然是不行的

换个思路考虑,我们如果有办法快速地知道有多少人的工资是大于y的,有多少人的工资是小于y的,并且能够快速求小于y的人的工资之和,那么这样也解决了相同的问题。

下面我们就来依次解决上述问题

①快速求人数
首先想到的肯定是记录某个工资对应的人数
但是由于工资的范围在[0,10^9],所以这样肯定是要爆空间
解决办法
我们可以记录每个人工资的大小位置,然后对位置进行叠加
举例

假设每个人的工资为1 2 7 8 5 9
那么工资的大小排序1 2 4 5 3 6

那么在修改时我们对工资为1的点,我们就在位置1上加1
                  工资为2的点,我们就在位置2上加1
                  工资为7的点,我们就在位置4上加1
简言之,在修改时,对工资修改是在它的大小位置上添加标记(别忘了在原位置上减1)

那么在查询有多少人工资比他低的人就只需要查询它工资的位置就好
例如

上述例子中
工资 1 2 7 8 9 5 9
查询工资比5低的人,5的位置是3,所以查询1 2位置上的标记个数即可

于是,对于扣除的工资数y,我们只要求出工资比y低的人数,用 总人数-工资比y低的人数=工资不低于y的人数

②快速求工资小于y的人的工资总和
有了上述解法,下面的问题就好解决了
解法
再开一个数组,对于每次修改,在对应位置上增加工资即可
举例

假设每个人的工资为1 2 7 8 5 9
那么工资的大小排序1 2 4 5 3 6

那么在修改时我们对工资为1的点,我们就在位置1上加1
                  工资为2的点,我们就在位置2上加2
                  工资为7的点,我们就在位置4上加7
简言之,在修改时,对工资修改是在它的大小位置上修改工资(别忘了在原位置上减去原先工资)

所以求工资小于y的人的工资总和,只要简单的在这个数组当中求和即可

上述例子中
工资 1 2 7 8 9 5 9

位置 1 2 3 4 5 6  7
标记 1 2 5 7 8 18  (因为9的位置都是6,所以在6上记录为9+9=18)
查询工资比7低的人的工资之和=>位置4之前的工资之和=1+2+5=8
问题解决

附上代码

#include <cstdio>
#include <iostream>
#include <algorithm>
#define lowbit(i) i&-i
using namespace std;

char mov[201234];
long long n,m,cnt[201234],sum[201234],to[201234],add[201234],loc[201234]={0,0},data[201234];

inline int input()
{
    char c=getchar();int o;
    while(c>57||c<48)c=getchar();
    for(o=0;c>47&&c<58;c=getchar())o=(o<<1)+(o<<3)+c-48;
    return o;
}

void ADD(int Loc,int C,int S){for(int i=Loc;i<=m;i+=lowbit(i))cnt[i]+=C,sum[i]+=S;}
long long count(int Loc)
{
    long long ans=0;
    for(int x=Loc;x>0;x-=lowbit(x))ans+=cnt[x];
    return ans;
}
long long SUM(int Loc)
{
    long long ans=0;
    for(int i=Loc;i;i-=lowbit(i))ans+=sum[i];
    return ans;
}

int main()
{
    //freopen("In.txt","r",stdin);
    //freopen("True.txt","w",stdout);
    scanf("%d%d\n",&n,&m);
    for(int i=1;i<=m;i++)
    {
        mov[i]=getchar();
        to[i]=input();
        add[i]=input();
        loc[i]=add[i];
    }
    sort(loc+1,loc+m+2);
    ADD(1,n,0);
    for(int i=1;i<=m;i++)
    {
        if(mov[i]=='U')
        {
            ADD((lower_bound(loc+1,loc+m+2,data[to[i]])-loc),-1,-data[to[i]]);
            ADD((lower_bound(loc+1,loc+m+2,add[i])-loc),1,add[i]);
            data[to[i]]=add[i];
        }
        else
        {
            int Loc=(lower_bound(loc+1,loc+m+1,add[i])-loc)-1,t=count(Loc),s=SUM(Loc);
            if(1LL*(n-count(Loc))*add[i]+SUM(Loc)>=1LL*add[i]*to[i])puts("TAK");
            else puts("NIE");
        }
    }
    return 0;
}

顺便附上对拍代码

#include<cstdlib>
#include<cstdio>
#include<ctime> 
#include<iostream>
#include<cstring>

int A[123456]={987654321};

using namespace std;
int main()
{
    freopen("In.txt","w",stdout);
    srand(time(NULL));
    int n=rand()%100000+1,m=rand()%100000+1;
    printf("%d %d\n",n,m);
    for(int i=1;i<=m;i++)
    {
        int c=rand()%2,a=rand()%n+1,add=rand()%1000000000;
        printf("%c %d %d\n",(c?'U':'Z'),a,add);
    }
    return 0;
}

对拍文件
运行对拍代码可以得到In.txt(输入数据)
运行删去//后的我的代码可以得到True.txt(正解)

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值