传送门
这道题作为NOIP2012D2T3的压轴题,虽然对于如今的NOIP来说其分量可能略有不足,但放在当年绝对是一道思维和代码难度兼顾的好题。
首先题目中有一个很明显的标志:控制疫情的总时长取决于时间最长的军队的时间,即我们要让最大时间最小,那么就启发我们应用二分算法。
对于当前的一个二分值\(mid\),如何检验能否在\(mid\)时间内控制疫情呢?
因为我们要满足每一个叶子节点到根的路径上至少要建立一个检查点,那么如果这个检查点里根节点越近,那么他能管辖的叶子节点就越多。所以对于初始的\(m\)个军队,我们可以让他们在\(mid\)的时间内尽量往上移,直到根节点的孩子节点。而这个上移的过程可以用倍增实现。
在上移的过程中,会出现两种情况:
1.这个军队能移动到根节点,那么他就可能去管辖根的其他孩子。
2.这个军队在给定时间到不了根节点,那么就停在能到达的最上面的节点。
对于第二种情况,直接标记这个节点即可,而对于第一种情况,要稍作复杂的讨论。
在讨论之前,我们可以通过一遍dfs求出根的哪些孩子需要被管辖,具体做法就是遇到被标记的节点直接返回,否则一直递归到叶子节点。
至此我们得到了需要被管辖的根的孩子节点的集合\(S\)和所有第一类军队(能到达根节点的军队)集合\(C\).现在的问题在于如何用\(C\)去匹配\(S\),使\(S\)中的所有元素都被匹配。
能否匹配只取决于\(S\)中根到他们的距离\(S_i\)和\(C\)中这些军队到达根节点后的剩余时间\(C_i\)的大小关系。那么我们只要分别将\(S,C\)从小到大排序,用双指针就能解决了。
但这么做忽视了一种情况:对于那些到达根节点后回不到原孩子的军队,我们应该让他们直接留在原处,而不是参与上述的匹配。因为如果参与匹配的话,他必定不能匹配原孩子,但是如果直接留在原处,就匹配上了原孩子。这就是“决策包容性”的贪心证明方法:在他不能经过根节点再回到原孩子的前提下,“自己留在原地匹配原孩子,比他剩余时间多的军队匹配其他孩子”这个决策显然包含了“自己不匹配原孩子,比他剩余时间多的其中一个军队来匹配这个孩子”的决策,也就是说,这个局部的贪心不会改变未来的可达状态。
总而言之,我们的算法应按如下顺序执行:
1.预处理倍增数组
2.二分答案
对于每一次二分:
(1) 让每个点尽量往上跳,得到两类节点。
(2) 统计哪些孩子需要被管辖。
(3) 处理上述特殊情况。
(4) 用双指针进行匹配,并判断本次二分是否可行。
详细过程请看代码(需支持c++11语法)
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
#define Mem(a, x) memset(a, x, sizeof(a))
#define forE(i, x, y) for(int i = head[x], y; ~i && (y = e[i].to); i = e[i].nxt)
typedef long long ll;
const int maxn = 5e4 + 5;
const int N = 16;
int n, m, a[maxn];
struct Edge
{
int nxt, to, w;
}e[maxn << 1];
int head[maxn], ecnt = -1;
void addEdge(int x, int y, int w)
{
e[++ecnt] = (Edge){head[x], y, w};
head[x] = ecnt;
}
int fa[N + 2][maxn], dep[maxn], p[maxn]; //p[i]表示点i属于根的哪个儿子
ll dis1[maxn], dis[N + 2][maxn]; //dis1[i]表示点i到根节点的时间
void dfs1(int u, int _f)
{
for(int i = 1; (1 << i) <= dep[u]; ++i)
{
fa[i][u] = fa[i - 1][fa[i - 1][u]];
dis[i][u] = dis[i - 1][u] + dis[i - 1][fa[i - 1][u]];
}
forE(i, u, v)
{
if(v == _f) continue;
dep[v] = dep[u] + 1;
fa[0][v] = u, dis[0][v] = e[i].w;
dis1[v] = dis1[u] + e[i].w;
p[v] = p[u] ? p[u] : v;
dfs1(v, u);
}
}
bool vis[maxn]; //vis[i]表示这个点是否驻扎了军队
vector<int> cap; //cap存放第一类节点(能跳到首都的点)
vector<ll> res, res_son; //res存放可调度的点的剩余时间,res_son存放需要控制的儿子的所需时间
void jump(int x, ll mid)
{
if(dis1[x] <= mid) cap.push_back(x); //第一类节点
else
{
for(int i = N; i >= 0; --i) //第二类节点
if(fa[i][x] && dis[i][x] <= mid) mid -= dis[i][x], x = fa[i][x];
vis[x] = 1; //跳不到根节点,就驻扎在最上面的节点
}
}
bool ned[maxn];
bool dfs2(int u, int _f)
{
if(vis[u]) return 0;
bool flg = 0, flg_leaf = 1;
forE(i, u, v) if(v != _f) flg |= dfs2(v, u), flg_leaf = 0;
return flg_leaf | flg;
}
bool check(ll mid)
{
Mem(vis, 0), cap.clear(); res.clear(); res_son.clear(); Mem(ned, 0);
for(int i = 1; i <= m; ++i) jump(a[i], mid); //每个点尽量往上跳
forE(i, 1, v) if(dfs2(v, 1)) ned[v] = 1; //这个孩子节点需要被占领
sort(cap.begin(), cap.end(), [&](int a, int b) {return mid - dis1[a] < mid - dis1[b];});
for(int i = 0; i < (int)cap.size(); ++i) //处理第一类节点中的特殊情况
{
int x = cap[i], son = p[x];
if(ned[son] && mid - dis1[x] < dis[0][son]) ned[son] = 0;
else res.push_back(mid - dis1[x]);
}
sort(res.begin(), res.end());
forE(i, 1, v) if(ned[v]) res_son.push_back(dis[0][v]);
sort(res_son.begin(), res_son.end());
for(int i = 0, j = 0; i < (int)res_son.size(); ++i) //双指针匹配,要先排序
{
while(j < (int)res.size() && res[j] < res_son[i]) ++j;
if(j == (int)res.size()) return 0;
++j;
}
return 1;
}
ll solve(ll M) //二分
{
ll L = 0, R = M + 1;
while(L < R)
{
ll mid = (L + R) >> 1;
if(check(mid)) R = mid;
else L = mid + 1;
}
return L == M + 1 ? -1 : L;
}
int main()
{
Mem(head, -1), ecnt = -1;
scanf("%d", &n);
ll Sum = 0;
for(int i = 1, u, v, w; i < n; ++i)
{
scanf("%d%d%d", &u, &v, &w);
addEdge(u, v, w), addEdge(v, u, w);
Sum += w;
}
scanf("%d", &m);
for(int i = 1; i <= m; ++i) scanf("%d", &a[i]);
dep[1] = 1, dfs1(1, 0); //预处理倍增数组
printf("%lld\n", solve(Sum));
return 0;
}