Solution Set【2024.2.16】
A. 寄(post)
对于点对贡献问题考虑在最近公共祖先处计算答案,称给定的 \(m\) 个点为关键点,选择的 \(k\) 个点为选择点。
既然我们已经要求了对于每一对关键点和选择点均在其最近公共祖先处计算答案,那么这也就意味着,存在某些节点,其子树中的关键点 / 选择点不会与其子树内的选择点 / 关键点配对。我们可以发现:若某节点子树内的关键点未与其子树内的选择点配对,那么产生的花费只与未配对的关键点数量有关;若某节点子树内的选择点未与其子树内的关键点配对,那么产生的花费只与其到子树根节点的距离有关,因此对于任意节点,其子树内将要与子树外的关键点配对的选择点只会存在一个最优选择。同时我们可以发现,对于任意节点,在考虑以其为最近公共祖先的点对时,每个儿子节点的子树不可能同时有未匹配的关键点和未匹配的选择点,否则这种情况一定不是最优情况。因此我们可以对上述两种情况进行 DP。
设 \(f_{u, i}\) 表示 \(u\) 子树内恰好存在 \(i\) 个关键点未配对的最小花费。\(g_{u, v}\) 表示 \(u\) 子树内不存在未配对的关键点且已经选择了点 \(v\) 作为与子树外的关键点进行配对的最小花费。使用类似于树上背包的转移即可。
复杂度为 \(\mathcal{O}(n^2)\)。
Code
#include <bits/stdc++.h>
typedef long long valueType;
typedef std::vector<valueType> ValueVector;
typedef std::vector<ValueVector> ValueMatrix;
typedef std::pair<valueType, valueType> ValuePair;
typedef std::vector<ValuePair> PairVector;
typedef std::vector<PairVector> PairMatrix;
constexpr valueType MAX = std::numeric_limits<valueType>::max() >> 2;
valueType N, M, C;
ValueVector Count;
PairMatrix Tree;
ValueVector Dist;
ValueMatrix F, G;
void dfs(valueType x, valueType from) {
F[x][0] = C;
F[x][Count[x]] = 0;
G[x][x] = C;
for (auto const &[to, w] : Tree[x]) {
if (to == from)
continue;
Dist[to] = Dist[x] + w;
dfs(to, x);
ValueVector NextF(Count[x] + Count[to] + 1, MAX), NextG(N + 1, MAX);
for (valueType i = 0; i <= Count[x]; ++i)
for (valueType j = 0; j <= Count[to]; ++j)
NextF[i + j] = std::min(NextF[i + j], F[x][i] + F[to][j] + w * j);
for (valueType i = 1; i <= N; ++i) {
if (G[x][i] == MAX)
continue;
for (valueType j = 0; j <= Count[to]; ++j)
NextG[i] = std::min(NextG[i], G[x][i] + F[to][j] + (Dist[i] - Dist[x] + w) * j);
NextF[0] = std::min(NextF[0], NextG[i]);
}
for (valueType i = 1; i <= N; ++i) {
if (G[to][i] == MAX)
continue;
for (valueType j = 0; j <= Count[x]; ++j)
NextG[i] = std::min(NextG[i], G[to][i] + F[x][j] + (Dist[i] - Dist[x]) * j);
NextF[0] = std::min(NextF[0], NextG[i]);
}
F[x].swap(NextF);
G[x].swap(NextG);
Count[x] += Count[to];
}
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
#ifndef LOCAL_STDIO
freopen("post.in", "r", stdin);
freopen("post.out", "w", stdout);
#endif
std::cin >> N >> M >> C;
Count.resize(N + 1, 0);
Tree.resize(N + 1);
Dist.resize(N + 1, 0);
F.resize(N + 1, ValueVector(N + 1, MAX));
G.resize(N + 1, ValueVector(N + 1, MAX));
for (valueType i = 1; i < N; ++i) {
valueType u, v, w;
std::cin >> u >> v >> w;
Tree[u].emplace_back(v, w);
Tree[v].emplace_back(u, w);
}
for (valueType i = 0; i < M; ++i) {
valueType x;
std::cin >> x;
++Count[x];
}
dfs(1, 0);
std::cout << F[1][0] << std::endl;
return 0;
}
B. 摆(bigben)
首先将矩阵表示出来,下面以 \(n = 6\) 为例,我们有矩阵:
发现主对角线以下的元素值均为 \(C\),因此我们可以执行变换 \(A_{i, j} \leftarrow A_{i, j} - A_{i - 1, j}\) 以将其转化为上 Hessenberg 矩阵。如下为变换后的矩阵:
我们考虑上述矩阵是如何得到的:
- 主对角线上的元素除 \(A_{1, 1}\) 外初始时均为 \(1 - C\)。
- 主对角线左下的次对角线的元素值初始时均为 \(C - 1\)。
- 对于其他元素初始时均为 \(0\)。
接下来我们执行如下操作:
- 对于第 \(i\) 行,所有满足 \(j \neq i \land i \mid j\) 的 \(j\),均有 \(A_{i, j} \leftarrow A_{i, j} - C\)。
- 对于第 \(i\) 行,所有满足 \(j \neq i - 1 \land \left(i - 1\right) \mid j\) 的 \(j\),均有 \(A_{i, j} \leftarrow A_{i, j} + C\)。
进而我们得到了上面的矩阵,其中 \(A_{2, 2}\) 初始时为 \(1 - C\),由于 \(2 \neq 1 \land 1 \mid 2\) 因此执行了 \(A_{2, 2} \leftarrow A_{2, 2} + C\)。
不难发现若 \(C = 1\) 只有当 \(n \le 2\) 时该矩阵的行列式值为 \(1\),否则为 \(0\)。
接下来为了方便计算,我们将矩阵中的元素均除以 \(1 - C\),设 \(V = \frac{C}{1 - C}\),操作后的矩阵形如:
我们考虑求其行列式,不妨设 \(f_{n}\) 表示前 \(n\) 行 \(n\) 列的矩阵的行列式。
我们考虑展开最后一列的行列式以求出 \(f_{n}\) 的值。
对于 \(A_{n, n}\),其贡献为 \(\left(-1\right)^{n + n} \times 1 \times f_{n - 1}\)。对于 \(i < n\),我们考虑 \(A_{i, n}\) 的代数余子式的值。不妨设 \(n = 6, i = 4\),那么删去第 \(i\) 行和 第 \(n\) 列后的矩阵如下:
可以发现此时前 \(i - 1\) 列的后 \(n - i\) 行均为 \(0\),那么根据行列式的置换定义,在前 \(i - 1\) 列选取的置换一定在前 \(i - 1\) 行中,进而后 \(n - i\) 行选取的置换一定在后 \(n - i\) 列中。因此我们可以将矩阵划分为两个子矩阵,其行列式乘积便为该矩阵的行列式。我们考虑这两个子矩阵,其中左上角的矩阵行列式为 \(f_{i - 1}\),这里不多赘述。而右下角的矩阵一定是一个上三角矩阵,其行列式为主对角线的元素之积。同时此子矩阵的主对角线上的元素均为原矩阵次对角线上的元素,其值均为 \(-1\)。
进而我们得到了 \(A_{i, n}\) 的代数余子式,其值为:
接下来我们考虑 \(A_{i, n}\) 的取值,不妨考虑其来源,即上述构造过程中的两种操作。设 \(d\) 满足 \(d \neq n \land d \mid n\),那么其会对 \(A_{d, n}\) 产生 \(-V\) 的贡献,会对 \(A_{d + 1, n}\) 产生 \(V\) 的贡献,对总行列式产生 \(V \times \left(f_d - f_{d - 1}\right)\) 的贡献。
至此我们得到了 \(f_n\) 的表达式:
发现难于计算,我们考虑其差分序列 \(g\),具体的,对于 \(n \ge 1\),设 \(g_n = f_n - f_{n - 1}\)。那么我们有:
那么我们要求的即其前缀和 \(f\)。考虑使用类似杜教筛的方法去处理。
按上式递归计算即可得到时间复杂度为 \(\mathcal{O}(n^{\frac{3}{4}})\)。
我们考虑如何在线性的复杂度内预处理出前 \(n\) 项的 \(f\) 值。
我们可以发现若对 \(n\) 进行质因子分解,设 \(n = \prod p_i^{a_i}\),那么可重集 \(\left\{a_i\right\}\) 相同的 \(n\) 对应的 \(g_n\) 值相同。通过使用线性筛可以处理出每个数该集合的哈希值。
考虑本质不同的集合数量,发现其严格不多于 \(\log_2 n\) 的分拆数。因此对于每个集合我们寻找出一个代表元素,在根号的时间复杂度内枚举其因子并计算出 \(g\) 值即可。
因此我们可以在线性的复杂度内预处理出 \(f\) 的前缀。
总复杂度为 \(\mathcal{O}(n^{\frac{2}{3}})\)。
Code
#include <bits/stdc++.h>
typedef long long valueType;
typedef std::vector<valueType> ValueVector;
typedef unsigned long long hashType;
typedef std::vector<hashType> HashVector;
namespace MODINT_WITH_FIXED_MOD {
constexpr valueType MOD = 998244353;
template<typename T1, typename T2>
void Inc(T1 &a, T2 b) {
a = a + b;
if (a >= MOD)
a -= MOD;
}
template<typename T1, typename T2>
void Dec(T1 &a, T2 b) {
a = a - b;
if (a < 0)
a += MOD;
}
template<typename T1, typename T2>
T1 sum(T1 a, T2 b) {
return a + b >= MOD ? a + b - MOD : a + b;
}
template<typename T1, typename T2>
T1 sub(T1 a, T2 b) {
return a - b < 0 ? a - b + MOD : a - b;
}
template<typename T1, typename T2>
T1 mul(T1 a, T2 b) {
return (long long) a * b % MOD;
}
template<typename T1, typename T2>
void Mul(T1 &a, T2 b) {
a = (long long) a * b % MOD;
}
template<typename T1, typename T2>
T1 pow(T1 a, T2 b) {
T1 result = 1;
while (b > 0) {
if (b & 1)
Mul(result, a);
Mul(a, a);
b = b >> 1;
}
return result;
}
} // namespace MODINT_WITH_FIXED_MOD
using namespace MODINT_WITH_FIXED_MOD;
std::mt19937_64 Engine(std::chrono::steady_clock::now().time_since_epoch().count() ^ (size_t) std::make_unique<char>().get());
static const hashType MaskA = Engine(), MaskB = Engine(), MaskC = Engine();
hashType xor_shift(hashType n) {
n ^= MaskC;
n ^= n << 13;
n ^= n >> 7;
n ^= n << 17;
return n;
}
constexpr valueType B = 2e7;
valueType N, C, V;
ValueVector F, Memory;
void Init() {
F.resize(B + 1, 0);
Memory.resize(N / B + 1, -1);
}
void Prework() {
ValueVector Prime;
Prime.reserve(B);
HashVector Hash(B + 1, 0);
std::bitset<B + 1> isPrime;
isPrime.set();
for (valueType i = 2; i <= B; ++i) {
if (isPrime[i]) {
Prime.push_back(i);
Hash[i] = xor_shift(MaskB);
}
for (auto const &p : Prime) {
valueType const t = p * i;
if (t > B)
break;
isPrime[t] = false;
if (i % p == 0) {
Hash[t] = xor_shift(Hash[i] + MaskA);
break;
} else {
Hash[t] = xor_shift(Hash[i] + MaskB);
}
}
}
F[1] = pow(sub(1, C), MOD - 2);
std::unordered_map<hashType, valueType> Table;
for (valueType i = 2; i <= B; ++i) {
if (Table.count(Hash[i]) > 0) {
F[i] = Table[Hash[i]];
continue;
}
F[i] = F[1];
for (valueType j = 2; j * j <= i; ++j) {
if (i % j != 0)
continue;
Inc(F[i], F[j]);
if (j * j != i)
Inc(F[i], F[i / j]);
}
Mul(F[i], V);
Table[Hash[i]] = F[i];
}
for (valueType i = 2; i <= B; ++i)
Inc(F[i], F[i - 1]);
}
valueType Calc(valueType n) {
if (n <= B)
return F[n];
if (Memory[N / n] != -1)
return Memory[N / n];
valueType &result = Memory[N / n];
result = 0;
valueType l = 1;
while (l < n) {
valueType const x = n / l, r = std::min(n / x, n - 1);
Inc(result, mul((x - 1) % MOD, sub(Calc(r), Calc(l - 1))));
l = r + 1;
}
Mul(result, V);
Inc(result, F[1]);
return result;
}
int main() {
std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
#ifndef LOCAL_STDIO
freopen("bigben.in", "r", stdin);
freopen("bigben.out", "w", stdout);
#endif
std::cin >> N >> C;
if (C == 1) {
std::cout << (N <= 2 ? 1 : 0) << std::endl;
return 0;
}
V = mul(C, pow(sub(1, C), MOD - 2));
Init();
Prework();
valueType const ans = mul(Calc(N), pow(sub(1, C), N));
std::cout << ans << std::endl;
}