题解 P3553 [POI2023] INS-Inspector 督察
huanxiong_2022 · · 题解
P3553 [POI2013] INS-Inspector
摘要:
- 二分
k ,每次沿时间轴检验前mid 个是否冲突。 - 人的记录使他有一个必须存在的时间段。
- 逐时间点、尽可能使人数最少地满足记录要求,并检查是否使用超过
n 个人。
题意:
公司有
分析:
-
发现
k 似乎无法直接求得,考虑转化为检验前k 条是否合法。 -
发现对于第
i+1 条记录,直接检验其与前i 条是否冲突似乎不好实现,考虑每次检验O(m) 地遍历全部时间点,因此需要二分k 。 -
如何检验
-
由于每个员工工作时间连续,所以将记录整合可以得到每个人必须要工作的区间,后文简称“必要区间”,整合区间可以得到每个时间点至少有多少人,检验记录是否小于最小值。以及是否存在对于同一时刻有不同记录。
-
通过了前一个检测的所有情况中,每个时间点的记录人数大于等于实际人数,因此若不限制人数一定合法(即每个人只工作于一个时间点)。问题转化为尝试使用不多于
n 个人构造一个合法解。 -
沿时间轴检验并时刻判断即可,详见下文。个人认为本题难点是检测的思路和具体实现。
-
-
检验的实现
-
思路:
尝试用最优方式满足限制,并记录使用了多少人,看是否超过
n 。 -
预处理:
每个时间点
i 的记录人数、有多少人的“必要区间”以当前时间开始或结束;即cnt_i,bg_i,ed_i 。 -
实现方案不唯一,部分名称含义如下:
$used$:已经使用过多少人。 $exl$:有多少人已经完成了必要区间,但是被迫延后下班。 $exr$:有多少人还没到必要区间,但是被迫提前上班。 $fre$:没有写记录的人,他们可以自由安排。 $left$:已经完成了工作,离开公司的人,后续安排与他们无关。 注意,初始时认为上述全部为零;另外,上述名称在表述时逻辑上也视作集合。 -
每一步的处理方法
处理顺序:
bg_i \rightarrow cnt_i \rightarrow ed_i 。通过变量上的加减、逻辑上的人员调动(即从某一集合移动到另一集合),用now,exl,exr 拼凑出cnt_i -
当前点至少有
bg_i 个人开始工作,逻辑上,要把exr 或者fre 拿出bg_i 个转化为now ,优先使用exr ,不足则增加used 。 -
-
若
now+exl+exr 大于cnt_i ,则先弹掉exl ,仍多则弹掉exr 。 -
在当前时间,有
ed_i 个人可以结束工作,让他们进入exl 等待后续调度。 -
最后,判断
used 是否超限。把上述步骤循环m 次即可。
-
-
实现:
本人采用了
注意,输入的是“除了他还有多少人”。
Code:
Link
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10,inf=0x3f3f3f3f;
int T,n,m;
struct Log{int id,time,cnt;};
struct Staff{int bg,ed;};
struct Time{int cnt,bgcnt,edcnt,vis;};
Log g[maxn];
Staff p[maxn];
Time t[maxn];
bool check(int mid)
{
for(int i=1;i<=n;i++)p[i]={0,0};
for(int i=1;i<=m;i++)t[i]={0,0,0,0};
for(int i=1;i<=n;i++)p[i]={inf,-inf};
for(int i=1;i<=mid;i++)
{
//同一时刻的记录发生冲突
if( t[g[i].time].cnt!=0 && t[g[i].time].cnt!=g[i].cnt)
{
return false;
}
t[g[i].time].cnt = g[i].cnt;
t[g[i].time].vis=1;
p[g[i].id].bg=min(p[g[i].id].bg,g[i].time);
p[g[i].id].ed=max(p[g[i].id].ed,g[i].time);
}
for(int i=1;i<=n;i++)
{
if(p[i].bg!=inf)
t[p[i].bg].bgcnt++,t[p[i].ed].edcnt++;
}
int now=0,used=0,exl=0,exr=0;
//“必要区间”决定的人数
//使用过的人数
//可以下班但被迫加班的人数
//暂时不用上班但是被迫提前的人数
//即 exl 位于左侧的可拓展人数,exr右侧可拓展
for(int i=1;i<=m;i++)
{
if(!t[i].vis)continue;
now += t[i].bgcnt;
if(now>t[i].cnt)
return false;
//“必要区间”与记录矛盾
while (t[i].bgcnt--)
{
if(exr)exr--;
else used++;
}
//需要有一些人开始工作,有 exr 就让他开始,没有就新调人来
//注意这里新增加的 used 是不可以被划分成 exl 的,他们会在结束自己的“必要区间”后加入 exl
//可以写成 if else 的判断,但是 while 易于理解且不容易错
if(now+exl+exr<t[i].cnt)
{
int d = t[i].cnt-now-exl-exr;
exr+= d; used+= d;
}
if(now+exl+exr>t[i].cnt)
{
int d = now+exl+exr-t[i].cnt;
while(d--)
{
if(exl>0)exl--;
else exr--;
}
}
now-=t[i].edcnt; exl+=t[i].edcnt;
if(used>n)
{
return false;
}
}
return true;
}
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&g[i].time,&g[i].id,&g[i].cnt);
g[i].cnt++;
}
int l=1,r=m;
while(l<r)
{
// printf("l=%d,r=%d ",l,r);
int mid=(l+r+1)>>1;
if(check(mid))l=mid;
else r=mid-1;
}
printf("%d\n",l);
}
return 0;
}
upd: 2023.09.22 初稿