题解 P1084 【疫情控制】
skydogli
·
·
题解
提供一种可能不太恶心的二分+set做法。
所以满足单调性,所以考虑二分答案。对于每次的答案$mid$,把所有
军队在$mid$的时间内尽量往上提,可以用倍增在单次$O(logn)$的时间里做到。如果能到达根节点,往$set$中插入该军队到根节点后还能移动的距离(下文称之为可移动距离),这些军队可以进驻在自身的可移动距离内的任意节点。
$\quad$所有节点都尽量向上移动后,统计根节点各个子节点是否需要军队驻扎。如果某个子节点$a$没有军队上到根节点且需要军队,那么直接在set中找到可移动距离尽量小但能到达$a$的军队驻扎并在$set$中删除该军队。
$\quad$如果某个节点$b$有军队到了根节点但是需要驻扎,我们设根到该节点距离为$w$,该点往$set$中插入的的军队的可移动距离为$v$(有多个取较小值),我们讨论一下:
- $w>v$,如果用根节点储存的军队驻扎,就是用大于$w$的值变成$v$明显不划算,所以我们直接"撤回",让这个节点的军队退回去,在set中删除该军队。
- $w<v$,用$set$储存的军队驻扎,可能使用的军队的可移动距离$<v$,这样我们是賺了的,而用大于$v$的也不会更劣(因为$v$已经插入$set$中了,如果当前$\ge v$,说明这个军队在进驻了其它节点,如果让这个军队回来就要让可移动距离更大的军队去原来这个军队进驻的点,不会让结果更优),那么我们就用$set$中的可移动距离最小且能移动到$b$的军队驻扎在$b$点。
$\quad$如果所有根节点子节点都有驻扎,那么这个$mid$是合法的。总的时间复杂度是$O(nlog^2n)$。常数略大,仅供娱乐。
另外如果军队数小于根节点的子节点数,那么无解,输出-1。
### 代码&注释
```cpp
// luogu-judger-enable-o2
#include<bits/stdc++.h>
using namespace std;
#define MN 50005
#define INF 1e16
#define Lg 17
#define LL long long
#define itset multiset<LL>::iterator
inline int max(int a,int b){if(a>b)return a;return b;}//提醒一下,三目运算符速度比if……else快,但是没有1个if快
inline int read(){
int a=0;char c=getchar();
while(c>57 or c<48)c=getchar();
while(47<c and c<58){
a=a*10+c-48;
c=getchar();
}
return a;
}
struct Edge{
int to,v;
Edge(int TO,int V){to=TO;v=V;}
};//vector建边的结构体
vector<Edge>edge[MN];
multiset<LL>cnt;//记录根节点储存的节点
int fa[Lg][MN],tag[MN],n,m,u,v,w,loc[MN];
LL UP[MN],cost[Lg][MN],lon[MN];
//fa、cost:倍增的父亲数组和花费的时间
//loc:军队初始位置
//tag:这个节点是否有军队
//UP:从这个节点上到根节点的军队的可移动距离
//lon:一棵树的最长链
void dfs(int x){
for(int i=1;i<Lg;++i){
fa[i][x]=fa[i-1][fa[i-1][x]];
cost[i][x]=cost[i-1][x]+cost[i-1][fa[i-1][x]];
}//倍增预处理
for(int i=0;i<edge[x].size();++i)
if(edge[x][i].to!=fa[0][x]){
fa[0][edge[x][i].to]=x;
cost[0][edge[x][i].to]=edge[x][i].v;
dfs(edge[x][i].to);
lon[x]=max(lon[x],lon[edge[x][i].to]+(LL)edge[x][i].v);
}
}
void up(int x,LL T){//尽量向上跳
int tmp=x;
for(int i=Lg-1;i>=0;--i)
if(cost[i][x]<=T&&fa[i][x]>1){
//要记录由哪个节点跳到1,所以先不能跳到1
T-=cost[i][x];x=fa[i][x];
//注意这里!先减花费的时间再跳向父亲!
}
if(T<cost[0][x]){tag[x]=1;return;}//跳不了了,打个标记
UP[x]=min(UP[x],T-cost[0][x]);
//记录由这个节点跳上去的军队的最小的可移动距离
cnt.insert(T-cost[0][x]);
//插入该军队的可移动距离
}
void DFS(int x){
if(tag[x])return;//该节点有军队,不用进驻了
if(edge[x].size()!=1)tag[x]=1;//size等于1就是叶子节点
for(int i=0;i<edge[x].size();++i)
if(edge[x][i].to!=fa[0][x]){
DFS(edge[x][i].to);
if(!tag[edge[x][i].to]){
tag[x]=0;//子节点都有军队,那么也不用进驻了
}
}
}
bool chk(LL T){
cnt.clear();
memset(tag,0,sizeof(tag));
memset(UP,0x7f7f7f,sizeof(UP));
for(int i=1;i<=m;++i){
up(loc[i],T);//跳
}
DFS(1);//统计
for(int i=0;i<edge[1].size();++i){
if(!tag[edge[1][i].to]){
if(UP[edge[1][i].to]<edge[1][i].v){
cnt.erase(cnt.find(UP[edge[1][i].to]));
tag[edge[1][i].to]=1;
}
}//直接把这个军队撤回来
}
for(int i=0;i<edge[1].size();++i){
if(!tag[edge[1][i].to]){
itset IT=cnt.lower_bound(edge[1][i].v);
if(IT==cnt.end()) return 0;//找不到符合要求的军队,不合法
cnt.erase(IT);
}
//上下2个操作不能同时进行,不然可能把需要撤回的军队拿去驻扎了,比较麻烦
}
return 1;
}
signed main(){
n=read();
for(int i=1;i<n;++i){
u=read();v=read();w=read();
edge[u].push_back(Edge(v,w));
edge[v].push_back(Edge(u,w));
//建边
}
dfs(1);
m=read();
for(int i=1;i<=m;++i)
loc[i]=read();
LL l=-1;
LL r=lon[1]<<1;//最大需要的时间是最长链的2倍
while(l+1<r){
LL mid=(l+r)>>1;
if(chk(mid)) r=mid;
else l=mid;
}//左开右闭的奇葩二分小心品尝。。。
printf("%lld",r);
return 0;
}
```