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(正解)