题面:#10471. 「2020-10-02 提高模拟赛」灌溉 (water)
假设只有一组询问,我们可以用二分求解:二分最大距离是多少,然后找到深度最大的结点,并且把它的(k)倍祖先的一整子树删掉,看一下一共要删几次,显然满足单调性。
现在要询问所有取值。上面二分的过程启发我们可以反过来,通过枚举答案,然后找到答案对应哪些询问。显然对于当前( ext{ans}),一次删除最少删掉(ans+1)个点,最多删(frac{n}{ans+1})次,因此是一个调和级数(frac{1}{1}+frac{1}{2}+frac{1}{3}+frac{1}{4}+....+frac{1}{n} -> nln(n)),只要保证每次枚举的时间复杂度与(n)脱钩就行,用线段树维护剩余数的最大值的位置就行了,时间复杂度(nlog^2(n))。
这个线段树的实现比较巧妙,( ext{lazytag})有三种取值,(0)表示没有操作,(1)表示要把孩子们全删了,(2)表示要把孩子们全弄回来。若一个点的懒标记大于零,就直接用它的懒标记覆盖它儿子们的懒标记。为了实现恢复操作,要额外存下每个点最初始的值用作恢复用。
int tree[maxn * 4], val[maxn * 4], fir[maxn * 4];
/// val: 0 -> 无要求; 1 -> 要求填满; 2 -> 要求删掉
int pushup(int x, int y)
{
return dep[x] < dep[y] ? y : x;
}
void spread(int x)
{
if (val[x] == 1)
{
val[x << 1] = val[x << 1 | 1] = 1;
tree[x << 1] = fir[x << 1];
tree[x << 1 | 1] = fir[x << 1 | 1];
}
if (val[x] == 2)
{
val[x << 1] = val[x << 1 | 1] = 2;
tree[x << 1] = tree[x << 1 | 1] = 0;
}
val[x] = 0;
}
void build(int x, int l, int r)
{
val[x] = true;
if (l == r)
{
fir[x] = tree[x] = pnt[l];
return;
}
int mid = (l + r) >> 1;
build(x << 1, l, mid);
build(x << 1 | 1, mid + 1, r);
fir[x] = tree[x] = pushup(tree[x << 1], tree[x << 1 | 1]);
}
void erase(int x, int l, int r, int ll, int rr)
{
//tree[x] *= val[x];
//tree[x] = fir[x] * val[x];
if (ll <= l && r <= rr)
{
tree[x] = 0; val[x] = 2;
return;
}
spread(x);
int mid = (l + r) >> 1;
if (ll <= mid)
{
erase(x << 1, l, mid, ll, rr);
}
if (rr > mid)
{
erase(x << 1 | 1, mid + 1, r, ll, rr);
}
tree[x] = pushup(tree[x << 1], tree[x << 1 | 1]);
}
int calc(int k)
{
//insert(1, 1, 1, 1, n);
val[1] = 1;
tree[1] = fir[1];
int ret = 0;
while (tree[1])
{
int del = Kfat(tree[1], k);
erase(1, 1, n, dfn[del], dfn[del] + sze[del] - 1);
ret++;
}
return ret;
}