题解:P16072 [ICPC 2023 NAC] Fail Fast
lailai0916 · · 题解
题意简述
有
遇到第一个失败的测试后停止,费用为已经消耗的时间;若所有测试都通过,费用记为
解题思路
暂时将全通过时的费用也设为所有测试耗时之和。对于执行顺序
第
题目真正的期望费用等于
将若干连续执行的测试视为一个块。对于块
比较
所有非空块都有
没有依赖时,按上述比值排序即可。有依赖时,不能只在当前可以执行的测试中贪心,因为某个较慢测试可能解锁非常值得提前执行的后继。
加入虚拟测试
在所有不含虚拟测试的当前块中,选比值最小的块
为什么可以强制它们连续?考虑当前块问题的一种最优顺序。父块
-
-
- 因此这些块与
A 没有先后依赖,可以逐个交换。 -
所以一定存在一种最优顺序让
反复收缩后只剩包含虚拟测试的一个块,其内部顺序就是所求答案。虚拟块始终在最前面,不参与比值选择,也不输出。
用并查集维护每个原测试当前属于哪个块。head 保存块的第一个测试,tail 保存最后一个测试,nxt 将块内顺序连成链表。把块
当前块的外部依赖,是 head 对应原测试的依赖对象所在的块。并查集采用按大小合并,代表元不一定是块的第一个测试,所以不能把代表元直接当成 head。合并前先保存顺序信息与新概率、新期望,选好代表元后再一起写回,避免并查集换根改变执行顺序。
最小比值用堆维护。块被吸收,或者合并后数值改变,原堆条目都会失效;用代表元检查和版本号丢弃这些旧条目。每次合并至多插入一个新条目,总插入数为
概率及期望使用 long double。按大小合并也限制了并查集树高,配合路径压缩,避免长依赖链产生深递归。总时间复杂度为
参考代码
#include <bits/stdc++.h>
using namespace std;
using ld=long double;
const int N=100005;
struct Node
{
ld c,p;
int id,ver;
bool operator<(const Node &x)const
{
ld a=c*(1-x.p),b=x.c*(1-p);
return a!=b?a>b:id>x.id;
}
};
ld c[N],p[N];
int d[N],fa[N],siz[N],head[N],tail[N],nxt[N],ver[N];
int find(int u){return u==fa[u]?u:fa[u]=find(fa[u]);}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin>>n;
p[0]=1;
siz[0]=1;
priority_queue<Node> q;
for(int i=1;i<=n;i++)
{
cin>>c[i]>>p[i]>>d[i];
fa[i]=head[i]=tail[i]=i;
siz[i]=1;
q.push({c[i],p[i],i,0});
}
for(int i=1;i<=n;i++)
{
while(fa[q.top().id]!=q.top().id||ver[q.top().id]!=q.top().ver)q.pop();
int u=q.top().id;
q.pop();
int v=find(d[head[u]]);
nxt[tail[v]]=head[u];
ld x=c[v]+p[v]*c[u],y=p[v]*p[u];
int h=head[v],t=tail[u];
if(siz[u]>siz[v])swap(u,v);
fa[u]=v;
siz[v]+=siz[u];
c[v]=x;
p[v]=y;
head[v]=h;
tail[v]=t;
ver[v]++;
if(h)q.push({c[v],p[v],v,ver[v]});
}
for(int i=nxt[0];i;i=nxt[i])cout<<i<<'\n';
return 0;
}