- P198's solution
-
P198's Solution
- @ 2025-9-7 12:14:36
显然最短的操作序列不包含 ,因为 的操作什么用也没有,可以直接删去。
当已经确定操作序列时,去掉操作序列中的重复元素(假设取出后还剩 个元素,它们本身构成集合 ,加上元素 后构成集合 ),考察 号垃圾站和操作序列中涉及到的其他垃圾站:
- 这 个垃圾站中最开始有 袋垃圾。
- 设原操作序列中使得员工们并没有什么都不做的最靠后的出现 的位置是 ,考察 中的某个垃圾站 :
- 如果 不存在,那么可以直接从原操作序列中删掉所有元素 (以及 号垃圾站的所有下级垃圾站、 号垃圾站的所有下级垃圾站的所有下级垃圾站、……所对应的元素,因为这些垃圾站不可能跨过 号垃圾站把垃圾运送到 号垃圾站,下同),显然不影响清理垃圾的袋数且可以使得操作序列更短。
- 同理,如果原操作序列中的某一项使得员工们什么都不做,那么也可以把它删去。
- 否则, 号垃圾站中最后:
- 要么剩下恰好 袋垃圾。
- 要么剩下多于 袋垃圾,但这意味着「它的下级垃圾站后来向它运了垃圾,但这些垃圾最终滞留在了 号垃圾站中,没能运到 号垃圾站,也没能增加清理垃圾的袋数」,此时显然可以直接选择不进行这些无用的运输,从原操作序列中删掉相关元素。(当然了,在剩下恰好 袋垃圾的情况中,我们也可以这么做,于是操作序列中在 后一定不会再出现任何有关 号垃圾站机器下级垃圾站的操作。)
也就是说,设 中所有元素 对应的 构成集合 ,那么最后留下的垃圾数量要想最少就尽量要让 集合中的所有元素之和 最小。
然而,在所有 集合(或 集合)相同的所有操作序列中,既没有相同元素也没有上文提到的这些无用元素的(或者说「最简洁的」)那个操作序列不仅可以实现 (显然不可能再小了),而且项数显然最少。这样的操作序列一定存在:
- 对于任何一个垃圾站 ,考虑删去(如果有)操作序列中除 外另一个等于 的元素 :
- 对于任何一个垃圾站 ,只要 依然不会使得员工什么都不做(或者说「 没有失效」),删去 要么( 时,删去 不影响 在操作序列中的位置)不影响 ,要么( 时,删去 使得 从操作序列中前移了一位),把 减小了 :
- 若 没有下级垃圾站,则原来的 显然不受对其他垃圾站进行的操作的影响, 依然不会使得员工什么都不做。
- 当操作到 时,所有应该运给 号垃圾站的垃圾已经运送完毕(因为后面不会再运了)。若 的下级垃圾站构成的集合 中的每个下级垃圾站 都被证明「 没有失效」,则它们最开始存放了 袋垃圾,最后滞留了 袋垃圾,二者之差 $\displaystyle \sum _{x \in P_z}a_x - \displaystyle \sum _{x \in P_z}d_x$ 就是它们运给 号垃圾站的垃圾袋数。由上面的分析, 不变, 只会变小或不变,因此二者之差 $\displaystyle \sum _{x \in P_z}a_x - \displaystyle \sum _{x \in P_z}d_x$ 只会变大或不变。由于执行 时 号垃圾站本就拥有多于 袋垃圾($\displaystyle \sum _{x \in P_z}a_x - \displaystyle \sum _{x \in P_z}d_x$ 袋),删除后 $\displaystyle \sum _{x \in P_z}a_x - \displaystyle \sum _{x \in P_z}d_x$ 只会变大或不变, 又只会变小或不变,因此仍然有 $\displaystyle \sum _{x \in P_z}a_x - \displaystyle \sum _{x \in P_z}d_x > d_z$,因此可以断言「 没有失效」。
- 综上,对于任何一个垃圾站 ,可以断言「 没有失效」。
- 删去 后,考察 号垃圾站和操作序列中涉及到的其他垃圾站,它们最开始存放的垃圾袋数之和 保持不变(事实上, 各自不变)。
- 综上,删去 一定是(并非严格地)更优的决策。
- 对于任何一个垃圾站 ,只要 依然不会使得员工什么都不做(或者说「 没有失效」),删去 要么( 时,删去 不影响 在操作序列中的位置)不影响 ,要么( 时,删去 使得 从操作序列中前移了一位),把 减小了 :
- 对于某个垃圾站 ,对所有满足上述条件的 的逐个考虑删去,操作序列中便只会剩下唯一一个()等于 的元素。
- 对 都像上面那样考虑一遍,操作序列中便不会再留下任何两个相同的元素,于是我们成功得到了「最简洁的」操作序列。
因此,我们只需要对每个 集合求出一个「最简洁的」操作序列(如果存在的话),然后对这些操作序列进行(清理垃圾袋数和项数的)比较和计数即可。
进一步地,让我们把垃圾站的关系抽象成一棵树,那么 集合一定构成一个包含 号结点的连通块,于是我们考虑树形 DP,设三元组 表示:
- 只考虑 集合在 号结点(及其子树)的部分(且这一部分恰好包含 个结点)。
- 此时的最多垃圾清理袋数(这里认为运送到 号结点就算清理成功)。
- 此时对应的操作序列(这里指完整操作序列的子序列)最小项数。
- 此时最优操作序列的个数。
接下来,我们惊喜地发现:
- 除非不存在「最简洁的」操作序列,否则最优操作序列一定等价于「最简洁的」操作序列。
- 除非不存在「最简洁的」操作序列,否则最优操作序列的操作次数一定是 。
- 除非不存在「最简洁的」操作序列,否则最多垃圾清理袋数一定是选定部分的初始垃圾袋数减去 $\displaystyle \sum _{i=1} ^{y-1} i = 1+2+\ldots+(y-1)=\dfrac{y(y-1)}{2}$ 的结果。
- 当不存在「最简洁的」操作序列时,如果将目前的最优操作序列中的无用操作去掉,得到一个「最简洁的」操作序列,那么这个操作序列:
- 项数一定少于目前的最优操作序列。
- 垃圾清理袋数一定不少于目前的最优操作序列。
- 所对应的「 集合在 号结点(及其子树)的部分」的结点数一定不大于(实际上是严格小于,因为我们已经假设了「不存在「最简洁的」操作序列」)目前的最优操作序列。
- 一定在某个 中()被统计过了,于是从 向上转移出来的答案都不是最优解(都比从 向上转移出来的答案更劣),我们不会在最终的答案计算中采纳任何涉及 的答案。
- 当不存在「最简洁的」操作序列时,我们会把(本就不是全局最优解的)目前的最优操作序列的垃圾清理袋数算少(因为无用操作的本质是剩余垃圾太少运不上去,如果无用操作对应的那些垃圾站最终剩下的垃圾袋数的总和多余对应的 序列中的预测值的总和,那么由前面的分析可知我们一定可以多运一些垃圾上去,这与它们是无用操作的试试矛盾,于是我们只会把最终剩下的垃圾袋数算多,而不会算少,这导致我们把目前的最优操作序列的垃圾清理袋数算少),于是我们更不会在最终的答案计算中采纳任何涉及 的答案。
也就是说,根据树形 DP 的流程,我们可以放心大胆地假设「最简洁的」操作序列有一定存在,因为我们不会在最终的答案计算中采纳任何涉及并非「最简洁的」操作序列的答案。
最后,当两个方案数分别为 ,项数分别为 的子序列合并时,大序列的方案数为 $\displaystyle w_1w_2\binom{l_1+l_2}{l_1}=w_1w_2C_{l_1+l_2}^{l_1}=w_1 \cdot w_2 \cdot \dfrac{(l_1+l_2)!}{l_1! \cdot l_2!}$,其中组合数可以 预处理。
最终的时间复杂度为 ,空间复杂度为 。
std 的 DP 实现与上文中的 DP 策略有两点不同:
- std 中 指操作序列长度,比上文中的 少 。
- std 在 DP 过程中没有扣掉剩余的垃圾袋数,因此计算最终答案时需要在 std 中减去 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1000 + 12;
const int M = 2000 + 12;
const int P = 1e9 + 7;
int C[N][N];
struct Bag
{
int n;
int maxsum[N];
int count[N];
};
Bag b[N];
void init()
{
for (int i = 0; i < N; i++)
C[i][0] = C[i][i] = 1;
for (int i = 2; i < N; i++)
for (int j = 1; j < i; j++)
{
C[i][j] = C[i - 1][j] + C[i - 1][j - 1];
if (C[i][j] >= P) C[i][j] -= P;
}
}
void operator+=(Bag& x, Bag y)
{
Bag z = x;
for (int i = 0; i <= x.n; i++)
x.maxsum[i] = x.count[i] = 0;
x.n = z.n + y.n;
for (int i = 0; i <= z.n; i++)
for (int j = 0; j <= y.n; j++)
{
if (z.maxsum[i] + y.maxsum[j] > x.maxsum[i + j])
{
x.maxsum[i + j] = z.maxsum[i] + y.maxsum[j];
x.count[i + j] = 0;
}
if (z.maxsum[i] + y.maxsum[j] == x.maxsum[i + j])
{
x.count[i + j] += 1ll * z.count[i] * y.count[j] % P * C[i + j][i] % P;
if (x.count[i + j] >= P) x.count[i + j] -= P;
}
}
}
void operator+=(Bag& x, int y)
{
x.n++;
for (int i = x.n; i >= 1; i--)
{
x.maxsum[i] = x.maxsum[i - 1] + y;
x.count[i] = x.count[i - 1];
}
}
struct Fk
{
int to[M];
int ns[M];
int fs[N];
int m = 0;
void init(int n)
{
for (int i = 1; i <= m; i++)
to[i] = ns[i] = 0;
for (int i = 1; i <= n; i++)
fs[i] = 0;
m = 0;
}
void add(int f, int t)
{
m++;
to[m] = t;
ns[m] = fs[f];
fs[f] = m;
}
};
Fk fk;
int f[N];
int a[N];
void dfs(int x)
{
int ii = fk.fs[x];
while (ii)
{
if (fk.to[ii] != f[x])
{
dfs(fk.to[ii]);
b[x] += b[fk.to[ii]];
}
ii = fk.ns[ii];
}
b[x] += a[x];
}
int main()
{
freopen("garbage.in", "r", stdin);
freopen("garbage.out", "w", stdout);
init();
int T;
cin >> T;
while (T--)
{
int n;
cin >> n >> a[1];
for (int i = 2; i <= n; i++)
cin >> f[i] >> a[i];
fk.init(n);
memset(b, 0, sizeof b);
for (int i = 1; i <= n; i++)
b[i].count[0] = 1;
for (int i = 2; i <= n; i++)
{
fk.add(i, f[i]);
fk.add(f[i], i);
}
dfs(1);
int bs = 0;
for (int i = 0; i <= n - 1; i++)
b[1].maxsum[i + 1] -= i * (i + 1) / 2;
for (int i = 0; i <= n - 1; i++)
if (b[1].maxsum[i + 1] > b[1].maxsum[bs + 1]) bs = i;
cout << b[1].maxsum[bs + 1] << ' ' << bs << ' ' << b[1].count[bs + 1] << endl;
}
return 0;
}